From c8c8db6220911a761577bde1e4cada018bc395f1 Mon Sep 17 00:00:00 2001 From: ypsdf1 Date: Sat, 27 Jun 2026 20:31:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DIP=E5=BD=92=E5=B1=9E=E5=9C=B0?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E5=8D=A1=E6=AD=BBbug=EF=BC=9A=E8=B6=85?= =?UTF-8?q?=E6=97=B6=E6=8E=A7=E5=88=B6+=E5=89=8D=E7=AB=AFAbortController?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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秒 --- admin.php | 8 +++++++- api/admin.php | 46 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/admin.php b/admin.php index fccf900..bb8c624 100644 --- a/admin.php +++ b/admin.php @@ -1064,9 +1064,13 @@ function batchQueryIpLocations(ips) { console.log('[BatchIP] Batch #' + batchNum + ' requesting:', batch); const startTime = Date.now(); + // ★ 前端超时15秒:防止PHP挂起后fetch永远不返回 + const controller = new AbortController(); + const abortTimer = setTimeout(() => controller.abort(), 15000); fetch('api/admin.php', { method: 'POST', credentials: 'same-origin', + signal: controller.signal, headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' @@ -1077,6 +1081,7 @@ function batchQueryIpLocations(ips) { }) }) .then(r => { + clearTimeout(abortTimer); console.log('[BatchIP] Batch #' + batchNum + ' HTTP status:', r.status, 'time:', (Date.now() - startTime) + 'ms'); return r.text(); }) @@ -1097,7 +1102,8 @@ function batchQueryIpLocations(ips) { setTimeout(queryNext, 300); }) .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); }); } diff --git a/api/admin.php b/api/admin.php index 9ac274e..df30e30 100644 --- a/api/admin.php +++ b/api/admin.php @@ -751,6 +751,10 @@ function doQueryIpLocation($ip) { static $cache = []; if (isset($cache[$ip])) return $cache[$ip]; + // ★ 单IP总超时控制:5秒内必须完成,防止4个API依次超时导致PHP挂起 + $startTime = microtime(true); + $maxTimePerIp = 5; // 秒 + // 收集所有结果,优先选有效格式 $bestResult = null; @@ -761,18 +765,24 @@ function doQueryIpLocation($ip) { $r1Loc = trim($r1); @error_log("[IP_QUERY_API] PConline RESULT: $ip -> $r1Loc"); if (isValidIpLocationFormat($r1Loc)) { - // 有效格式,直接返回 @error_log("[IP_QUERY_API] PConline VALID: $ip -> $r1Loc"); $cache[$ip] = ['location' => $r1Loc, 'ip' => $ip]; return $cache[$ip]; } else { @error_log("[IP_QUERY_API] PConline INVALID FORMAT (will try next): $ip -> $r1Loc"); - $bestResult = $r1Loc; // 暂存,万一后面都没结果就用这个 + $bestResult = $r1Loc; } } else { @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 ========== @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) { @@ -782,7 +792,7 @@ function doQueryIpLocation($ip) { if ($loc && $loc !== '--' && strpos($loc, '中国 ') !== 0) return $loc; } return null; - }, 3); + }, 2); if ($r2 !== null) { @error_log("[IP_QUERY_API] Baidu RESULT: $ip -> $r2"); if (isValidIpLocationFormat($r2)) { @@ -797,6 +807,13 @@ function doQueryIpLocation($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 ========== @error_log("[IP_QUERY_API] Trying aa1.cn: $ip"); $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; } return null; - }, 3); + }, 2); if ($r3 !== null) { @error_log("[IP_QUERY_API] aa1.cn RESULT: $ip -> $r3"); if (isValidIpLocationFormat($r3)) { @@ -821,6 +838,13 @@ function doQueryIpLocation($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(兜底,格式最准) ========== @error_log("[IP_QUERY_API] Trying IP9 (fallback): $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' => '查询失败']; return $cache[$ip]; } @@ -956,7 +980,7 @@ function queryIpLocationWithTimeout($ip, $timeout) { /** * 带超时的通用HTTP请求封装 */ -function fetchIpApiWithTimeout($url, $parser, $timeout = 5) { +function fetchIpApiWithTimeout($url, $parser, $timeout = 3) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); @@ -988,7 +1012,7 @@ function fetchPconline($ip) { $ctx = stream_context_create([ 'http' => [ 'method' => 'GET', - 'timeout' => 3, + 'timeout' => 2, '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" ] @@ -1057,7 +1081,7 @@ function queryIpLocationFromIp9($ip) { $ctx = stream_context_create([ 'http' => [ 'method' => 'GET', - 'timeout' => 5, + 'timeout' => 3, '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" ], @@ -1828,8 +1852,8 @@ function adminBatchQueryIps() { $ips = array_map('trim', $ips); } - // 限制每次最多3个 - $batch = array_slice($ips, 0, 3); + // 限制每次最多2个(避免单批超时:每个IP最多5秒 × 2 = 10秒,安全于30秒PHP限制) + $batch = array_slice($ips, 0, 2); $results = []; foreach ($batch as $ip) { @@ -1843,7 +1867,7 @@ function adminBatchQueryIps() { } // 返回结果和剩余未查询的IP - $remaining = array_slice($ips, 3); + $remaining = array_slice($ips, 2); exit(json_encode([ 'success' => true, 'data' => $results,