|
|
@@ -0,0 +1,121 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Helpers;
|
|
|
+
|
|
|
+use Illuminate\Support\Facades\Cache;
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 国家/地区:按 IP 解析(带缓存),用于登录日志回填展示
|
|
|
+ */
|
|
|
+class IpCountry
|
|
|
+{
|
|
|
+ private const CACHE_TTL = 86400;
|
|
|
+ private const CACHE_PREFIX = 'ip_country_v2_';
|
|
|
+
|
|
|
+ private const CODE_NAME_MAP = [
|
|
|
+ 'CN' => '中国',
|
|
|
+ 'HK' => '香港',
|
|
|
+ 'MO' => '澳门',
|
|
|
+ 'TW' => '台湾',
|
|
|
+ 'US' => '美国',
|
|
|
+ 'JP' => '日本',
|
|
|
+ 'KR' => '韩国',
|
|
|
+ 'SG' => '新加坡',
|
|
|
+ 'MY' => '马来西亚',
|
|
|
+ 'TH' => '泰国',
|
|
|
+ 'VN' => '越南',
|
|
|
+ 'ID' => '印尼',
|
|
|
+ 'PH' => '菲律宾',
|
|
|
+ 'KH' => '柬埔寨',
|
|
|
+ 'MM' => '缅甸',
|
|
|
+ 'IN' => '印度',
|
|
|
+ 'GB' => '英国',
|
|
|
+ 'AU' => '澳大利亚',
|
|
|
+ 'CA' => '加拿大',
|
|
|
+ 'DE' => '德国',
|
|
|
+ 'FR' => '法国',
|
|
|
+ 'RU' => '俄罗斯',
|
|
|
+ 'BR' => '巴西',
|
|
|
+ 'AE' => '阿联酋',
|
|
|
+ ];
|
|
|
+
|
|
|
+ public static function resolve(string $ip): string
|
|
|
+ {
|
|
|
+ $ip = trim($ip);
|
|
|
+ if ($ip === '') {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ if (in_array($ip, ['127.0.0.1', '::1'], true)) {
|
|
|
+ return '本地';
|
|
|
+ }
|
|
|
+ if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ $cacheKey = self::CACHE_PREFIX . md5($ip);
|
|
|
+ $cached = Cache::get($cacheKey);
|
|
|
+ if (is_string($cached)) {
|
|
|
+ return $cached;
|
|
|
+ }
|
|
|
+
|
|
|
+ $country = self::lookup($ip);
|
|
|
+ Cache::put($cacheKey, $country, self::CACHE_TTL);
|
|
|
+ return $country;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 批量解析,减少列表页重复请求
|
|
|
+ *
|
|
|
+ * @param array<int, string> $ips
|
|
|
+ * @return array<string, string> ip => country
|
|
|
+ */
|
|
|
+ public static function resolveMany(array $ips): array
|
|
|
+ {
|
|
|
+ $result = [];
|
|
|
+ foreach (array_unique(array_filter(array_map('trim', $ips))) as $ip) {
|
|
|
+ $result[$ip] = self::resolve($ip);
|
|
|
+ }
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected static function lookup(string $ip): string
|
|
|
+ {
|
|
|
+ $url = 'http://ip-api.com/json/' . rawurlencode($ip)
|
|
|
+ . '?fields=status,country,countryCode,regionName,city&lang=zh-CN';
|
|
|
+
|
|
|
+ try {
|
|
|
+ $ctx = stream_context_create([
|
|
|
+ 'http' => [
|
|
|
+ 'timeout' => 2,
|
|
|
+ 'method' => 'GET',
|
|
|
+ ],
|
|
|
+ ]);
|
|
|
+ $raw = @file_get_contents($url, false, $ctx);
|
|
|
+ if ($raw === false || $raw === '') {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ $data = json_decode($raw, true);
|
|
|
+ if (!is_array($data) || ($data['status'] ?? '') !== 'success') {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ $code = strtoupper((string)($data['countryCode'] ?? ''));
|
|
|
+ $country = (string)($data['country'] ?? '');
|
|
|
+ if ($country === '' && $code !== '' && isset(self::CODE_NAME_MAP[$code])) {
|
|
|
+ $country = self::CODE_NAME_MAP[$code];
|
|
|
+ }
|
|
|
+
|
|
|
+ $parts = array_filter([
|
|
|
+ $country,
|
|
|
+ (string)($data['regionName'] ?? ''),
|
|
|
+ (string)($data['city'] ?? ''),
|
|
|
+ ], static fn ($v) => $v !== '');
|
|
|
+
|
|
|
+ return implode(' ', array_unique($parts));
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ Log::warning('ip_country_lookup_failed', ['ip' => $ip, 'error' => $e->getMessage()]);
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|