Files

189 lines
5.3 KiB
PHP
Raw Permalink Normal View History

<?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);
}
}
}