232 lines
8.0 KiB
PHP
232 lines
8.0 KiB
PHP
<?php
|
|
/**
|
|
* AKSRT IP Geolocation Service
|
|
*
|
|
* 提供 IP 归属地查询能力。支持多 provider、transient 缓存和统一状态输出。
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
class AKSRT_IP_Geo {
|
|
|
|
private static $instance = null;
|
|
|
|
public static function get_instance(): self {
|
|
if (null === self::$instance) self::$instance = new self();
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* 查询 IP 归属地
|
|
*
|
|
* @param string $ip IPv4 or IPv6
|
|
* @param bool $force 管理端测试用:跳过「启用」开关与缓存,仍拒绝私有 IP
|
|
* @return array{country: string, province: string, city: string, raw: array, reason?: string}
|
|
*/
|
|
public function lookup(string $ip, bool $force = false): array {
|
|
$config = $this->get_config();
|
|
|
|
if (!$force && empty($config['ip_geo_enabled'])) {
|
|
return $this->empty_result('disabled');
|
|
}
|
|
|
|
// 本地/私有 IP 跳过查询
|
|
if ($this->is_private_ip($ip)) {
|
|
return $this->empty_result('private_ip');
|
|
}
|
|
|
|
$cache_key = 'aksrt_ip_geo_' . md5($ip);
|
|
$ttl = max(3600, (int) ($config['ip_geo_cache_ttl'] ?? 86400));
|
|
if (!$force) {
|
|
$cached = get_transient($cache_key);
|
|
if ($cached !== false && is_array($cached)) {
|
|
return $cached;
|
|
}
|
|
}
|
|
|
|
$provider = sanitize_key((string) ($config['ip_addr_sdk'] ?? ($config['ip_geo_provider'] ?? 'null')));
|
|
$result = $this->query_provider($provider, $ip, $config);
|
|
|
|
if (!empty($result['province']) || !empty($result['city']) || !empty($result['country'])) {
|
|
if (!$force) {
|
|
set_transient($cache_key, $result, $ttl);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
if (empty($result['reason'])) {
|
|
$result['reason'] = 'provider_empty';
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 获取服务运行时状态
|
|
*/
|
|
public function get_status(): array {
|
|
$config = $this->get_config();
|
|
$provider = sanitize_key((string) ($config['ip_addr_sdk'] ?? ($config['ip_geo_provider'] ?? 'null')));
|
|
$enabled = !empty($config['ip_geo_enabled']);
|
|
|
|
$missing = [];
|
|
if ($enabled && $provider === 'qq') {
|
|
if (empty($config['ip_addr_sdk_qq_appkey'])) $missing[] = 'ip_addr_sdk_qq_appkey';
|
|
if (empty($config['ip_addr_sdk_qq_secretkey'])) $missing[] = 'ip_addr_sdk_qq_secretkey';
|
|
}
|
|
if ($enabled && $provider === 'amap') {
|
|
if (empty($config['ip_addr_sdk_amap_appkey'])) $missing[] = 'ip_addr_sdk_amap_appkey';
|
|
if (empty($config['ip_addr_sdk_amap_secretkey'])) $missing[] = 'ip_addr_sdk_amap_secretkey';
|
|
}
|
|
|
|
return [
|
|
'enabled' => $enabled,
|
|
'provider' => $provider,
|
|
'configured' => $enabled && empty($missing),
|
|
'available' => $enabled,
|
|
'verified' => !empty($config['ip_geo_verified']),
|
|
'transport' => 'http_api',
|
|
'missing_fields' => $missing,
|
|
'cache_ttl' => (int) ($config['ip_geo_cache_ttl'] ?? 86400),
|
|
'display' => (array) ($config['ip_geo_display'] ?? ['comment']),
|
|
'format' => (string) ($config['ip_geo_format'] ?? 'province_city'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 格式化归属地显示文本
|
|
*/
|
|
public function format(array $geo, string $format = ''): string {
|
|
if ($format === '') {
|
|
$config = $this->get_config();
|
|
$format = (string) ($config['ip_geo_format'] ?? 'province_city');
|
|
}
|
|
|
|
switch ($format) {
|
|
case 'province':
|
|
return $geo['province'] ?? '';
|
|
case 'country_province':
|
|
return trim(($geo['country'] ?? '') . ' ' . ($geo['province'] ?? ''));
|
|
case 'province_city':
|
|
default:
|
|
$parts = array_filter([$geo['province'] ?? '', $geo['city'] ?? '']);
|
|
return implode(' ', $parts) ?: ($geo['country'] ?? '');
|
|
}
|
|
}
|
|
|
|
private function get_config(): array {
|
|
if (class_exists('AKSRT_Settings')) {
|
|
return AKSRT_Settings::get_ip_geo_settings();
|
|
}
|
|
return ['ip_geo_enabled' => false];
|
|
}
|
|
|
|
private function query_provider(string $provider, string $ip, array $config): array {
|
|
switch ($provider) {
|
|
case 'qq':
|
|
return $this->query_qq($ip, $config);
|
|
case 'amap':
|
|
return $this->query_amap($ip, $config);
|
|
case 'polling':
|
|
foreach (['qq', 'amap', 'null'] as $fallback) {
|
|
$result = $this->query_provider($fallback, $ip, $config);
|
|
if (!empty($result['province']) || !empty($result['city']) || !empty($result['country'])) {
|
|
return $result;
|
|
}
|
|
}
|
|
return $this->empty_result();
|
|
case 'null':
|
|
default:
|
|
return $this->query_ip_api($ip);
|
|
}
|
|
}
|
|
|
|
private function query_qq(string $ip, array $config): array {
|
|
$appkey = trim((string) ($config['ip_addr_sdk_qq_appkey'] ?? ''));
|
|
if ($appkey === '') {
|
|
return $this->empty_result();
|
|
}
|
|
|
|
$url = add_query_arg([
|
|
'ip' => $ip,
|
|
'key' => $appkey,
|
|
], 'https://apis.map.qq.com/ws/location/v1/ip');
|
|
|
|
$response = wp_remote_get($url, [
|
|
'timeout' => 8,
|
|
'headers' => ['Accept' => 'application/json'],
|
|
]);
|
|
if (is_wp_error($response)) return $this->empty_result();
|
|
|
|
$data = json_decode(wp_remote_retrieve_body($response), true);
|
|
if (!is_array($data) || (int) ($data['status'] ?? 1) !== 0) return $this->empty_result();
|
|
|
|
$info = $data['result']['ad_info'] ?? [];
|
|
return [
|
|
'country' => '中国',
|
|
'province' => (string) ($info['province'] ?? ''),
|
|
'city' => (string) ($info['city'] ?? ''),
|
|
'raw' => $data,
|
|
];
|
|
}
|
|
|
|
private function query_amap(string $ip, array $config): array {
|
|
$appkey = trim((string) ($config['ip_addr_sdk_amap_appkey'] ?? ''));
|
|
if ($appkey === '') {
|
|
return $this->empty_result();
|
|
}
|
|
|
|
$url = add_query_arg([
|
|
'ip' => $ip,
|
|
'key' => $appkey,
|
|
], 'https://restapi.amap.com/v3/ip');
|
|
|
|
$response = wp_remote_get($url, [
|
|
'timeout' => 8,
|
|
'headers' => ['Accept' => 'application/json'],
|
|
]);
|
|
if (is_wp_error($response)) return $this->empty_result();
|
|
|
|
$data = json_decode(wp_remote_retrieve_body($response), true);
|
|
if (!is_array($data) || (string) ($data['status'] ?? '0') !== '1') return $this->empty_result();
|
|
|
|
return [
|
|
'country' => '中国',
|
|
'province' => (string) ($data['province'] ?? ''),
|
|
'city' => (string) ($data['city'] ?? ''),
|
|
'raw' => $data,
|
|
];
|
|
}
|
|
|
|
private function query_ip_api(string $ip): array {
|
|
$response = wp_remote_get("http://ip-api.com/json/{$ip}?lang=zh-CN", [
|
|
'timeout' => 5,
|
|
]);
|
|
if (is_wp_error($response)) return $this->empty_result();
|
|
|
|
$data = json_decode(wp_remote_retrieve_body($response), true);
|
|
if (!is_array($data) || ($data['status'] ?? '') !== 'success') return $this->empty_result();
|
|
|
|
return [
|
|
'country' => (string) ($data['country'] ?? ''),
|
|
'province' => (string) ($data['regionName'] ?? ''),
|
|
'city' => (string) ($data['city'] ?? ''),
|
|
'raw' => $data,
|
|
];
|
|
}
|
|
|
|
private function is_private_ip(string $ip): bool {
|
|
return !filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
|
|
}
|
|
|
|
private function empty_result(string $reason = ''): array {
|
|
$out = ['country' => '', 'province' => '', 'city' => '', 'raw' => []];
|
|
if ($reason !== '') {
|
|
$out['reason'] = $reason;
|
|
}
|
|
return $out;
|
|
}
|
|
}
|