189 lines
5.3 KiB
PHP
189 lines
5.3 KiB
PHP
<?php
|
|
/**
|
|
* IP → 国家码跨请求缓存(Redis 优先,文件降级,默认 TTL 30 天)
|
|
*/
|
|
require_once __DIR__ . '/CountryCodeNormalizer.php';
|
|
|
|
class GeoIpCountryCache
|
|
{
|
|
const REDIS_PREFIX = 'cloak:geoip:cc:';
|
|
const NONE_SENTINEL = '__NONE__';
|
|
|
|
/** @var bool|null */
|
|
private static $redisAvailable = null;
|
|
|
|
/**
|
|
* @param string $ip
|
|
* @return string|false|null 命中返回国家码或 null(已知无国家);false=未命中
|
|
*/
|
|
public static function get($ip)
|
|
{
|
|
$ip = ClientIpResolver::normalizeIp($ip);
|
|
if ($ip === null) {
|
|
return false;
|
|
}
|
|
|
|
$redis = self::redisClient();
|
|
if ($redis) {
|
|
try {
|
|
$value = $redis->get(self::REDIS_PREFIX . $ip);
|
|
if ($value === false || $value === null) {
|
|
return false;
|
|
}
|
|
return self::normalizeCachedValue($value === self::NONE_SENTINEL ? null : (string) $value);
|
|
} catch (Throwable $e) {
|
|
self::logError('Redis get failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
return self::getFile($ip);
|
|
}
|
|
|
|
/**
|
|
* @param string $ip
|
|
* @param string|null $countryCode
|
|
*/
|
|
public static function set($ip, $countryCode)
|
|
{
|
|
$ip = ClientIpResolver::normalizeIp($ip);
|
|
if ($ip === null) {
|
|
return;
|
|
}
|
|
|
|
if ($countryCode !== null && $countryCode !== '') {
|
|
$normalized = CountryCodeNormalizer::toAlpha2($countryCode);
|
|
$value = $normalized !== null ? $normalized : strtoupper($countryCode);
|
|
} else {
|
|
$value = self::NONE_SENTINEL;
|
|
}
|
|
$ttl = self::cacheTtl();
|
|
|
|
$redis = self::redisClient();
|
|
if ($redis) {
|
|
try {
|
|
$redis->setex(self::REDIS_PREFIX . $ip, $ttl, $value);
|
|
return;
|
|
} catch (Throwable $e) {
|
|
self::logError('Redis set failed: ' . $e->getMessage());
|
|
}
|
|
}
|
|
|
|
self::setFile($ip, $value, $ttl);
|
|
}
|
|
|
|
private static function cacheTtl()
|
|
{
|
|
$ttl = defined('CLOAK_GEOIP_CACHE_TTL') ? (int) CLOAK_GEOIP_CACHE_TTL : 2592000;
|
|
return max(60, $ttl);
|
|
}
|
|
|
|
/**
|
|
* @return Redis|null
|
|
*/
|
|
private static function redisClient()
|
|
{
|
|
if (self::$redisAvailable === false) {
|
|
return null;
|
|
}
|
|
if (!extension_loaded('redis') || !defined('REDIS_ENABLED') || strtoupper(REDIS_ENABLED) !== 'ON') {
|
|
self::$redisAvailable = false;
|
|
return null;
|
|
}
|
|
try {
|
|
$r = new Redis();
|
|
$timeout = defined('REDIS_TIMEOUT') ? (float) REDIS_TIMEOUT : 1.0;
|
|
if (!@$r->connect(REDIS_HOST, (int) REDIS_PORT, $timeout)) {
|
|
self::$redisAvailable = false;
|
|
return null;
|
|
}
|
|
if (defined('REDIS_PASSWORD') && REDIS_PASSWORD !== '') {
|
|
$r->auth(REDIS_PASSWORD);
|
|
}
|
|
self::$redisAvailable = true;
|
|
return $r;
|
|
} catch (Throwable $e) {
|
|
self::$redisAvailable = false;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static function cacheDir()
|
|
{
|
|
$dir = dirname(__DIR__, 2) . '/storage/runtime/c7f2a9e1/cc_cache';
|
|
if (!is_dir($dir)) {
|
|
@mkdir($dir, 0755, true);
|
|
}
|
|
return $dir;
|
|
}
|
|
|
|
private static function cacheFilePath($ip)
|
|
{
|
|
return self::cacheDir() . '/' . hash('sha256', $ip) . '.json';
|
|
}
|
|
|
|
/**
|
|
* @param string $ip
|
|
* @return string|false|null
|
|
*/
|
|
private static function getFile($ip)
|
|
{
|
|
$path = self::cacheFilePath($ip);
|
|
if (!is_file($path)) {
|
|
return false;
|
|
}
|
|
$raw = @file_get_contents($path);
|
|
if ($raw === false || $raw === '') {
|
|
return false;
|
|
}
|
|
$data = json_decode($raw, true);
|
|
if (!is_array($data) || empty($data['expires_at']) || empty($data['value'])) {
|
|
@unlink($path);
|
|
return false;
|
|
}
|
|
if ((int) $data['expires_at'] < time()) {
|
|
@unlink($path);
|
|
return false;
|
|
}
|
|
return self::normalizeCachedValue($data['value'] === self::NONE_SENTINEL ? null : (string) $data['value']);
|
|
}
|
|
|
|
/**
|
|
* @param string|null $value
|
|
* @return string|null
|
|
*/
|
|
private static function normalizeCachedValue($value)
|
|
{
|
|
if ($value === null || $value === '') {
|
|
return null;
|
|
}
|
|
$normalized = CountryCodeNormalizer::toAlpha2($value);
|
|
return $normalized !== null ? $normalized : strtoupper((string) $value);
|
|
}
|
|
|
|
/**
|
|
* @param string $ip
|
|
* @param string $value
|
|
* @param int $ttl
|
|
*/
|
|
private static function setFile($ip, $value, $ttl)
|
|
{
|
|
$payload = json_encode([
|
|
'value' => $value,
|
|
'expires_at' => time() + $ttl,
|
|
], JSON_UNESCAPED_UNICODE);
|
|
if ($payload === false) {
|
|
return;
|
|
}
|
|
@file_put_contents(self::cacheFilePath($ip), $payload, LOCK_EX);
|
|
}
|
|
|
|
public static function logError($message)
|
|
{
|
|
$handle = @fopen(dirname(__DIR__, 2) . '/err.txt', 'a+');
|
|
if ($handle) {
|
|
fwrite($handle, date('Y-m-d H:i:s') . ' | GeoIpCountryCache | ' . $message . "\n");
|
|
fclose($handle);
|
|
}
|
|
}
|
|
}
|