From f2859fe6e5818bbfff034285459ac2652bbbafab Mon Sep 17 00:00:00 2001 From: ypsdf1 Date: Fri, 23 Oct 2026 23:01:59 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC53=E6=89=B9=EF=BC=9A=E7=BD=91=E9=A1=B5?= =?UTF-8?q?=E7=BB=91=E9=82=AE=E7=AE=B1=E4=B8=89=E6=8E=A5=E5=8F=A3+?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=90=91=E5=AF=BC=E2=80=94=E2=80=94=E4=B8=8D?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=E6=94=B9=E6=B8=B8=E6=88=8F=E5=86=8560?= =?UTF-8?q?=E7=A7=92=E6=89=B9=E5=87=86=EF=BC=8C=E5=AF=86=E7=A0=81=E5=A4=8D?= =?UTF-8?q?=E6=A0=B8+IP=E8=BF=9E=E9=94=995=E6=AC=A1=E5=B0=815=E5=88=86?= =?UTF-8?q?=E9=92=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/account.php | 200 +++++++++++++++++++++++++++++++++++++++++++++++- api/sync.php | 120 +++++++++++++++++++++++++++++ player.php | 133 +++++++++++++++++++++++++++++++- 3 files changed, 449 insertions(+), 4 deletions(-) diff --git a/api/account.php b/api/account.php index 43475e1..4d0f1ba 100644 --- a/api/account.php +++ b/api/account.php @@ -62,6 +62,9 @@ try { case 'totp_unbind': acctTotpUnbind(); break; case 'recovery_list': acctRecoveryList(); break; case 'recovery_regenerate': acctRecoveryRegen(); break; + case 'bind_email_start': acctBindEmailStart(); break; + case 'bind_email_poll': acctBindEmailPoll(); break; + case 'bind_email_confirm': acctBindEmailConfirm(); break; default: error('未知操作: ' . $action); } @@ -271,6 +274,19 @@ function acctEnsureTables() { )"); // 顺手清理过期超过 1 天的会话行 $db->exec("DELETE FROM web_account_verify WHERE expire_at < " . (time() - 86400)); + // ★第53批:网页绑定邮箱请求表(schema 必须与 sync.php checkPendingWebEmailBindRequests 完全一致) + $db->exec("CREATE TABLE IF NOT EXISTS web_email_bind_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_name TEXT NOT NULL, + email TEXT NOT NULL, + ip_address TEXT DEFAULT '', + status TEXT DEFAULT 'pending', + created_at INTEGER NOT NULL, + expire_at INTEGER NOT NULL, + approved_at INTEGER DEFAULT 0, + processed_at INTEGER DEFAULT 0, + fail_reason TEXT DEFAULT '' + )"); } // ===================== 公共辅助 ===================== @@ -457,7 +473,7 @@ function acctStatus() { $hint = ''; if (!$methods) { - $hint = '账号未绑定邮箱,也未绑定二次验证器 —— 无法完成第二层验证。请先在游戏中使用 /bindemail 绑定邮箱,否则无法修改密码等账号信息。'; + $hint = '账号未绑定邮箱,也未绑定二次验证器 —— 无法完成第二层验证。可在下方点击「网页绑定邮箱」,游戏内批准后即可完成绑定。'; } success([ @@ -495,7 +511,8 @@ function acctVerifyStart() { $email = acctEmailOf($player); $methods = acctMethods($player, $email); if (!$methods) { - error('无法完成第二层验证:账号未绑定邮箱且未绑定二次验证器,请先在游戏中使用 /bindemail 绑定邮箱'); + // ★第53批:不再硬拦截——密码第一层已过,返回导航标记让前端直接进入「网页绑定邮箱」定向导 + jsonResponse(['success' => false, 'message' => '账号未绑定邮箱且未绑定二次验证器,请先完成邮箱绑定', 'need_bind_email' => true], 409); } $challenge = acctIssueToken($player, 'challenge', 300, json_encode($methods, JSON_UNESCAPED_UNICODE), acctClientIp()); @@ -841,3 +858,182 @@ function acctRecoveryRegen() { debugLog('[account.php] 恢复代码已重新生成', ['player' => $player, 'ip' => acctClientIp()]); success(['recovery_codes' => $codes], '恢复代码已重新生成,旧代码全部作废'); } + +// ===================== 第53批:网页端绑定邮箱(游戏内批准流) ===================== +// +// 流程:bind_email_start 建 60 秒请求(要求玩家在线)→ Java 定时器A 拉取并游戏内发 +// clickable 通知(/webbind approve|reject )→ sync.php web_email_bind_result 回写 +// approved/rejected/offline/expired → bind_email_poll 前端轮询 → approved 后 +// bind_email_confirm 复核登录密码才写 users.email。 +// 防盗绑三重:①游戏内本人批准 ②批准后重验登录密码 ③IP 连错 5 次封 5 分钟。 + +/** 玩家当前是否在线:online_players 有行 且 Java 心跳(online_player_hb)<=120秒(Java 死了通知发不出,等同不在线) */ +function acctPlayerOnlineNow($player) { + $db = getDB(); + try { + $db->exec("CREATE TABLE IF NOT EXISTS online_players (player_name TEXT PRIMARY KEY, login_time INTEGER DEFAULT 0)"); + $db->exec("CREATE TABLE IF NOT EXISTS online_player_hb (id INTEGER PRIMARY KEY DEFAULT 1, last_seen INTEGER DEFAULT 0)"); + $hb = $db->query("SELECT last_seen FROM online_player_hb WHERE id = 1")->fetchArray(SQLITE3_ASSOC); + if (!$hb || time() - (int)$hb['last_seen'] > 120) return false; + $stmt = $db->prepare("SELECT 1 FROM online_players WHERE player_name = :p"); + $stmt->bindValue(':p', $player, SQLITE3_TEXT); + return ($stmt->execute()->fetchArray() !== false); + } catch (\Throwable $e) { + @error_log("[acctPlayerOnlineNow] error: " . $e->getMessage()); + return false; + } +} + +/** 发起绑定:校验邮箱格式 + 在线 → 建 60 秒请求(同玩家旧未终结请求全部作废) */ +function acctBindEmailStart() { + $player = acctAuthPlayer(); + $email = strtolower(trim((string)getParam('email', ''))); + if ($email === '') error('请输入邮箱地址'); + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) error('邮箱格式不正确'); + + $cur = acctEmailOf($player); + if ($cur !== '') error('账号已绑定邮箱,无需重复绑定'); + + if (!acctPlayerOnlineNow($player)) { + error('你当前未在线或未登录游戏,请先登录游戏后再发起绑定(需在游戏内点击批准)'); + } + + $db = getDB(); + $now = time(); + // 同一玩家任何时刻只允许一条活跃请求(含已批准未输密码的,重新发起即作废旧的) + $void = $db->prepare("UPDATE web_email_bind_requests SET status = 'expired', processed_at = :t WHERE player_name = :p AND status IN ('pending','notified','approved')"); + $void->bindValue(':t', $now, SQLITE3_INTEGER); + $void->bindValue(':p', $player, SQLITE3_TEXT); + $void->execute(); + + $ins = $db->prepare("INSERT INTO web_email_bind_requests (player_name, email, ip_address, status, created_at, expire_at, processed_at) + VALUES (:p, :e, :ip, 'pending', :c, :x, 0)"); + $ins->bindValue(':p', $player, SQLITE3_TEXT); + $ins->bindValue(':e', $email, SQLITE3_TEXT); + $ins->bindValue(':ip', acctClientIp(), SQLITE3_TEXT); + $ins->bindValue(':c', $now, SQLITE3_INTEGER); + $ins->bindValue(':x', $now + 60, SQLITE3_INTEGER); // CLI通知1分钟时效 + $ins->execute(); + $id = (int)$db->lastInsertRowID(); + + debugLog('[account.php] 发起网页绑邮箱', ['player' => $player, 'id' => $id, 'ip' => acctClientIp()]); + success(['id' => $id, 'expire_in' => 60], '请求已发送,等待游戏内批准'); +} + +/** 前端轮询请求状态(1秒一次):pending/notified/approved/rejected/offline/expired/completed/failed */ +function acctBindEmailPoll() { + $player = acctAuthPlayer(); + $id = (int)getParam('id', 0); + if ($id <= 0) error('缺少请求id'); + + $db = getDB(); + $stmt = $db->prepare("SELECT id, player_name, status, created_at, expire_at, approved_at, fail_reason FROM web_email_bind_requests WHERE id = :id"); + $stmt->bindValue(':id', $id, SQLITE3_INTEGER); + $row = $stmt->execute()->fetchArray(SQLITE3_ASSOC); + if (!$row) error('请求不存在'); + if ((string)$row['player_name'] !== $player) error('无权查看该请求'); + + $now = time(); + $status = (string)$row['status']; + $left = 0; + + // 惰性超时:不等 Java 拉,前端轮询到就地终结(批准时效60秒) + if (in_array($status, array('pending', 'notified'), true) && $now > (int)$row['expire_at']) { + $status = 'expired'; + } + // 批准后密码窗口 180 秒,超窗即折返终结 + if ($status === 'approved') { + $left = max(0, ((int)$row['approved_at'] + 180) - $now); + if ($left <= 0) $status = 'expired'; + } elseif (in_array($status, array('pending', 'notified'), true)) { + $left = max(0, (int)$row['expire_at'] - $now); + } + + if ($status === 'expired' && (string)$row['status'] !== 'expired') { + $upd = $db->prepare("UPDATE web_email_bind_requests SET status = 'expired', processed_at = :t WHERE id = :id AND status IN ('pending','notified','approved')"); + $upd->bindValue(':t', $now, SQLITE3_INTEGER); + $upd->bindValue(':id', $id, SQLITE3_INTEGER); + $upd->execute(); + } + + success([ + 'id' => $id, + 'status' => $status, + 'left' => $left, + 'fail_reason' => (string)$row['fail_reason'], + ], 'ok'); +} + +/** 终态确认:批准窗口内复核登录密码 → 写 users.email(IP 连错5次封5分钟) */ +function acctBindEmailConfirm() { + $player = acctAuthPlayer(); + $id = (int)getParam('id', 0); + $pwd = (string)getParam('password', ''); + if ($id <= 0) error('缺少请求id'); + if ($pwd === '') error('请输入登录密码'); + + $ip = acctClientIp(); + // ★第53批风控:绑定确认按 IP 计数,连错 5 次封锁 5 分钟(防密码爆破盗绑) + $locked = acctThrottleLocked('bindip', $ip, 5, 300); + if ($locked > 0) error('密码错误次数过多,IP 已被限制,请 ' . $locked . ' 秒后再试', 429); + + $db = getDB(); + $stmt = $db->prepare("SELECT id, player_name, email, status, approved_at FROM web_email_bind_requests WHERE id = :id"); + $stmt->bindValue(':id', $id, SQLITE3_INTEGER); + $row = $stmt->execute()->fetchArray(SQLITE3_ASSOC); + if (!$row) error('请求不存在'); + if ((string)$row['player_name'] !== $player) error('无权操作该请求'); + + $status = (string)$row['status']; + if ($status === 'completed') success(['id' => $id, 'status' => 'completed'], '邮箱已绑定'); + if ($status !== 'approved') error('该请求未获游戏内批准或已失效,请重新发起绑定'); + + $now = time(); + if ($now > ((int)$row['approved_at'] + 180)) { + $exp = $db->prepare("UPDATE web_email_bind_requests SET status = 'expired', processed_at = :t WHERE id = :id"); + $exp->bindValue(':t', $now, SQLITE3_INTEGER); + $exp->bindValue(':id', $id, SQLITE3_INTEGER); + $exp->execute(); + error('批准已超时,未做任何处置,请重新发起绑定'); + } + + $email = (string)$row['email']; + if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { + error('请求数据异常,请重新发起绑定'); + } + if (acctEmailOf($player) !== '') { + // 绑定期间已被别处绑定 → 幂等终结本请求 + $dup = $db->prepare("UPDATE web_email_bind_requests SET status = 'completed', processed_at = :t WHERE id = :id"); + $dup->bindValue(':t', $now, SQLITE3_INTEGER); + $dup->bindValue(':id', $id, SQLITE3_INTEGER); + $dup->execute(); + success(['id' => $id, 'status' => 'completed'], '邮箱已绑定'); + } + + // 密码复核(只认主密码,同第一层)——错:保持 approved 可在窗口内重试,IP 计数 + $cred = acctCredentials($player); + if (!acctPwdCheck($cred, $pwd)) { + $remain = acctThrottleFail('bindip', $ip, 5, 300); + debugLog('[account.php] 网页绑邮箱密码错误', ['player' => $player, 'ip' => $ip, 'id' => $id]); + if ($remain > 0) error('密码错误(连续错误 5 次将封锁 IP 5 分钟),IP 即将被限制', 403); + error('密码错误', 403); + } + acctThrottleReset('bindip', $ip); + + // 写库:users.email —— users 类别对账 pull 会在几十秒内同步到 Java login.db + $upd = $db->prepare("UPDATE users SET email = :e WHERE player_name = :p"); + $upd->bindValue(':e', $email, SQLITE3_TEXT); + $upd->bindValue(':p', $player, SQLITE3_TEXT); + $upd->execute(); + if ($db->changes() <= 0) { + error('绑定失败:玩家数据不存在,请重新发起'); + } + + $done = $db->prepare("UPDATE web_email_bind_requests SET status = 'completed', processed_at = :t WHERE id = :id"); + $done->bindValue(':t', time(), SQLITE3_INTEGER); + $done->bindValue(':id', $id, SQLITE3_INTEGER); + $done->execute(); + + debugLog('[account.php] 网页绑邮箱成功', ['player' => $player, 'id' => $id, 'ip' => $ip]); + success(['id' => $id, 'status' => 'completed', 'email_masked' => acctMaskEmail($email)], '邮箱绑定成功'); +} diff --git a/api/sync.php b/api/sync.php index 8a13340..bd4a211 100644 --- a/api/sync.php +++ b/api/sync.php @@ -303,6 +303,12 @@ switch ($action) { case 'complete_web_register_request': completeWebRegisterRequest(); break; + case 'check_pending_web_email_bind_requests': + checkPendingWebEmailBindRequests(); + break; + case 'web_email_bind_result': + webEmailBindResult(); + break; case 'delete_user': deleteUser(); break; @@ -5025,6 +5031,120 @@ function completeWebRegisterRequest() { success(['request_id' => $requestId, 'status' => $status]); } +// ===== 第53批:插件轮询待处理的网页邮箱绑定请求(SECRET_KEY认证)===== +function checkPendingWebEmailBindRequests() { + $secret = getParam('secret'); + if (!$secret || $secret !== SECRET_KEY) { + error('密钥验证失败', 403); + } + + $db = getDB(); + try { + $db->exec("SELECT id FROM web_email_bind_requests LIMIT 0"); + } catch (\Throwable $e) { + $db->exec("CREATE TABLE IF NOT EXISTS web_email_bind_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_name TEXT NOT NULL, + email TEXT NOT NULL, + ip_address TEXT DEFAULT '', + status TEXT DEFAULT 'pending', + created_at INTEGER NOT NULL, + expire_at INTEGER NOT NULL, + approved_at INTEGER DEFAULT 0, + processed_at INTEGER DEFAULT 0, + fail_reason TEXT DEFAULT '' + )"); + } + + $now = time(); + // 惰性终结超时未处理的请求(CLI通知1分钟时效) + try { + $db->exec("UPDATE web_email_bind_requests SET status = 'expired', processed_at = " . $now . " WHERE status IN ('pending','notified') AND expire_at < " . $now); + } catch (\Throwable $e) { + @error_log("[checkPendingWebEmailBindRequests] expire error: " . $e->getMessage()); + } + + // ★ 只读查询不需要BEGIN IMMEDIATE(避免与其它并发请求抢锁导致500) + $stmt = $db->prepare("SELECT id, player_name, email, created_at, expire_at FROM web_email_bind_requests WHERE status = 'pending' ORDER BY created_at ASC"); + $result = $stmt->execute(); + + $requests = array(); + $ids = array(); + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $requests[] = $row; + $ids[] = (int)$row['id']; + } + + // 拉取即标记notified(防重复通知;允许失败,下次还会拉到) + if (!empty($ids)) { + try { + $idList = implode(',', $ids); + $db->exec("UPDATE web_email_bind_requests SET status = 'notified' WHERE id IN ($idList) AND status = 'pending'"); + } catch (\Throwable $e) { + @error_log("[checkPendingWebEmailBindRequests] mark notified error: " . $e->getMessage()); + } + } + + success(['requests' => $requests, 'count' => count($requests)]); +} + +// ===== 第53批:插件回传游戏内玩家对邮箱绑定请求的处理结果(SECRET_KEY认证)===== +function webEmailBindResult() { + $secret = getParam('secret'); + if (!$secret || $secret !== SECRET_KEY) { + error('密钥验证失败', 403); + } + + $requestId = getParam('request_id'); + $action = getParam('action', ''); // approve / reject / offline / failed + $player = getParam('player', ''); + $failReason = getParam('fail_reason', ''); + + if (!$requestId) error('缺少request_id'); + if (!in_array($action, array('approve', 'reject', 'offline', 'failed'), true)) error('非法action'); + if (!$player) error('缺少player'); + + $db = getDB(); + $stmt = $db->prepare("SELECT id, player_name, status, expire_at FROM web_email_bind_requests WHERE id = :id"); + $stmt->bindValue(':id', (int)$requestId, SQLITE3_INTEGER); + $row = $stmt->execute()->fetchArray(SQLITE3_ASSOC); + if (!$row) error('请求不存在'); + if ((string)$row['player_name'] !== (string)$player) error('玩家名不匹配'); + + $now = time(); + $current = (string)$row['status']; + $open = in_array($current, array('pending', 'notified'), true); + + if (!$open) { + // 已终结:幂等返回当前状态(重复点击/迟到回调直接忽略,不做任何处置) + success(['request_id' => (int)$requestId, 'status' => $current], '请求已终结,忽略'); + return; + } + + if ($action === 'approve') { + $newStatus = ($now > (int)$row['expire_at']) ? 'expired' : 'approved'; + } elseif ($action === 'reject') { + $newStatus = 'rejected'; + } elseif ($action === 'offline') { + $newStatus = 'offline'; + } else { + $newStatus = 'failed'; + } + + $approvedAt = ($newStatus === 'approved') ? $now : 0; + $reason = ($newStatus === 'failed') ? (string)$failReason : ''; + + $upd = $db->prepare("UPDATE web_email_bind_requests SET status = :status, approved_at = :approved, processed_at = :time, fail_reason = :reason WHERE id = :id"); + $upd->bindValue(':status', $newStatus, SQLITE3_TEXT); + $upd->bindValue(':approved', $approvedAt, SQLITE3_INTEGER); + $upd->bindValue(':time', $now, SQLITE3_INTEGER); + $upd->bindValue(':reason', $reason, SQLITE3_TEXT); + $upd->bindValue(':id', (int)$requestId, SQLITE3_INTEGER); + $upd->execute(); + + success(['request_id' => (int)$requestId, 'status' => $newStatus]); +} + // ===== CDK验证(供sdf1插件调用)===== // sdf1插件本地CDK未找到时,请求Web后端验证 function validateCdk() { diff --git a/player.php b/player.php index e9097ee..77d42f7 100644 --- a/player.php +++ b/player.php @@ -2552,7 +2552,7 @@ if ($currentVersion !== $BUILD_VERSION) { ``); html += acctRow('绑定邮箱', d.has_email ? escapeHtml(d.email_masked) : '未绑定', d.has_email ? '第二层验证可用' - : '请先游戏中 /bindemail'); + : ''); const twofaVal = d.twofa_bound ? '✅ 已绑定认证器(TOTP)' : (d.pending_setup ? '⏳ 密钥已生成但未确认' : '未绑定'); @@ -2578,6 +2578,131 @@ if ($currentVersion !== $BUILD_VERSION) { return html; } + // ===== 第53批:网页绑定邮箱向导(游戏内批准流,60秒时效) ===== + let _bindWizTimer = null; + let _bindWizId = 0; + + function acctBindEmailWizard() { + if (IS_PREVIEW || !TOKEN || !currentPlayer) { showToast('请先登录游戏后再操作', 'error'); return; } + if (_bindWizTimer) { clearInterval(_bindWizTimer); _bindWizTimer = null; } + acctModalOpen('📧 网页绑定邮箱'); + acctModalSet(` +
输入邮箱后,你的游戏内会收到一条批准通知(需在线)。1分钟内点击「✔ 批准」,再回到这里输入登录密码即可完成绑定;拒绝或超时则直接折返,不做任何处置。
+ +
`, + '发送批准请求 →', acctBindEmailStart); + setTimeout(() => { const el = document.getElementById('bindEmailInput'); if (el) el.focus(); }, 120); + } + + async function acctBindEmailStart() { + const el = document.getElementById('bindEmailInput'); + const email = el ? el.value.trim() : ''; + if (!email) { acctErr('请输入邮箱地址'); return; } + const btn = document.getElementById('modalConfirm'); + btn.disabled = true; acctErr('发送中...'); + try { + const res = await apiPost('account.php', {action: 'bind_email_start', email: email}); + btn.disabled = false; + if (!res.success) { acctErr(res.message || '发起失败'); return; } + _bindWizId = res.data.id; + acctBindEmailWait(res.data.expire_in || 60); + } catch (e) { + btn.disabled = false; + acctErr('连接失败: ' + e.message); + } + } + + function acctBindEmailWait(expireIn) { + acctModalSet(` +
+
🎮
+
等待游戏内批准…
+
请到游戏中点击聊天栏的 ✔ 批准,再回来输入登录密码
+
${expireIn}s
+
已向游戏发送通知…
+
+
`, + '取消', () => { if (_bindWizTimer) { clearInterval(_bindWizTimer); _bindWizTimer = null; } closeModal(); }); + if (_bindWizTimer) clearInterval(_bindWizTimer); + _bindWizTimer = setInterval(acctBindEmailPollTick, 1000); + acctBindEmailPollTick(); + } + + async function acctBindEmailPollTick() { + const leftEl = document.getElementById('bindLeft'); + if (leftEl) { + const v = parseInt(leftEl.textContent) || 0; + if (v > 0) leftEl.textContent = (v - 1) + 's'; + } + try { + const res = await apiPost('account.php', {action: 'bind_email_poll', id: _bindWizId}); + if (!res.success) return; + const d = res.data || {}; + const stEl = document.getElementById('bindWizStatus'); + if (d.status === 'pending' || d.status === 'notified') { + if (stEl) stEl.textContent = d.status === 'pending' ? '正在通知游戏…' : '已发送游戏内通知,等待点击批准…'; + if (!d.left) acctBindEmailFinish('expired'); + return; + } + if (d.status === 'approved') { + if (_bindWizTimer) { clearInterval(_bindWizTimer); _bindWizTimer = null; } + acctBindEmailPwdStep(); + return; + } + acctBindEmailFinish(d.status, d.fail_reason); + } catch (e) { /* 网络抖动忽略,下一轮重试 */ } + } + + function acctBindEmailPwdStep() { + acctModalSet(` +
✅ 游戏内已批准。输入登录密码完成绑定(3分钟内有效;密码错误不生效、可重试,连错 5 次将封锁 IP 5 分钟)
+ +
`, + '确认绑定', acctBindEmailConfirm); + setTimeout(() => { const p = document.getElementById('bindPwdInput'); if (p) p.focus(); }, 120); + } + + async function acctBindEmailConfirm() { + const el = document.getElementById('bindPwdInput'); + const pwd = el ? el.value : ''; + if (!pwd) { acctErr('请输入密码'); return; } + const btn = document.getElementById('modalConfirm'); + btn.disabled = true; acctErr('验证中...'); + try { + const res = await apiPost('account.php', {action: 'bind_email_confirm', id: _bindWizId, password: pwd}); + btn.disabled = false; + if (!res.success) { acctErr(res.message || '密码错误'); return; } + if (res.data && res.data.status === 'completed') { + acctModalSet(` +
+
🎉
+
邮箱绑定成功
+
${escapeHtml(res.data.email_masked || '')}
+
现在可以完成第二层验证了,请重新发起刚才的操作
+
`, + '完成', () => { closeModal(); acctRefreshStatus(); }); + } + } catch (e) { + btn.disabled = false; + acctErr('连接失败: ' + e.message); + } + } + + function acctBindEmailFinish(status, reason) { + if (_bindWizTimer) { clearInterval(_bindWizTimer); _bindWizTimer = null; } + const msgs = { + rejected: '❌ 你在游戏内拒绝了本次绑定,已折返、不做任何处置', + offline: '📴 你当前不在线,无法批准。请先登录游戏后重新发起', + expired: '⌛ 1分钟内未处理,请求已超时作废(未做任何处置)', + failed: '⚠️ 绑定失败:' + (reason || '未知原因'), + completed: '✅ 邮箱已绑定' + }; + const msg = msgs[status] || ('状态: ' + status); + acctModalSet(` +
${escapeHtml(msg)}
`, + '知道了', () => { closeModal(); acctRefreshStatus(); }); + } + // ===== 通用 ===== function toast(msg, type = 'success') { const t = document.createElement('div'); @@ -3059,7 +3184,11 @@ if ($currentVersion !== $BUILD_VERSION) { try { const res = await apiPost('account.php', {action: 'verify_start', password: pwd}); btn.disabled = false; - if (!res.success) { acctErr(res.message || '密码错误'); return; } + if (!res.success) { + // ★第53批:第一层密码已过但未绑邮箱 → 不拦截,直接进入网页绑定定向导 + if (res.need_bind_email) { acctBindEmailWizard(); return; } + acctErr(res.message || '密码错误'); return; + } st.challenge = res.data.challenge; st.methods = res.data.methods || []; st.emailMasked = res.data.email_masked || '';