From c3d9c0925d72369424c64e38e5e2a3c52dab8718 Mon Sep 17 00:00:00 2001 From: ypsdf1 Date: Mon, 20 Jul 2026 13:18:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D4=E4=B8=AA=E9=97=AE=E9=A2=98:?= =?UTF-8?q?=20=E7=94=A8=E6=88=B7=E7=BB=84=E8=B4=AD=E4=B9=B0=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E5=BC=B9=E7=AA=97=E3=80=81=E5=9C=A8=E7=BA=BF=E6=9B=B2?= =?UTF-8?q?=E7=BA=BF=E6=B8=85=E7=90=86=E3=80=81=E9=AB=98=E9=A2=91=E5=87=BA?= =?UTF-8?q?=E5=94=AE=E7=BC=93=E5=86=B2=E3=80=81=E6=AF=8F=E5=B0=8F=E6=97=B6?= =?UTF-8?q?=E6=B4=BB=E8=B7=83=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- active_players.php | 42 +++++++--- api/minecraft_auth.php | 121 +++++++++++++++++++++++----- api/pay_secrets.范文.php | 36 +++++++++ api/sync.php | 46 +++++++++++ deploy_php_only.py | 168 +++++++++++++++++++++++++++++++++++++++ online_curve.php | 6 +- player.php | 4 +- 7 files changed, 392 insertions(+), 31 deletions(-) create mode 100644 api/pay_secrets.范文.php create mode 100644 deploy_php_only.py diff --git a/active_players.php b/active_players.php index 84fdfde..c9bc557 100644 --- a/active_players.php +++ b/active_players.php @@ -10,14 +10,26 @@ function getActivePlayers() { try { $db = getDB(); // 确保表存在 + $db->exec("CREATE TABLE IF NOT EXISTS hourly_active_players (player_name TEXT PRIMARY KEY, first_seen INTEGER NOT NULL, last_seen INTEGER NOT NULL, hour_key TEXT NOT NULL)"); $db->exec("CREATE TABLE IF NOT EXISTS online_players (player_name TEXT PRIMARY KEY, login_time INTEGER DEFAULT 0)"); - $res = $db->query("SELECT player_name, login_time FROM online_players ORDER BY login_time ASC"); + $currentHourKey = date('Y-m-d H'); + // ★ 查询本小时活跃过的所有玩家(包括已离线但本小时曾上线的) + $res = $db->query("SELECT player_name, first_seen, last_seen FROM hourly_active_players WHERE hour_key = '" . SQLite3::escapeString($currentHourKey) . "' ORDER BY first_seen ASC"); + $now = time(); while ($row = $res->fetchArray(SQLITE3_ASSOC)) { + // 检查玩家当前是否在线(从online_players表判断) + $checkStmt = $db->prepare("SELECT 1 FROM online_players WHERE player_name = :name"); + $checkStmt->bindValue(':name', $row['player_name'], SQLITE3_TEXT); + $checkRes = $checkStmt->execute(); + $isOnline = $checkRes->fetchArray() !== false; + $online[] = [ 'name' => $row['player_name'], - 'login_time' => (int)$row['login_time'], - 'login_time_fmt' => $row['login_time'] > 0 ? date('H:i:s', (int)$row['login_time']) : '-', - 'duration' => $row['login_time'] > 0 ? time() - (int)$row['login_time'] : 0, + 'login_time' => (int)$row['first_seen'], + 'login_time_fmt' => $row['first_seen'] > 0 ? date('H:i:s', (int)$row['first_seen']) : '-', + 'duration' => $row['first_seen'] > 0 ? $now - (int)$row['first_seen'] : 0, + 'is_online' => $isOnline, + 'last_seen' => (int)$row['last_seen'], ]; } $count = count($online); @@ -58,9 +70,14 @@ body { background:var(--bg); color:var(--text); font-family:'Segoe UI',system-ui .player-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:12px; } .player-card { background:var(--card); border:1px solid var(--border); border-radius:10px; padding:14px 16px; transition:border-color .2s; } .player-card:hover { border-color:var(--accent); } -.player-card .name { font-size:15px; font-weight:700; color:var(--text); margin-bottom:4px; } +.player-card.offline { opacity:0.55; border-style:dashed; } +.player-card .name { font-size:15px; font-weight:700; color:var(--text); margin-bottom:4px; display:flex; align-items:center; gap:6px; } +.player-card .name .status-dot { width:8px; height:8px; border-radius:50%; display:inline-block; flex-shrink:0; } +.player-card .name .status-dot.on { background:var(--green); box-shadow:0 0 6px var(--green); } +.player-card .name .status-dot.off { background:var(--dim); } .player-card .meta { font-size:12px; color:var(--dim); display:flex; justify-content:space-between; } .player-card .meta .online-tag { color:var(--green); } +.player-card .meta .offline-tag { color:var(--yellow); } .player-card .time-bar { margin-top:8px; height:4px; background:var(--bg); border-radius:4px; overflow:hidden; } .player-card .time-bar .fill { height:100%; background:var(--green); border-radius:4px; transition:width 1s; } .empty { grid-column:1/-1; text-align:center; padding:40px; color:var(--dim); } @@ -100,12 +117,19 @@ function render(data) { document.getElementById('countBadge').textContent = data.online_count + ' 人'; const grid = document.getElementById('playerGrid'); if (data.online_list.length === 0) { - grid.innerHTML = '
当前没有玩家在线
'; + grid.innerHTML = '
本小时暂无玩家活跃
'; } else { - grid.innerHTML = data.online_list.map(p => { - const mx = 6 * 3600; // 最长6小时占满 + // 在线玩家排前面,离线玩家排后面 + const sorted = [...data.online_list].sort((a, b) => (b.is_online ? 1 : 0) - (a.is_online ? 1 : 0)); + grid.innerHTML = sorted.map(p => { + const mx = 6 * 3600; const pct = Math.min(100, Math.round(p.duration / mx * 100)); - return `
${esc(p.name)}
登录:${p.login_time_fmt}已在线 ${fmtDuration(p.duration)}
`; + const cls = p.is_online ? 'player-card' : 'player-card offline'; + const dot = p.is_online ? '' : ''; + const tag = p.is_online + ? `在线 ${fmtDuration(p.duration)}` + : `已离线`; + return `
${dot}${esc(p.name)}
首次:${p.login_time_fmt}${tag}
`; }).join(''); } document.getElementById('refreshHint').textContent = '更新:' + new Date().toLocaleTimeString('zh-CN', {hour12:false}); diff --git a/api/minecraft_auth.php b/api/minecraft_auth.php index 68c3904..bb4f012 100644 --- a/api/minecraft_auth.php +++ b/api/minecraft_auth.php @@ -217,11 +217,35 @@ function getAuthUrl($sessionId) { * ★ 公共客户端(00000000402b5328)不需要client_secret * redirect_uri必须与授权时一致 */ +/** + * 将Microsoft返回的OAuth错误映射为中文友好提示 + */ +function mapMsTokenError($msErr, $msDesc = '') { + $msDesc = trim($msDesc); + if ($msErr === 'invalid_grant') { + return '授权码已失效(可能已过期,或已被使用过一次)。请重新点击"打开Microsoft登录"获取新的授权码,并在短时间内完成粘贴提交,不要重复使用旧授权码。'; + } + if ($msErr === 'invalid_client') { + return 'OAuth客户端验证失败,请联系管理员检查 MS_CLIENT_ID 配置。'; + } + if ($msErr === 'invalid_request') { + return '请求参数有误(' . ($msDesc ?: 'invalid_request') . '),请联系管理员。'; + } + return 'Token兑换失败: ' . $msErr . ($msDesc ? '(' . $msDesc . ')' : ''); +} + function exchangeToken($code) { $clientId = '00000000402b5328'; // Minecraft公共客户端ID // 桌面端redirect_uri(与getAuthUrl一致) $redirectUri = 'https://login.live.com/oauth20_desktop.srf'; + // ★ 关键修复:对code做一次urldecode归一化。 + // 前端JS提取code后会encodeURIComponent(! → %21), + // 若此处不解码直接交给http_build_query,PHP会再次编码成 %2521(双重编码), + // 导致Microsoft收到失真的授权码 → invalid_grant 400。 + // 做一次urldecode保证最终只编码一次。urldecode对已是明文的内容是幂等的,安全。 + $code = urldecode($code); + $result = msPost('https://login.live.com/oauth20_token.srf', [ 'client_id' => $clientId, 'code' => $code, @@ -234,7 +258,10 @@ function exchangeToken($code) { return ['error' => 'Token exchange failed: ' . $result['error']]; } if ($result['code'] !== 200) { - return ['error' => 'Token exchange HTTP ' . $result['code']]; + $body = $result['body'] ?? []; + $msErr = $body['error'] ?? 'unknown'; + $msDesc = $body['error_description'] ?? ''; + return ['error' => mapMsTokenError($msErr, $msDesc), 'ms_error' => $msErr]; } $body = $result['body']; @@ -477,10 +504,16 @@ switch ($action) { // 生成授权URL $authUrl = getAuthUrl($sessionId); + // ★ 动态构建paste_url:基于当前请求的域名和脚本路径,不硬编码 + $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; + $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? ''; + $scriptPath = $_SERVER['SCRIPT_NAME'] ?? '/api/minecraft_auth.php'; + $pasteUrl = $scheme . '://' . $host . $scriptPath . '?action=paste_code_page&session_id=' . $sessionId; + success([ 'session_id' => $sessionId, 'auth_url' => $authUrl, - 'paste_url' => 'https://caoyuan.ypshidifu.cn/plugin/api/minecraft_auth.php?action=paste_code_page&session_id=' . $sessionId, + 'paste_url' => $pasteUrl, 'expires_in' => 600, ]); break; @@ -620,24 +653,41 @@ switch ($action) { // ===== 玩家手动粘贴授权码(公共客户端流程) ===== case 'verify_code': { // 兼容JSON和表单两种POST方式 - $json = json_decode(file_get_contents('php://input'), true); + $rawInput = file_get_contents('php://input'); + $json = json_decode($rawInput, true); $sessionId = postParam('session_id') ?: ($json['session_id'] ?? ''); $code = postParam('code') ?: ($json['code'] ?? ''); $secret = postParam('secret') ?: ($json['secret'] ?? ''); + // ★ 详细调试日志 + $debugLine = date('Y-m-d H:i:s') . " [verify_code] 来源: " . (postParam('code') !== '' ? 'POST表单' : 'JSON体') . "\n"; + $debugLine .= " Raw input(前300): " . substr($rawInput, 0, 300) . "\n"; + $debugLine .= " Session: $sessionId\n"; + $debugLine .= " Code(pre-extract,前200): " . substr($code, 0, 200) . " (len=" . strlen($code) . ")\n"; + $debugLine .= " Secret: " . substr($secret, 0, 30) . "\n"; + @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine, FILE_APPEND); + if (empty($sessionId)) error('缺少session_id参数'); if (empty($code)) error('缺少code参数(授权码)'); - // 自动从URL中提取code参数 - if (strpos($code, 'code=') !== false || strpos($code, 'http') === 0) { - if (preg_match('/[?&]code=([^&]+)/', $code, $m)) { + // 自动从URL中提取code参数(兼容 ?code= & #code= & 直接粘贴code) + if (strpos($code, 'code=') !== false || strpos($code, 'http') === 0 || strpos($code, '#') !== false) { + if (preg_match('/[#?&]code=([^&]+)/', $code, $m)) { $code = urldecode($m[1]); } } + // ★ 提取后日志 + $debugLine2 = " Code(post-extract,前200): " . substr($code, 0, 200) . " (len=" . strlen($code) . ")\n"; + @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine2, FILE_APPEND); + // 密钥验证:Java调用需要secret,浏览器表单用session_id自身认证 if (!empty($secret)) { - if ($secret !== MC_AUTH_SECRET) error('密钥错误', 403); + if ($secret !== MC_AUTH_SECRET) { + $debugLine3 = " ★ 密钥不匹配! 收到: " . substr($secret, 0, 30) . " 期望: " . substr(MC_AUTH_SECRET, 0, 30) . "\n"; + @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine3, FILE_APPEND); + error('密钥错误', 403); + } } // 查询会话 @@ -645,8 +695,16 @@ switch ($action) { $stmt->execute([$sessionId]); $session = $stmt->fetch(PDO::FETCH_ASSOC); - if (!$session) error('会话不存在或已过期'); - if ($session['status'] !== 'pending') error('会话状态异常: ' . $session['status']); + if (!$session) { + $debugLine4 = " ★ 会话不存在! sessionId=$sessionId\n"; + @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine4, FILE_APPEND); + error('会话不存在或已过期'); + } + if ($session['status'] !== 'pending') { + $debugLine4 = " ★ 会话状态异常: " . $session['status'] . " (期望pending)\n"; + @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine4, FILE_APPEND); + error('会话状态异常: ' . $session['status']); + } if (time() > $session['expires_at']) { $stmt = $db->prepare("UPDATE mc_auth_sessions SET status = 'expired' WHERE session_id = ?"); $stmt->execute([$sessionId]); @@ -654,7 +712,12 @@ switch ($action) { } // 执行完整OAuth流程 + $debugLine5 = " → 调用 completeAuthFlow, code前50: " . substr($code, 0, 50) . "\n"; + @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine5, FILE_APPEND); $result = completeAuthFlow($code); + $debugLine6 = " ← completeAuthFlow返回: " . (isset($result['error']) ? 'ERROR: ' . $result['error'] : 'SUCCESS uuid=' . ($result['uuid'] ?? '?') . ' name=' . ($result['name'] ?? '?')) . "\n"; + $debugLine6 .= str_repeat('-', 80) . "\n"; + @file_put_contents(__DIR__ . '/debug_minecraft.log', $debugLine6, FILE_APPEND); if (isset($result['error'])) { $stmt = $db->prepare("UPDATE mc_auth_sessions SET status = 'failed' WHERE session_id = ?"); @@ -782,22 +845,24 @@ button:disabled{opacity:0.5;cursor:not-allowed;transform:none} diff --git a/api/pay_secrets.范文.php b/api/pay_secrets.范文.php new file mode 100644 index 0000000..c6640c3 --- /dev/null +++ b/api/pay_secrets.范文.php @@ -0,0 +1,36 @@ +exec('PRAGMA busy_timeout=10000'); $db->exec("CREATE TABLE IF NOT EXISTS online_players (player_name TEXT PRIMARY KEY, login_time INTEGER NOT NULL)"); $db->exec("CREATE TABLE IF NOT EXISTS online_player_hb (id INTEGER PRIMARY KEY DEFAULT 1, last_seen INTEGER DEFAULT 0)"); + // ★ 每小时活跃玩家表:记录本小时内所有曾上线的玩家(离线不清空,跨小时清理) + $db->exec("CREATE TABLE IF NOT EXISTS hourly_active_players ( + player_name TEXT PRIMARY KEY, + first_seen INTEGER NOT NULL, + last_seen INTEGER NOT NULL, + hour_key TEXT NOT NULL + )"); // ★ 迁移:修复旧表结构(旧行无主键导致INSERT OR REPLACE失效) try { $colCheck = $db->query("PRAGMA table_info(online_player_hb)"); @@ -1510,6 +1517,25 @@ function syncOnlinePlayers() { @error_log("[syncOnlinePlayers] FAIL insert $name: " . $e->getMessage()); } + // ★ 每小时活跃玩家追踪:记录曾上线的玩家(离线不清空,跨小时清理) + try { + $hourKey = date('Y-m-d H', $now); + $haStmt = $db->prepare("INSERT INTO hourly_active_players (player_name, first_seen, last_seen, hour_key) + VALUES (:name, :now, :now, :hk) + ON CONFLICT(player_name) DO UPDATE SET + last_seen = :now2, + hour_key = CASE WHEN excluded.hour_key > hourly_active_players.hour_key THEN excluded.hour_key ELSE hourly_active_players.hour_key END, + first_seen = CASE WHEN excluded.hour_key > hourly_active_players.hour_key THEN :now3 ELSE hourly_active_players.first_seen END"); + $haStmt->bindValue(':name', $name, SQLITE3_TEXT); + $haStmt->bindValue(':now', $now, SQLITE3_INTEGER); + $haStmt->bindValue(':now2', $now, SQLITE3_INTEGER); + $haStmt->bindValue(':now3', $now, SQLITE3_INTEGER); + $haStmt->bindValue(':hk', $hourKey, SQLITE3_TEXT); + $haStmt->execute(); + } catch (\Throwable $e) { + @error_log("[syncOnlinePlayers] hourly_active记录失败 {$name}: " . $e->getMessage()); + } + // ★ 如果Java推送了IP,同步更新 player_ip_changes(实时IP来源) $ip = $player['ip'] ?? ''; if (!empty($ip)) { @@ -1546,6 +1572,26 @@ function syncOnlinePlayers() { @error_log("[syncOnlinePlayers] heartbeat update error: " . $e->getMessage()); } + // ★ 每小时清理:跨小时时,删除上一个小时不再在线的玩家 + try { + $currentHourKey = date('Y-m-d H', $now); + $prevHourKey = date('Y-m-d H', $now - 3600); + // 只在刚进入新小时的前5分钟执行清理(避免每次推送都扫表) + $minuteOfHour = (int)date('i', $now); + if ($minuteOfHour < 5) { + // 删除上一个小时及更早的玩家(他们已不在本小时内) + $delStmt = $db->prepare("DELETE FROM hourly_active_players WHERE hour_key < :hk"); + $delStmt->bindValue(':hk', $currentHourKey, SQLITE3_TEXT); + $delStmt->execute(); + $deleted = $db->changes(); + if ($deleted > 0) { + @error_log("[syncOnlinePlayers] ★ 跨小时清理:删除" . $deleted . "条上一个小时的活跃记录"); + } + } + } catch (\Throwable $e) { + @error_log("[syncOnlinePlayers] hourly_active清理失败: " . $e->getMessage()); + } + $db->exec("COMMIT"); } catch (\Throwable $e) { try { $db->exec("ROLLBACK"); } catch (\Throwable $e2) {} diff --git a/deploy_php_only.py b/deploy_php_only.py new file mode 100644 index 0000000..f9ff072 --- /dev/null +++ b/deploy_php_only.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +PHP only deployment script for caoyuan.ypshidifu.cn +Uploads PHP files via FTP to /plugin directory +""" + +import ftplib +import os +import sys +from pathlib import Path + +# FTP Configuration +FTP_HOST = "43.153.203.46" +FTP_PORT = 38367 +FTP_USER = "zxcvb" +FTP_PASS = "32RPNtkKF68j" +FTP_REMOTE_BASE = "/plugin" + +# Local PHP directory +PHP_DIR = r"D:\桌面\源码\mc_plugn\Web" + +# Files/directories to exclude from upload +EXCLUDE_PATTERNS = [ + 'db/', + 'db\\', + '.git', + '.gitignore', + 'deploy_*.py', + '__pycache__', + '*.pyc', + '密钥*.txt', + '密钥*.md', + '*.bak', + 'node_modules', + 'vendor', +] + +def should_exclude(path): + """Check if file should be excluded from upload""" + for pattern in EXCLUDE_PATTERNS: + if pattern in path: + return True + return False + +def upload_file(ftp, local_path, remote_path): + """Upload a single file via FTP""" + try: + with open(local_path, 'rb') as f: + ftp.storbinary(f'STOR {remote_path}', f) + print(f" Uploaded: {remote_path}") + return True + except Exception as e: + print(f" Failed: {remote_path} - {e}") + return False + +def fix_root_config_db_path(config_path): + """Fix DB_PATH in root config.php to point to /plugin/db/web.db""" + try: + with open(config_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Check if DB_PATH needs fixing + if 'DB_PATH' in content and '/plugin/db/web.db' not in content: + # Replace DB_PATH with correct path + import re + new_content = re.sub( + r"define\s*\(\s*['\"]DB_PATH['\"]\s*,\s*['\"][^'\"]*['\"]\s*\)", + "define('DB_PATH', '/plugin/db/web.db')", + content + ) + if new_content != content: + with open(config_path, 'w', encoding='utf-8') as f: + f.write(new_content) + print(f"Fixed DB_PATH in {config_path}") + except Exception as e: + print(f"Warning: Could not fix DB_PATH in {config_path}: {e}") + +def main(): + print("=" * 60) + print("PHP Deployment to caoyuan.ypshidifu.cn") + print("=" * 60) + + # Fix root config.php DB_PATH + root_config = os.path.join(PHP_DIR, 'config.php') + if os.path.exists(root_config): + fix_root_config_db_path(root_config) + + # Connect to FTP + try: + print(f"\nConnecting to FTP: {FTP_HOST}:{FTP_PORT}") + ftp = ftplib.FTP() + ftp.connect(FTP_HOST, FTP_PORT) + ftp.login(FTP_USER, FTP_PASS) + print("Connected successfully!") + + # Ensure remote base directory exists + try: + ftp.cwd(FTP_REMOTE_BASE) + except: + try: + ftp.mkd(FTP_REMOTE_BASE) + ftp.cwd(FTP_REMOTE_BASE) + print(f"Created remote directory: {FTP_REMOTE_BASE}") + except Exception as e: + print(f"Warning: Could not create remote directory: {e}") + + # Walk through local directory and upload files + uploaded = 0 + skipped = 0 + failed = 0 + + for root, dirs, files in os.walk(PHP_DIR): + # Skip excluded directories + dirs[:] = [d for d in dirs if not should_exclude(os.path.join(root, d))] + + for file in files: + local_path = os.path.join(root, file) + + # Skip excluded files + if should_exclude(local_path): + skipped += 1 + continue + + # Calculate relative path + rel_path = os.path.relpath(local_path, PHP_DIR) + remote_path = os.path.join(FTP_REMOTE_BASE, rel_path).replace('\\', '/') + + # Create remote directory if needed + remote_dir = os.path.dirname(remote_path) + try: + ftp.cwd(remote_dir) + except: + try: + ftp.mkd(remote_dir) + ftp.cwd(remote_dir) + except: + pass + + # Upload file + if upload_file(ftp, local_path, remote_path): + uploaded += 1 + else: + failed += 1 + + # Close connection + ftp.quit() + + print("\n" + "=" * 60) + print("Deployment Summary:") + print(f" Uploaded: {uploaded} files") + print(f" Skipped: {skipped} files") + print(f" Failed: {failed} files") + print("=" * 60) + + if failed == 0: + print("\n✅ PHP deployment completed successfully!") + else: + print(f"\n⚠️ Deployment completed with {failed} failures") + return 1 + + return 0 + + except Exception as e: + print(f"\n❌ FTP connection failed: {e}") + return 1 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/online_curve.php b/online_curve.php index 4f34fe3..3ca871c 100644 --- a/online_curve.php +++ b/online_curve.php @@ -15,6 +15,8 @@ function ensureSnapshotTable() { online_count INTEGER NOT NULL DEFAULT 0 )"); $db->exec("CREATE INDEX IF NOT EXISTS idx_snap_time ON online_snapshots(snapshot_time)"); + // 清理24小时前的旧快照(保留24小时数据) + $db->exec("DELETE FROM online_snapshots WHERE snapshot_time < " . (time() - 86400)); } catch (\Throwable $e) { debugLog('[online_curve] 建表异常: ' . $e->getMessage()); } @@ -74,7 +76,9 @@ function getHourlyData() { $stmt->bindValue(':since', $since, SQLITE3_INTEGER); $res = $stmt->execute(); while ($row = $res->fetchArray(SQLITE3_ASSOC)) { - $h = (int)date('G', (int)$row['snapshot_time']); // 0-23 无前导零 + $snapshotTime = (int)$row['snapshot_time']; + // 使用本地时区获取小时(0-23) + $h = (int)date('G', $snapshotTime); $buckets[$h]['count'] += (int)$row['online_count']; $buckets[$h]['samples']++; } diff --git a/player.php b/player.php index c5cb2e9..f3e7934 100644 --- a/player.php +++ b/player.php @@ -2291,7 +2291,7 @@ if ($currentVersion !== $BUILD_VERSION) { const g = window._availGroupsMap?.[groupName]; const price = g ? g.join_price + ' 张债券' : '(价格未知)'; const dur = g ? (g.duration_minutes == 0 ? '永久' : (g.duration_minutes >= 1440 ? Math.floor(g.duration_minutes/1440) + '天' : Math.floor(g.duration_minutes/60) + '小时')) : '未知'; - if (!showConfirm(`确认付费加入用户组「${g ? (g.display_name || groupName) : groupName}」?\n价格: ${price}\n有效期: ${dur}\n将从债券余额扣费。`)) return; + if (!(await showConfirm(`确认付费加入用户组「${g ? (g.display_name || groupName) : groupName}」?\n价格: ${price}\n有效期: ${dur}\n将从债券余额扣费。`))) return; try { const res = await api('land_api.php', {action: 'buy_group', group: groupName, player: currentPlayer, token: TOKEN}); if (res.success) { @@ -2310,7 +2310,7 @@ if ($currentVersion !== $BUILD_VERSION) { const g = window._availGroupsMap?.[groupName]; const price = g ? (parseInt(g.renew_price) > 0 ? g.renew_price + ' 张债券' : '免费') : '未知'; const dur = g && g.duration_minutes ? (g.duration_minutes >= 1440 ? Math.floor(g.duration_minutes/1440) + '天' : Math.floor(g.duration_minutes/60) + '小时') : '由用户组配置决定'; - if (!showConfirm(`确认续费用户组「${g ? (g.display_name || groupName) : groupName}」?\n续费价格: ${price}\n延长有效期: ${dur}\n将从债券余额扣费。`)) return; + if (!(await showConfirm(`确认续费用户组「${g ? (g.display_name || groupName) : groupName}」?\n续费价格: ${price}\n延长有效期: ${dur}\n将从债券余额扣费。`))) return; try { const res = await api('land_api.php', {action: 'renew_group', group: groupName, player: currentPlayer, token: TOKEN}); if (res.success) {