From 52e07ec9de0dc09e74507837e2cad0f6895ccc83 Mon Sep 17 00:00:00 2001 From: ypsdf1 Date: Tue, 29 Sep 2026 18:03:32 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20SN=E5=9B=9E=E6=89=A7=E5=B9=82=E7=AD=89?= =?UTF-8?q?=E9=98=B2=E6=AD=A2=E9=87=8D=E5=A4=8D=E8=A1=A5=E5=8F=91=EF=BC=8C?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E5=91=BD=E4=BB=A4=E4=B8=8B=E5=8F=91=E5=90=8E?= =?UTF-8?q?=E8=BD=AE=E8=AF=A2=E5=9B=9E=E6=89=A7=E7=9B=B4=E5=88=B0=E5=87=BA?= =?UTF-8?q?=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin.php | 35 +++++++++++++++++++++++++++++++++++ api/sn.php | 8 +++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/admin.php b/admin.php index ccb0e08..257c9e6 100644 --- a/admin.php +++ b/admin.php @@ -4258,11 +4258,46 @@ async function snAdmCmd(cmd) { if (!r.success) { toast(r.message || '下发失败', 'err'); return; } toast(r.message || '命令已下发', 'ok'); setTimeout(() => renderSnAdmin(document.getElementById('C')), 1200); + // ★ 游戏服回执是异步的(原先要等下一轮 Timer C,10~2 分钟不等)。 + // 原实现只在 1.2 秒后刷新一次,那时 status 还是 pending、result 为空, + // 看起来就像"没动作 / 结果没返回"。这里持续轮询该命令回执直到出结果。 + const cmdId = (r.data && r.data.id) || 0; + if (cmdId) snAdmPollCmd(cmdId, sn, 0); } catch (e) { toast('下发失败: ' + e.message, 'err'); } } +/** + * 轮询 SN 命令回执(3 秒一次,最长 150 秒)。 + * 游戏服执行完会把 web_sn_commands.status 置为 done 并写入 result; + * 3 分钟未回执则被后端标记为 timeout。 + */ +function snAdmPollCmd(cmdId, sn, tries) { + const MAX_TRIES = 50; // 50 * 3s = 150 秒 + setTimeout(async () => { + if (snAdm.tab !== 'detail' || snAdm.sn !== sn) return; // 用户已离开该详情页 + let c = null; + try { + const r = await snAdmApi('admin_detail', { sn: sn }); + const list = (r.success && r.data && r.data.commands) || []; + c = list.find(x => Number(x.id) === Number(cmdId)) || null; + } catch (e) { /* 网络抖动,下一轮再试 */ } + if (c && (c.status === 'done' || c.status === 'timeout')) { + if (c.status === 'timeout') toast('游戏服 3 分钟内未回执,命令已超时', 'err'); + else toast('游戏服已执行:' + (c.result || '(无结果)'), 'ok'); + renderSnAdmin(document.getElementById('C')); + return; + } + if (tries < MAX_TRIES) { + snAdmPollCmd(cmdId, sn, tries + 1); + } else { + toast('游戏服仍未回执,可稍后刷新查看', 'err'); + renderSnAdmin(document.getElementById('C')); + } + }, 3000); +} + async function renderSnLost(el) { const r = await snAdmApi('admin_lost_list', { page: 1, size: 100 }); if (!r.success) { el.innerHTML = '

' + escAdmHtml(r.message || '加载失败') + '

'; return; } diff --git a/api/sn.php b/api/sn.php index a02cab1..80c4c0e 100644 --- a/api/sn.php +++ b/api/sn.php @@ -493,7 +493,8 @@ function snAckCommands() { $itemType = ''; $linkType = ''; $linkId = 0; - $res = $db->prepare("SELECT cmd, sn, player, item_type, link_type, link_id + $alreadyDone = false; + $res = $db->prepare("SELECT cmd, sn, player, item_type, link_type, link_id, status FROM web_sn_commands WHERE id = :id"); $res->bindValue(':id', $id, SQLITE3_INTEGER); $rr = $res->execute(); @@ -504,8 +505,13 @@ function snAckCommands() { $itemType = (string)$row['item_type']; $linkType = (string)$row['link_type']; $linkId = (int)$row['link_id']; + $alreadyDone = ((string)$row['status'] === 'done'); } + // ★ 幂等:Java 侧只有确认收到才从本地回执缓冲移除,网络抖动会原样重发。 + // 已处理过的直接跳过,否则报失核查通过会再入队一次补发、注销会重置冷静期。 + if ($alreadyDone) continue; + $u = $db->prepare("UPDATE web_sn_commands SET status='done', result=:r, done_at=:t WHERE id=:id"); $u->bindValue(':r', $result, SQLITE3_TEXT);