修复IP归属地查询卡死bug:超时控制+前端AbortController

1. doQueryIpLocation增加单IP总超时5秒(防止4个API依次超时卡死)
2. 单API超时从3-5秒降至2-3秒
3. 批次从3个减为2个(最坏10秒,安全于30秒PHP限制)
4. 前端fetch增加AbortController 15秒超时
5. fetchIpApiWithTimeout默认超时改为3秒
This commit is contained in:
ypsdf1
2026-06-27 20:31:53 +08:00
parent fc42b777ca
commit c8c8db6220
2 changed files with 42 additions and 12 deletions
+7 -1
View File
@@ -1064,9 +1064,13 @@ function batchQueryIpLocations(ips) {
console.log('[BatchIP] Batch #' + batchNum + ' requesting:', batch); console.log('[BatchIP] Batch #' + batchNum + ' requesting:', batch);
const startTime = Date.now(); const startTime = Date.now();
// ★ 前端超时15秒:防止PHP挂起后fetch永远不返回
const controller = new AbortController();
const abortTimer = setTimeout(() => controller.abort(), 15000);
fetch('api/admin.php', { fetch('api/admin.php', {
method: 'POST', method: 'POST',
credentials: 'same-origin', credentials: 'same-origin',
signal: controller.signal,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest' 'X-Requested-With': 'XMLHttpRequest'
@@ -1077,6 +1081,7 @@ function batchQueryIpLocations(ips) {
}) })
}) })
.then(r => { .then(r => {
clearTimeout(abortTimer);
console.log('[BatchIP] Batch #' + batchNum + ' HTTP status:', r.status, 'time:', (Date.now() - startTime) + 'ms'); console.log('[BatchIP] Batch #' + batchNum + ' HTTP status:', r.status, 'time:', (Date.now() - startTime) + 'ms');
return r.text(); return r.text();
}) })
@@ -1097,7 +1102,8 @@ function batchQueryIpLocations(ips) {
setTimeout(queryNext, 300); setTimeout(queryNext, 300);
}) })
.catch(e => { .catch(e => {
console.error('[BatchIP] Batch #' + batchNum + ' fetch error:', e); clearTimeout(abortTimer);
console.error('[BatchIP] Batch #' + batchNum + ' fetch error:', e.name === 'AbortError' ? 'TIMEOUT (15s)' : e);
setTimeout(queryNext, 300); setTimeout(queryNext, 300);
}); });
} }
+35 -11
View File
@@ -751,6 +751,10 @@ function doQueryIpLocation($ip) {
static $cache = []; static $cache = [];
if (isset($cache[$ip])) return $cache[$ip]; if (isset($cache[$ip])) return $cache[$ip];
// ★ 单IP总超时控制:5秒内必须完成,防止4个API依次超时导致PHP挂起
$startTime = microtime(true);
$maxTimePerIp = 5; // 秒
// 收集所有结果,优先选有效格式 // 收集所有结果,优先选有效格式
$bestResult = null; $bestResult = null;
@@ -761,18 +765,24 @@ function doQueryIpLocation($ip) {
$r1Loc = trim($r1); $r1Loc = trim($r1);
@error_log("[IP_QUERY_API] PConline RESULT: $ip -> $r1Loc"); @error_log("[IP_QUERY_API] PConline RESULT: $ip -> $r1Loc");
if (isValidIpLocationFormat($r1Loc)) { if (isValidIpLocationFormat($r1Loc)) {
// 有效格式,直接返回
@error_log("[IP_QUERY_API] PConline VALID: $ip -> $r1Loc"); @error_log("[IP_QUERY_API] PConline VALID: $ip -> $r1Loc");
$cache[$ip] = ['location' => $r1Loc, 'ip' => $ip]; $cache[$ip] = ['location' => $r1Loc, 'ip' => $ip];
return $cache[$ip]; return $cache[$ip];
} else { } else {
@error_log("[IP_QUERY_API] PConline INVALID FORMAT (will try next): $ip -> $r1Loc"); @error_log("[IP_QUERY_API] PConline INVALID FORMAT (will try next): $ip -> $r1Loc");
$bestResult = $r1Loc; // 暂存,万一后面都没结果就用这个 $bestResult = $r1Loc;
} }
} else { } else {
@error_log("[IP_QUERY_API] PConline FAILED: $ip"); @error_log("[IP_QUERY_API] PConline FAILED: $ip");
} }
// ★ 总超时检查
if (microtime(true) - $startTime > $maxTimePerIp) {
@error_log("[IP_QUERY_API] TIMEOUT after PConline: $ip (elapsed=" . round(microtime(true) - $startTime, 1) . "s)");
$cache[$ip] = ['location' => $bestResult ?: '查询失败'];
return $cache[$ip];
}
// ========== 线路2: 百度OpenData ========== // ========== 线路2: 百度OpenData ==========
@error_log("[IP_QUERY_API] Trying Baidu: $ip"); @error_log("[IP_QUERY_API] Trying Baidu: $ip");
$r2 = fetchIpApiWithTimeout('https://opendata.baidu.com/api.php?query=' . urlencode($ip) . '&resource_id=6006&oe=utf8&format=json', function($d) { $r2 = fetchIpApiWithTimeout('https://opendata.baidu.com/api.php?query=' . urlencode($ip) . '&resource_id=6006&oe=utf8&format=json', function($d) {
@@ -782,7 +792,7 @@ function doQueryIpLocation($ip) {
if ($loc && $loc !== '--' && strpos($loc, '中国 ') !== 0) return $loc; if ($loc && $loc !== '--' && strpos($loc, '中国 ') !== 0) return $loc;
} }
return null; return null;
}, 3); }, 2);
if ($r2 !== null) { if ($r2 !== null) {
@error_log("[IP_QUERY_API] Baidu RESULT: $ip -> $r2"); @error_log("[IP_QUERY_API] Baidu RESULT: $ip -> $r2");
if (isValidIpLocationFormat($r2)) { if (isValidIpLocationFormat($r2)) {
@@ -797,6 +807,13 @@ function doQueryIpLocation($ip) {
@error_log("[IP_QUERY_API] Baidu FAILED: $ip"); @error_log("[IP_QUERY_API] Baidu FAILED: $ip");
} }
// ★ 总超时检查
if (microtime(true) - $startTime > $maxTimePerIp) {
@error_log("[IP_QUERY_API] TIMEOUT after Baidu: $ip (elapsed=" . round(microtime(true) - $startTime, 1) . "s)");
$cache[$ip] = ['location' => $bestResult ?: '查询失败'];
return $cache[$ip];
}
// ========== 线路3: aa1.cn ========== // ========== 线路3: aa1.cn ==========
@error_log("[IP_QUERY_API] Trying aa1.cn: $ip"); @error_log("[IP_QUERY_API] Trying aa1.cn: $ip");
$r3 = fetchIpApiWithTimeout('https://v.api.aa1.cn/api/ipcha-baidu/?ip=' . urlencode($ip), function($d) { $r3 = fetchIpApiWithTimeout('https://v.api.aa1.cn/api/ipcha-baidu/?ip=' . urlencode($ip), function($d) {
@@ -806,7 +823,7 @@ function doQueryIpLocation($ip) {
if ($loc && $loc !== '未知' && strpos($loc, '中国 ') !== 0) return $loc; if ($loc && $loc !== '未知' && strpos($loc, '中国 ') !== 0) return $loc;
} }
return null; return null;
}, 3); }, 2);
if ($r3 !== null) { if ($r3 !== null) {
@error_log("[IP_QUERY_API] aa1.cn RESULT: $ip -> $r3"); @error_log("[IP_QUERY_API] aa1.cn RESULT: $ip -> $r3");
if (isValidIpLocationFormat($r3)) { if (isValidIpLocationFormat($r3)) {
@@ -821,6 +838,13 @@ function doQueryIpLocation($ip) {
@error_log("[IP_QUERY_API] aa1.cn FAILED: $ip"); @error_log("[IP_QUERY_API] aa1.cn FAILED: $ip");
} }
// ★ 总超时检查
if (microtime(true) - $startTime > $maxTimePerIp) {
@error_log("[IP_QUERY_API] TIMEOUT after aa1.cn: $ip (elapsed=" . round(microtime(true) - $startTime, 1) . "s)");
$cache[$ip] = ['location' => $bestResult ?: '查询失败'];
return $cache[$ip];
}
// ========== 线路4: IP9(兜底,格式最准) ========== // ========== 线路4: IP9(兜底,格式最准) ==========
@error_log("[IP_QUERY_API] Trying IP9 (fallback): $ip"); @error_log("[IP_QUERY_API] Trying IP9 (fallback): $ip");
$ip9Result = queryIpLocationFromIp9($ip); $ip9Result = queryIpLocationFromIp9($ip);
@@ -840,7 +864,7 @@ function doQueryIpLocation($ip) {
} }
// ★ 所有接口都没返回有效格式:返回"查询失败",不返回无效格式 // ★ 所有接口都没返回有效格式:返回"查询失败",不返回无效格式
@error_log("[IP_QUERY_API] ALL FAILED/INVALID: $ip (bestResult=" . ($bestResult ?: 'null') . ")"); @error_log("[IP_QUERY_API] ALL FAILED/INVALID: $ip (bestResult=" . ($bestResult ?: 'null') . ", elapsed=" . round(microtime(true) - $startTime, 1) . "s)");
$cache[$ip] = ['location' => '查询失败']; $cache[$ip] = ['location' => '查询失败'];
return $cache[$ip]; return $cache[$ip];
} }
@@ -956,7 +980,7 @@ function queryIpLocationWithTimeout($ip, $timeout) {
/** /**
* 带超时的通用HTTP请求封装 * 带超时的通用HTTP请求封装
*/ */
function fetchIpApiWithTimeout($url, $parser, $timeout = 5) { function fetchIpApiWithTimeout($url, $parser, $timeout = 3) {
$ch = curl_init(); $ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
@@ -988,7 +1012,7 @@ function fetchPconline($ip) {
$ctx = stream_context_create([ $ctx = stream_context_create([
'http' => [ 'http' => [
'method' => 'GET', 'method' => 'GET',
'timeout' => 3, 'timeout' => 2,
'ignore_errors' => true, 'ignore_errors' => true,
'header' => "Accept: application/json\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 SDF1_IP_Lookup/1.0" 'header' => "Accept: application/json\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 SDF1_IP_Lookup/1.0"
] ]
@@ -1057,7 +1081,7 @@ function queryIpLocationFromIp9($ip) {
$ctx = stream_context_create([ $ctx = stream_context_create([
'http' => [ 'http' => [
'method' => 'GET', 'method' => 'GET',
'timeout' => 5, 'timeout' => 3,
'ignore_errors' => true, 'ignore_errors' => true,
'header' => "Accept: application/json, text/plain, */*\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\nReferer: https://ip9.com.cn/\r\nOrigin: https://ip9.com.cn" 'header' => "Accept: application/json, text/plain, */*\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\nReferer: https://ip9.com.cn/\r\nOrigin: https://ip9.com.cn"
], ],
@@ -1828,8 +1852,8 @@ function adminBatchQueryIps() {
$ips = array_map('trim', $ips); $ips = array_map('trim', $ips);
} }
// 限制每次最多3个 // 限制每次最多2个(避免单批超时:每个IP最多5秒 × 2 = 10秒,安全于30秒PHP限制)
$batch = array_slice($ips, 0, 3); $batch = array_slice($ips, 0, 2);
$results = []; $results = [];
foreach ($batch as $ip) { foreach ($batch as $ip) {
@@ -1843,7 +1867,7 @@ function adminBatchQueryIps() {
} }
// 返回结果和剩余未查询的IP // 返回结果和剩余未查询的IP
$remaining = array_slice($ips, 3); $remaining = array_slice($ips, 2);
exit(json_encode([ exit(json_encode([
'success' => true, 'success' => true,
'data' => $results, 'data' => $results,