feat: 合并PHP Web后端到仓库 - 统一代码管理\n\n将D:\桌面\源码\mc_plugn\Web\的PHP文件合并到web/子目录\n- 包含所有API端点(sync.php, register.php, shop.php等)\n- 包含前端页面(admin.php, player.php, login.php等)\n- 排除Web.db、调试日志、备份文件\n- .gitignore更新:忽略web/Web.db、debug_log_tail.txt、.bak文件

This commit is contained in:
youpaishidifu
2026-06-23 14:11:40 +08:00
parent 439da63793
commit ce13396c22
30 changed files with 17272 additions and 0 deletions
+2423
View File
File diff suppressed because it is too large Load Diff
+88
View File
@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SDF1 - 管理后台登录</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0d1117; color: #e6edf3;
font-family: 'Segoe UI', system-ui, sans-serif;
display: flex; justify-content: center; align-items: center;
min-height: 100vh;
}
.login-box {
background: #161b22; border: 1px solid #30363d; border-radius: 12px;
padding: 40px; width: 380px; max-width: 90%;
}
.login-box h1 { text-align: center; color: #58a6ff; margin-bottom: 8px; font-size: 24px; }
.login-box .sub { text-align: center; color: #8b949e; font-size: 13px; margin-bottom: 28px; }
.field { margin-bottom: 16px; }
.field label { display: block; font-size: 13px; color: #8b949e; margin-bottom: 6px; }
.field input {
width: 100%; padding: 10px 14px; background: #0d1117; border: 1px solid #30363d;
border-radius: 6px; color: #e6edf3; font-size: 14px; outline: none; transition: border-color 0.2s;
}
.field input:focus { border-color: #58a6ff; }
.login-btn {
width: 100%; padding: 12px; background: #58a6ff; color: #fff; border: none;
border-radius: 6px; font-size: 15px; font-weight: 600; cursor: pointer;
transition: background 0.2s; margin-top: 8px;
}
.login-btn:hover { background: #79c0ff; }
.login-btn:disabled { background: #30363d; cursor: not-allowed; }
.error { color: #f85149; font-size: 13px; text-align: center; margin-top: 12px; display: none; }
.links { text-align: center; margin-top: 20px; }
.links a { color: #58a6ff; text-decoration: none; font-size: 13px; }
.links a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="login-box">
<h1>🔐 管理后台</h1>
<div class="sub">SDF1 插件管理系统</div>
<div class="field">
<label>管理密码</label>
<input type="password" id="password" placeholder="请输入管理密码" autofocus>
</div>
<button class="login-btn" id="loginBtn" onclick="doLogin()">登 录</button>
<div class="error" id="errorMsg"></div>
<div class="links"><a href="player.php">← 返回玩家商城</a></div>
</div>
<script>
document.getElementById('password').addEventListener('keydown', e => {
if (e.key === 'Enter') doLogin();
});
async function doLogin() {
const pw = document.getElementById('password').value;
if (!pw) return;
document.getElementById('loginBtn').disabled = true;
document.getElementById('loginBtn').textContent = '登录中...';
try {
const res = await fetch('api/admin.php?action=login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({password: pw})
});
const data = await res.json();
if (data.success) {
// 设置session cookie后跳转
window.location.href = 'admin.php';
} else {
document.getElementById('errorMsg').textContent = data.message;
document.getElementById('errorMsg').style.display = 'block';
}
} catch (e) {
document.getElementById('errorMsg').textContent = '网络错误';
document.getElementById('errorMsg').style.display = 'block';
}
document.getElementById('loginBtn').disabled = false;
document.getElementById('loginBtn').textContent = '登 录';
}
</script>
</body>
</html>
+2265
View File
File diff suppressed because it is too large Load Diff
+314
View File
@@ -0,0 +1,314 @@
<?php
/**
* 余额查询+充值API
* GET ?action=query - 查询余额(需token)
* POST ?action=recharge - 充值债券(需token)
* GET ?action=transactions - 查看流水(需token)
*/
// 防止任何输出污染JSON响应
ob_start();
require_once __DIR__ . '/../core.php';
ob_end_clean();
$action = getParam('action', 'query');
$token = getParam('token');
try {
switch ($action) {
case 'query':
balanceQuery($token);
break;
case 'recharge':
balanceRecharge($token);
break;
case 'transactions':
balanceTransactions($token);
break;
default:
error('未知操作: ' . $action);
}
} catch (\Throwable $e) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => ['code' => 500, 'message' => 'Internal error: ' . $e->getMessage()]], JSON_UNESCAPED_UNICODE);
exit;
}
// ===== 查询余额 =====
function balanceQuery($token) {
$player = getParam('player');
$password = getParam('password');
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
if (!$player) error('缺少player');
// ★ 安全验证:必须验证token且token对应的玩家必须与查询的玩家匹配
$tokenInfo = null;
$queriedPlayer = $player; // 最终查询的玩家名
// 1. 检查是否为管理后台session
if (session_status() === PHP_SESSION_NONE) session_start();
$isAdmin = isset($_SESSION['admin_auth']) && $_SESSION['admin_auth'];
if ($isAdmin) {
// 管理员:可以查任意玩家
$tokenInfo = ['player' => null, 'purpose' => 'admin'];
} elseif ($token) {
// 2. 验证token并获取token对应的玩家名
$tempTokenInfo = validateToken($token);
if ($tempTokenInfo) {
$tokenPlayer = $tempTokenInfo['player'];
$tokenPurpose = $tempTokenInfo['purpose'] ?? '';
if ($tokenPurpose === 'admin' || $tokenPurpose === 'all') {
// 管理员token:可以查任意玩家
$tokenInfo = $tempTokenInfo;
} elseif ($tokenPlayer === $player) {
// 普通token:只能查自己的余额
// 进一步验证weblogin token的访问权限
$accessResult = validateWebAccess($token, 'view', $password, $ipAddress);
if ($accessResult['ok']) {
$queriedPlayer = $accessResult['player'];
$tokenInfo = ['player' => $queriedPlayer, 'purpose' => 'weblogin'];
} elseif ($accessResult['mode'] === 'need_password') {
jsonResponse([
'success' => false,
'need_password' => true,
'player' => $accessResult['player'],
'message' => $accessResult['message']
], 401);
} else {
error($accessResult['message'], 401);
}
} else {
error('无权查询其他玩家余额', 403);
}
} else {
error('无效token,请先登录', 401);
}
} else {
error('查询余额需要有效token,请先登录', 401);
}
// 查询债券余额(从Web端的债券缓存表)
$db = getDB();
// 先检查是否有缓存表
$db->exec("CREATE TABLE IF NOT EXISTS bond_cache (
player_name TEXT PRIMARY KEY,
amount INTEGER DEFAULT 0,
updated_at INTEGER DEFAULT 0
)");
$stmt = $db->prepare("SELECT amount, updated_at FROM bond_cache WHERE player_name = :name");
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
// 同时查询积分
$stmt2 = $db->prepare("SELECT points FROM users WHERE player_name = :name");
$stmt2->bindValue(':name', $player, SQLITE3_TEXT);
$result2 = $stmt2->execute();
$row2 = $result2->fetchArray(SQLITE3_ASSOC);
$bonds = $row ? $row['amount'] : 0;
$points = $row2 ? $row2['points'] : 0;
$bondsUpdated = $row ? $row['updated_at'] : 0;
// 计算数据新鲜度
$freshness = '';
if ($bondsUpdated > 0) {
$age = time() - $bondsUpdated;
if ($age < 300) $freshness = '刚刚同步';
elseif ($age < 3600) $freshness = floor($age / 60) . '分钟前同步';
elseif ($age < 86400) $freshness = floor($age / 3600) . '小时前同步';
else $freshness = floor($age / 86400) . '天前同步(数据可能过期)';
} else {
$freshness = '尚未同步';
}
success([
'player' => $player,
'bonds' => $bonds,
'points' => $points,
'bonds_updated' => $bondsUpdated,
'freshness' => $freshness
]);
}
// ===== 充值债券 =====
function balanceRecharge($token) {
$player = getParam('player');
$amount = (int)getParam('amount', 0);
$operator = getParam('operator', 'web_admin');
if (!$player) error('缺少player');
if ($amount <= 0) error('充值金额必须大于0');
// 需要token
$tokenInfo = null;
$isPreview = true;
if ($token) {
$tokenInfo = validateToken($token);
if ($tokenInfo) {
$isPreview = false;
// 权限检查:只有管理员token才能充值
if ($tokenInfo['purpose'] !== 'admin' && $tokenInfo['purpose'] !== 'all') {
error('充值需要管理员权限');
}
validateAndUseToken($token);
}
}
$db = getDB();
// 确保缓存表存在
$db->exec("CREATE TABLE IF NOT EXISTS bond_cache (
player_name TEXT PRIMARY KEY,
amount INTEGER DEFAULT 0,
updated_at INTEGER DEFAULT 0
)");
// 查询当前余额
$stmt = $db->prepare("SELECT amount FROM bond_cache WHERE player_name = :name");
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
$currentBalance = $row ? $row['amount'] : 0;
$newBalance = $currentBalance + $amount;
if ($isPreview) {
preview([
'player' => $player,
'amount' => $amount,
'balance_before' => $currentBalance,
'balance_after' => $newBalance,
'operator' => $operator
], '预览模式 - 充值不会生效');
}
// 执行充值
if ($row) {
$stmt = $db->prepare("UPDATE bond_cache SET amount = :amount, updated_at = :time WHERE player_name = :name");
$stmt->bindValue(':amount', $newBalance, SQLITE3_INTEGER);
$stmt->bindValue(':time', time(), SQLITE3_INTEGER);
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
$stmt->execute();
} else {
$stmt = $db->prepare("INSERT INTO bond_cache (player_name, amount, updated_at) VALUES (:name, :amount, :time)");
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
$stmt->bindValue(':amount', $newBalance, SQLITE3_INTEGER);
$stmt->bindValue(':time', time(), SQLITE3_INTEGER);
$stmt->execute();
}
// 记录流水
$stmt = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, reason, status, created_at) VALUES (:player, 'admin_recharge', :amount, :reason, 'pending', :time)");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$stmt->bindValue(':amount', $amount, SQLITE3_INTEGER);
$stmt->bindValue(':reason', "管理员充值: {$operator}", SQLITE3_TEXT);
$stmt->bindValue(':time', time(), SQLITE3_INTEGER);
$stmt->execute();
success([
'player' => $player,
'amount' => $amount,
'balance_before' => $currentBalance,
'balance_after' => $newBalance
], '充值成功');
}
// ===== 查看流水 =====
function balanceTransactions($token) {
$player = getParam('player');
$limit = (int)getParam('limit', 50);
// 需要token
$tokenInfo = null;
if ($token) {
$tokenInfo = validateToken($token);
}
if (!$tokenInfo) {
error('查看流水需要有效token', 401);
}
$db = getDB();
$txs = [];
if ($player) {
// 查看指定玩家流水
if ($tokenInfo['player'] !== $player && $tokenInfo['purpose'] !== 'admin' && $tokenInfo['purpose'] !== 'all') {
error('无权查看其他玩家流水');
}
// Web端交易
try {
$stmt = $db->prepare("SELECT *, 'web' as source FROM web_transactions WHERE player_name = :player ORDER BY created_at DESC LIMIT :limit");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$stmt->bindValue(':limit', $limit, SQLITE3_INTEGER);
$result = $stmt->execute();
while ($row = $result->fetchArray(SQLITE3_ASSOC)) $txs[] = $row;
} catch (\Throwable $e) { /* ignore */ }
// 游戏内交易
try {
$stmt = $db->prepare("SELECT java_id as id, player_name, type, amount, operator, reason, '' as detail, tx_time as created_at, 'game' as source, balance_before, balance_after FROM game_transactions WHERE player_name = :player ORDER BY tx_time DESC LIMIT :limit");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$stmt->bindValue(':limit', $limit, SQLITE3_INTEGER);
$result = $stmt->execute();
while ($row = $result->fetchArray(SQLITE3_ASSOC)) $txs[] = $row;
} catch (\Throwable $e) { /* ignore */ }
} else {
// 管理员查看全服流水
if ($tokenInfo['purpose'] !== 'admin' && $tokenInfo['purpose'] !== 'all') {
error('查看全服流水需要管理员权限');
}
// Web端交易
try {
$stmt = $db->prepare("SELECT *, 'web' as source FROM web_transactions ORDER BY created_at DESC LIMIT :limit");
$stmt->bindValue(':limit', $limit, SQLITE3_INTEGER);
$result = $stmt->execute();
while ($row = $result->fetchArray(SQLITE3_ASSOC)) $txs[] = $row;
} catch (\Throwable $e) { /* ignore */ }
// 游戏内交易
try {
$stmt = $db->prepare("SELECT java_id as id, player_name, type, amount, operator, reason, '' as detail, tx_time as created_at, 'game' as source, balance_before, balance_after FROM game_transactions ORDER BY tx_time DESC LIMIT :limit");
$stmt->bindValue(':limit', $limit, SQLITE3_INTEGER);
$result = $stmt->execute();
while ($row = $result->fetchArray(SQLITE3_ASSOC)) $txs[] = $row;
} catch (\Throwable $e) { /* ignore */ }
}
// 按时间排序(倒序)
usort($txs, function($a, $b) {
return ($b['created_at'] ?? 0) - ($a['created_at'] ?? 0);
});
$txs = array_slice($txs, 0, (int)$limit);
success($txs);
}
// ===== Weblogin Token验证(用于API调用) =====
function validateWebloginTokenForAPI($webToken) {
$db = getDB();
try {
$stmt = $db->prepare("SELECT * FROM weblogin_tokens WHERE web_token = :token");
$stmt->bindValue(':token', $webToken, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if (!$row) return false;
$createdAt = (int)$row['created_at'];
$expireSeconds = (int)$row['expire_seconds'];
if (time() - $createdAt > $expireSeconds) return false;
return [
'player' => $row['player_name'],
'purpose' => 'weblogin',
'created_at' => $createdAt,
'expires_at' => $createdAt + $expireSeconds
];
} catch (Exception $e) {
return false;
}
}
+490
View File
@@ -0,0 +1,490 @@
<?php
/**
* CDK兑换API v2 - 20260620
* POST ?action=exchange - CDK兑换(需token,由插件请求)
* POST ?action=create - 创建CDK(需管理token)
* GET ?action=list - CDK列表(需管理token)
* POST ?action=delete - 删除CDK(需管理token)
* POST ?action=batch - 批量生成CDK(需管理token)
*/
// 防止任何输出污染JSON响应
while (ob_get_level() > 0) { ob_end_clean(); }
require_once __DIR__ . '/../core.php';
// 再次确保清理前置输出
while (ob_get_level() > 0) { ob_end_clean(); }
$action = getParam('action', 'exchange');
$token = getParam('token');
try {
switch ($action) {
case 'exchange':
cdkExchange($token);
break;
case 'create':
cdkCreate($token);
break;
case 'list':
cdkList($token);
break;
case 'delete':
cdkDelete($token);
break;
case 'batch':
cdkBatch($token);
break;
default:
error('未知操作: ' . $action);
}
} catch (\Throwable $e) {
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => ['code' => 500, 'message' => 'Internal error: ' . $e->getMessage()]], JSON_UNESCAPED_UNICODE);
exit;
}
// ===== CDK兑换 =====
function cdkExchange($token) {
$code = getParam('code');
$player = getParam('player');
$password = getParam('password');
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
if (!$code) error('缺少code');
if (!$player) error('缺少player');
// ★ 双保险验证:CDK兑换需要密码确认
$isPreview = true;
$validated = false;
if ($token) {
// 先检查是否为普通管理token
$tokenInfo = validateToken($token);
if ($tokenInfo && ($tokenInfo['purpose'] === 'admin' || $tokenInfo['purpose'] === 'all')) {
$isPreview = false;
$validated = true;
} else {
// weblogin token → 走双保险验证
$accessResult = validateWebAccess($token, 'cdk', $password, $ipAddress);
if ($accessResult['ok'] || $accessResult['mode'] === 'full_verified') {
$isPreview = false;
$validated = true;
$player = $accessResult['player'];
} elseif ($accessResult['mode'] === 'need_password') {
jsonResponse([
'success' => false,
'need_password' => true,
'player' => $accessResult['player'],
'message' => $accessResult['message']
], 401);
} else {
error($accessResult['message'], 401);
}
}
}
// 如果没有token也不是管理token,返回预览
if (!$validated) {
$isPreview = true;
}
$db = getDB();
// 查询CDK
$stmt = $db->prepare("SELECT * FROM cdk WHERE code = :code");
$stmt->bindValue(':code', strtoupper(trim($code)), SQLITE3_TEXT);
$result = $stmt->execute();
$cdk = $result->fetchArray(SQLITE3_ASSOC);
if (!$cdk) {
// ★ 本地没找到 → 写待验证请求,轮询Java Sdf1_login返回结果
$cdk = tryRemoteCdkValidation($db, $code, $player);
if (!$cdk) {
error('CDK不存在: ' . $code);
}
}
if ($cdk['used'] == 1) {
error('CDK已被使用: ' . $cdk['used_by']);
}
// 查询当前余额
$db->exec("CREATE TABLE IF NOT EXISTS bond_cache (
player_name TEXT PRIMARY KEY,
amount INTEGER DEFAULT 0,
updated_at INTEGER DEFAULT 0
)");
$stmt = $db->prepare("SELECT amount FROM bond_cache WHERE player_name = :name");
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
$currentBalance = $row ? $row['amount'] : 0;
$newBalance = $currentBalance + $cdk['amount'];
if ($isPreview) {
preview([
'code' => $code,
'amount' => $cdk['amount'],
'type' => $cdk['type'],
'player' => $player,
'balance_before' => $currentBalance,
'balance_after' => $newBalance,
'valid' => true
], '预览模式 - CDK兑换不会生效');
}
// 执行兑换
$now = time();
$isRemote = !empty($cdk['_remote']);
// ★ 全部DB操作包装在事务中,防止database is locked
$db->exec('BEGIN IMMEDIATE');
try {
// 标记CDK已使用(仅本地CDK需要)
if (!$isRemote) {
$stmt = $db->prepare("UPDATE cdk SET used = 1, used_by = :player, used_at = :time WHERE code = :code");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt->bindValue(':code', strtoupper(trim($code)), SQLITE3_TEXT);
$stmt->execute();
}
// ★ 远程CDK:Java端(cdkManager.redeem)已处理加债券,Web端只做记录,不再加bond_cache
if (!$isRemote) {
// 更新余额
if ($row) {
$stmt = $db->prepare("UPDATE bond_cache SET amount = :amount, updated_at = :time WHERE player_name = :name");
$stmt->bindValue(':amount', $newBalance, SQLITE3_INTEGER);
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
$stmt->execute();
} else {
$stmt = $db->prepare("INSERT INTO bond_cache (player_name, amount, updated_at) VALUES (:name, :amount, :time)");
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
$stmt->bindValue(':amount', $newBalance, SQLITE3_INTEGER);
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt->execute();
}
// 记录流水 + sync_requests(仅本地CDK需要,远程CDK由Java记录流水)
$stmt = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, reason, detail, status, created_at) VALUES (:player, 'cdk_redeem', :amount, :reason, :detail, 'pending', :time)");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$stmt->bindValue(':amount', $cdk['amount'], SQLITE3_INTEGER);
$stmt->bindValue(':reason', "CDK兑换: {$code}", SQLITE3_TEXT);
$stmt->bindValue(':detail', json_encode(['code' => $code, 'remote' => false], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt->execute();
$db->exec("CREATE TABLE IF NOT EXISTS sync_requests (player_name TEXT PRIMARY KEY, created_at INTEGER NOT NULL)");
$stmt2 = $db->prepare("INSERT OR REPLACE INTO sync_requests (player_name, created_at) VALUES (:player, :time)");
$stmt2->bindValue(':player', $player, SQLITE3_TEXT);
$stmt2->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt2->execute();
} else {
// 远程CDK:只记录日志流水(status=completed,不走pending同步)
$stmt = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, reason, detail, status, created_at) VALUES (:player, 'cdk_redeem_remote', :amount, :reason, :detail, 'completed', :time)");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$stmt->bindValue(':amount', $cdk['amount'], SQLITE3_INTEGER);
$stmt->bindValue(':reason', "CDK远程兑换: {$code}", SQLITE3_TEXT);
$stmt->bindValue(':detail', json_encode(['code' => $code, 'remote' => true], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt->execute();
}
$db->exec('COMMIT');
} catch (\Throwable $e) {
try { $db->exec('ROLLBACK'); } catch (\Throwable $e2) {}
@error_log("[cdk] 兑换事务失败: " . $e->getMessage());
}
success([
'code' => $code,
'amount' => $cdk['amount'],
'player' => $player,
'balance_before' => $currentBalance,
'balance_after' => $newBalance
], 'CDK兑换成功');
}
// ===== 创建CDK =====
function cdkCreate($token) {
$amount = (int)getParam('amount', 0);
$type = getParam('type', 'bond');
$code = getParam('code');
if ($amount <= 0) error('金额必须大于0');
// 需要管理token
$tokenInfo = null;
$isPreview = true;
if ($token) {
$tokenInfo = validateToken($token);
if ($tokenInfo && ($tokenInfo['purpose'] === 'admin' || $tokenInfo['purpose'] === 'all')) {
$isPreview = false;
validateAndUseToken($token);
}
}
if (!$code) {
$code = strtoupper(bin2hex(random_bytes(4))); // 8位随机码
}
$db = getDB();
$now = time();
// 检查是否已存在
$stmt = $db->prepare("SELECT 1 FROM cdk WHERE code = :code");
$stmt->bindValue(':code', $code, SQLITE3_TEXT);
$result = $stmt->execute();
if ($result->fetchArray()) {
error('CDK已存在: ' . $code);
}
if ($isPreview) {
preview([
'code' => $code,
'amount' => $amount,
'type' => $type
], '预览模式 - CDK创建不会生效');
}
$stmt = $db->prepare("INSERT INTO cdk (code, amount, type, created_at) VALUES (:code, :amount, :type, :time)");
$stmt->bindValue(':code', $code, SQLITE3_TEXT);
$stmt->bindValue(':amount', $amount, SQLITE3_INTEGER);
$stmt->bindValue(':type', $type, SQLITE3_TEXT);
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt->execute();
success(['code' => $code, 'amount' => $amount, 'type' => $type], 'CDK创建成功');
}
// ===== CDK列表 =====
function cdkList($token) {
$tokenInfo = null;
$isPreview = true;
if ($token) {
$tokenInfo = validateToken($token);
if ($tokenInfo && ($tokenInfo['purpose'] === 'admin' || $tokenInfo['purpose'] === 'all')) {
$isPreview = false;
}
}
$db = getDB();
$result = $db->query("SELECT code, amount, type, used, used_by, created_at, used_at FROM cdk ORDER BY created_at DESC LIMIT 200");
$list = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$list[] = $row;
}
if ($isPreview) {
preview($list, '预览模式');
} else {
success($list);
}
}
// ===== 删除CDK =====
function cdkDelete($token) {
$code = getParam('code');
if (!$code) error('缺少code');
$tokenInfo = null;
$isPreview = true;
if ($token) {
$tokenInfo = validateToken($token);
if ($tokenInfo && ($tokenInfo['purpose'] === 'admin' || $tokenInfo['purpose'] === 'all')) {
$isPreview = false;
validateAndUseToken($token);
}
}
$db = getDB();
if ($isPreview) {
preview(['code' => $code], '预览模式 - 删除不会生效');
}
$stmt = $db->prepare("DELETE FROM cdk WHERE code = :code");
$stmt->bindValue(':code', strtoupper(trim($code)), SQLITE3_TEXT);
$stmt->execute();
success(['code' => $code], 'CDK已删除');
}
// ===== 批量生成CDK =====
function cdkBatch($token) {
$amount = (int)getParam('amount', 0);
$count = (int)getParam('count', 1);
$type = getParam('type', 'bond');
if ($amount <= 0) error('金额必须大于0');
if ($count < 1 || $count > 100) error('数量范围: 1-100');
$tokenInfo = null;
$isPreview = true;
if ($token) {
$tokenInfo = validateToken($token);
if ($tokenInfo && ($tokenInfo['purpose'] === 'admin' || $tokenInfo['purpose'] === 'all')) {
$isPreview = false;
validateAndUseToken($token);
}
}
$db = getDB();
$now = time();
$codes = [];
for ($i = 0; $i < $count; $i++) {
$code = strtoupper(bin2hex(random_bytes(4)));
$codes[] = $code;
if (!$isPreview) {
$stmt = $db->prepare("INSERT INTO cdk (code, amount, type, created_at) VALUES (:code, :amount, :type, :time)");
$stmt->bindValue(':code', $code, SQLITE3_TEXT);
$stmt->bindValue(':amount', $amount, SQLITE3_INTEGER);
$stmt->bindValue(':type', $type, SQLITE3_TEXT);
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt->execute();
}
}
if ($isPreview) {
preview([
'codes' => $codes,
'amount' => $amount,
'count' => $count
], '预览模式 - 批量创建不会生效');
} else {
success([
'codes' => $codes,
'amount' => $amount,
'count' => $count
], "成功创建{$count}个CDK");
}
}
// ===== Weblogin Token验证(用于CDK兑换)=====
function validateWebloginTokenForCDK($webToken) {
$db = getDB();
try {
$stmt = $db->prepare("SELECT * FROM weblogin_tokens WHERE web_token = :token");
$stmt->bindValue(':token', $webToken, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if (!$row) return false;
$createdAt = (int)$row['created_at'];
$expireSeconds = (int)$row['expire_seconds'];
if (time() - $createdAt > $expireSeconds) return false;
return [
'player' => $row['player_name'],
'purpose' => 'weblogin',
'created_at' => $createdAt,
'expires_at' => $createdAt + $expireSeconds
];
} catch (\Throwable $e) {
return false;
}
}
// ===== 远程CDK验证:写请求表,轮询Java Sdf1_login结果 =====
function tryRemoteCdkValidation($db, $code, $player = '') {
$db->exec("CREATE TABLE IF NOT EXISTS cdk_validate_requests (
request_id TEXT PRIMARY KEY,
code TEXT NOT NULL,
player_name TEXT DEFAULT '',
status TEXT DEFAULT 'pending',
created_at INTEGER NOT NULL
)");
// ★ 迁移:给已有表补 player_name 列
$cols = [];
$colResult = $db->query("PRAGMA table_info(cdk_validate_requests)");
while ($col = $colResult->fetchArray(SQLITE3_ASSOC)) { $cols[] = $col['name']; }
if (!in_array('player_name', $cols)) {
$db->exec("ALTER TABLE cdk_validate_requests ADD COLUMN player_name TEXT DEFAULT ''");
}
$db->exec("CREATE TABLE IF NOT EXISTS cdk_validate_results (
request_id TEXT PRIMARY KEY,
code TEXT NOT NULL,
status TEXT NOT NULL,
amount INTEGER DEFAULT 0,
message TEXT DEFAULT '',
used INTEGER DEFAULT 0,
used_by TEXT DEFAULT '',
created_at INTEGER NOT NULL
)");
$requestId = strtoupper(bin2hex(random_bytes(8)));
$now = time();
// ★ 写入待验证请求 — 用BEGIN IMMEDIATE防止database is locked
try {
$db->exec('BEGIN IMMEDIATE');
$stmt = $db->prepare("INSERT OR REPLACE INTO cdk_validate_requests (request_id, code, player_name, status, created_at) VALUES (:id, :code, :player, 'pending', :time)");
$stmt->bindValue(':id', $requestId, SQLITE3_TEXT);
$stmt->bindValue(':code', strtoupper(trim($code)), SQLITE3_TEXT);
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (\Throwable $e) {
$db->exec('ROLLBACK');
@error_log("[cdk] 远程验证写入失败: {$e->getMessage()}");
return null;
}
@error_log("[cdk] 远程验证请求: {$requestId} code={$code} player={$player}");
// ★ 轮询结果(最多8秒,每300ms一次)
$result = null;
for ($i = 0; $i < 27; $i++) {
usleep(300000); // 300ms
$stmt = $db->prepare("SELECT * FROM cdk_validate_results WHERE request_id = :id");
$stmt->bindValue(':id', $requestId, SQLITE3_TEXT);
$rs = $stmt->execute();
$result = $rs->fetchArray(SQLITE3_ASSOC);
if ($result) break;
}
// 清理请求和结果
try {
$db->exec('BEGIN IMMEDIATE');
$db->exec("DELETE FROM cdk_validate_requests WHERE request_id = '" . SQLite3::escapeString($requestId) . "'");
if ($result) {
$db->exec("DELETE FROM cdk_validate_results WHERE request_id = '" . SQLite3::escapeString($requestId) . "'");
}
$db->exec('COMMIT');
} catch (\Throwable $e) {
$db->exec('ROLLBACK');
}
if (!$result) {
@error_log("[cdk] 远程验证超时: {$requestId}");
return null;
}
@error_log("[cdk] 远程验证结果: status={$result['status']} amount={$result['amount']}");
if ($result['status'] === 'success') {
// ★ Java端已标记used并加债券,Web端只做记录不重复加钱
return [
'code' => $result['code'],
'amount' => (int)$result['amount'],
'type' => 'bond',
'used' => 0,
'used_by' => '',
'created_at' => $now,
'used_at' => 0,
'_remote' => true
];
}
return null;
}
+29
View File
@@ -0,0 +1,29 @@
<?php
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
require_once __DIR__ . '/../inc/function.php';
/** @var PDO $db */
function getPlayerList() {
global $db;
$stmt = $db->prepare("SELECT player_name FROM users");
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$players = [];
foreach ($rows as $row) {
$players[] = $row['player_name'];
}
exit(json_encode(['success' => true, 'data' => $players], JSON_UNESCAPED_UNICODE));
}
if (!isLogin()) error('请先登录', 401);
if ($_SERVER['REQUEST_METHOD'] !== 'GET') error('方法不允许');
getPlayerList();
+289
View File
@@ -0,0 +1,289 @@
<?php
/**
* 领地系统API - 接收Java端领地数据 + 管理面板
* POST: Java端推送领地数据
* GET: admin面板读取领地数据
*/
header('Content-Type: application/json; charset=utf-8');
error_reporting(E_ERROR | E_PARSE);
try {
// ★ 加载core.php(与sync.php相同的加载方式)
$coreFile = dirname(__DIR__) . '/core.php';
if (!file_exists($coreFile)) {
$coreFile = dirname(__DIR__) . '/plugin/core.php';
}
require_once $coreFile;
$action = $_GET['action'] ?? $_POST['action'] ?? '';
$secret = $_GET['secret'] ?? $_POST['secret'] ?? '';
$method = $_SERVER['REQUEST_METHOD'];
// ===== 验证 =====
if ($method === 'POST') {
if (!validateSecret($secret)) {
echo json_encode(['success' => false, 'error' => 'invalid secret']);
exit;
}
}
$db = getDB();
// 建表
initLandTables($db);
switch ($action) {
// ===== Java端推送领地数据 =====
case 'sync_lands':
handleSyncLands($db, $_POST);
break;
// ===== Java端推送权限商店数据 =====
case 'sync_shop':
handleSyncShop($db, $_POST);
break;
// ===== 管理面板:获取所有领地 =====
case 'list_lands':
if ($method !== 'GET') {
echo json_encode(['success' => false, 'error' => 'GET only']);
exit;
}
handleListLands($db);
break;
// ===== 管理面板:获取权限商店 =====
case 'list_shop':
if ($method !== 'GET') {
echo json_encode(['success' => false, 'error' => 'GET only']);
exit;
}
handleListShop($db);
break;
// ===== 管理面板:获取配置 =====
case 'get_config':
handleGetConfig($db);
break;
// ===== 管理面板:更新配置 =====
case 'update_config':
handleUpdateConfig($db, $_POST);
break;
// ===== 管理面板:删除领地 =====
case 'delete_land':
handleDeleteLand($db, $_POST);
break;
// ===== 管理面板:更新领地所有者 =====
case 'update_land_owner':
handleUpdateLandOwner($db, $_POST);
break;
// ===== 管理面板:删除权限商品 =====
case 'delete_shop_item':
handleDeleteShopItem($db, $_POST);
break;
default:
echo json_encode(['success' => false, 'error' => 'unknown action']);
}
} catch (\Throwable $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
// ==================== 函数 ====================
function initLandTables($db) {
$db->exec("CREATE TABLE IF NOT EXISTS web_area_lands (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
owner TEXT DEFAULT '',
world TEXT DEFAULT '',
x1 INTEGER DEFAULT 0, z1 INTEGER DEFAULT 0,
x2 INTEGER DEFAULT 0, z2 INTEGER DEFAULT 0,
y_min INTEGER DEFAULT 0, y_max INTEGER DEFAULT 255,
area_size INTEGER DEFAULT 0,
created_at INTEGER DEFAULT 0,
synced_at INTEGER DEFAULT 0
)");
$db->exec("CREATE TABLE IF NOT EXISTS web_area_shop (
id INTEGER PRIMARY KEY,
land_id INTEGER NOT NULL,
land_name TEXT DEFAULT '',
seller TEXT NOT NULL,
permission TEXT DEFAULT 'visitor',
price INTEGER DEFAULT 0,
duration INTEGER DEFAULT 86400,
status TEXT DEFAULT 'active',
buyer TEXT DEFAULT '',
bought_at INTEGER DEFAULT 0,
created_at INTEGER DEFAULT 0,
synced_at INTEGER DEFAULT 0
)");
$db->exec("CREATE TABLE IF NOT EXISTS web_area_config (
key TEXT PRIMARY KEY,
value TEXT DEFAULT ''
)");
}
function handleSyncLands($db, $post) {
$lands = json_decode($post['lands'] ?? '[]', true);
if (!is_array($lands)) {
echo json_encode(['success' => false, 'error' => 'invalid lands data']);
return;
}
$now = time();
$stmt = $db->prepare("INSERT OR REPLACE INTO web_area_lands
(id, name, owner, world, x1, z1, x2, z2, y_min, y_max, area_size, created_at, synced_at)
VALUES (:id, :name, :owner, :world, :x1, :z1, :x2, :z2, :ymin, :ymax, :size, :created, :synced)");
foreach ($lands as $land) {
$stmt->bindValue(':id', (int)($land['id'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':name', $land['name'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':owner', $land['owner'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':world', $land['world'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':x1', (int)($land['x1'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':z1', (int)($land['z1'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':x2', (int)($land['x2'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':z2', (int)($land['z2'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':ymin', (int)($land['y_min'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':ymax', (int)($land['y_max'] ?? 255), SQLITE3_INTEGER);
$stmt->bindValue(':size', (int)($land['area_size'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':created', (int)($land['created_at'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':synced', $now, SQLITE3_INTEGER);
$stmt->execute();
}
echo json_encode(['success' => true, 'count' => count($lands)]);
}
function handleSyncShop($db, $post) {
$items = json_decode($post['items'] ?? '[]', true);
if (!is_array($items)) {
echo json_encode(['success' => false, 'error' => 'invalid shop data']);
return;
}
$now = time();
$stmt = $db->prepare("INSERT OR REPLACE INTO web_area_shop
(id, land_id, land_name, seller, permission, price, duration, status, buyer, bought_at, created_at, synced_at)
VALUES (:id, :land_id, :land_name, :seller, :perm, :price, :dur, :status, :buyer, :bought, :created, :synced)");
foreach ($items as $item) {
$stmt->bindValue(':id', (int)($item['id'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':land_id', (int)($item['land_id'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':land_name', $item['land_name'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':seller', $item['seller'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':perm', $item['permission'] ?? 'visitor', SQLITE3_TEXT);
$stmt->bindValue(':price', (int)($item['price'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':dur', (int)($item['duration'] ?? 86400), SQLITE3_INTEGER);
$stmt->bindValue(':status', $item['status'] ?? 'active', SQLITE3_TEXT);
$stmt->bindValue(':buyer', $item['buyer'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':bought', (int)($item['bought_at'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':created', (int)($item['created_at'] ?? 0), SQLITE3_INTEGER);
$stmt->bindValue(':synced', $now, SQLITE3_INTEGER);
$stmt->execute();
}
echo json_encode(['success' => true, 'count' => count($items)]);
}
function handleListLands($db) {
$result = $db->query("SELECT * FROM web_area_lands ORDER BY synced_at DESC");
$lands = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$lands[] = $row;
}
echo json_encode(['success' => true, 'lands' => $lands]);
}
function handleListShop($db) {
$result = $db->query("SELECT * FROM web_area_shop ORDER BY created_at DESC LIMIT 100");
$items = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$items[] = $row;
}
echo json_encode(['success' => true, 'items' => $items]);
}
function handleGetConfig($db) {
$config = [];
$result = $db->query("SELECT * FROM web_area_config");
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$config[$row['key']] = $row['value'];
}
echo json_encode(['success' => true, 'config' => $config]);
}
function handleUpdateConfig($db, $post) {
$key = $post['key'] ?? '';
$value = $post['value'] ?? '';
if (empty($key)) {
echo json_encode(['success' => false, 'error' => 'missing key']);
return;
}
$stmt = $db->prepare("INSERT OR REPLACE INTO web_area_config (key, value) VALUES (:key, :val)");
$stmt->bindValue(':key', $key, SQLITE3_TEXT);
$stmt->bindValue(':val', $value, SQLITE3_TEXT);
$stmt->execute();
echo json_encode(['success' => true]);
}
function handleDeleteLand($db, $post) {
$name = $post['name'] ?? '';
if (empty($name)) {
echo json_encode(['success' => false, 'error' => 'missing name']);
return;
}
$stmt = $db->prepare("DELETE FROM web_area_lands WHERE name = :name");
$stmt->bindValue(':name', $name, SQLITE3_TEXT);
$stmt->execute();
echo json_encode(['success' => true]);
}
function handleUpdateLandOwner($db, $post) {
$name = $post['name'] ?? '';
$owner = $post['owner'] ?? '';
if (empty($name)) {
echo json_encode(['success' => false, 'error' => 'missing name']);
return;
}
$stmt = $db->prepare("UPDATE web_area_lands SET owner = :owner WHERE name = :name");
$stmt->bindValue(':owner', $owner, SQLITE3_TEXT);
$stmt->bindValue(':name', $name, SQLITE3_TEXT);
$stmt->execute();
echo json_encode(['success' => true]);
}
function handleDeleteShopItem($db, $post) {
$id = (int)($post['id'] ?? 0);
if ($id <= 0) {
echo json_encode(['success' => false, 'error' => 'invalid id']);
return;
}
$stmt = $db->prepare("UPDATE web_area_shop SET status = 'removed' WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
echo json_encode(['success' => true]);
}
function validateSecret($secret) {
if (empty($secret)) return false;
$validSecrets = [
'sdf1_web_comm_2026_ypshidifu'
];
return in_array($secret, $validSecrets);
}
+303
View File
@@ -0,0 +1,303 @@
<?php
/**
* 注册API
* POST ?action=check - 检查用户名是否已注册
* POST ?action=register - 注册新账号(需token)
* GET ?action=query - 查询账号信息(需token)
*/
// 防止任何输出污染JSON响应
ob_start();
require_once __DIR__ . '/../core.php';
ob_end_clean();
$action = getParam('action', 'check');
$token = getParam('token');
try {
switch ($action) {
case 'check':
registerCheck();
break;
case 'register':
registerAccount($token);
break;
case 'web_register':
webRegister();
break;
case 'query':
registerQuery($token);
break;
default:
error('未知操作: ' . $action);
}
} catch (\Throwable $e) {
while (ob_get_level() > 0) ob_end_clean();
error('服务器内部错误: ' . $e->getMessage(), 500);
}
// ===== 检查用户名 =====
function registerCheck() {
$player = getParam('player');
if (!$player) error('缺少player参数');
$db = getDB();
// 只查询users表实际存在的列
$stmt = $db->prepare("SELECT player_name, register_time, email FROM users WHERE player_name = :name");
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
success([
'exists' => !!$row,
'player' => $player,
'info' => $row ?: null
]);
}
// ===== 注册账号 =====
function registerAccount($token) {
$player = getParam('player');
$passwordHash = getParam('password_hash');
$salt = getParam('salt');
$email = getParam('email', '');
$ip = getParam('ip', '');
if (!$player) error('缺少player');
if (!$passwordHash) error('缺少password_hash');
if (!$salt) error('缺少salt');
// 验证token(注册需要token)
$isPreview = true;
if ($token) {
$tokenInfo = validateToken($token);
if ($tokenInfo) {
$isPreview = false;
validateAndUseToken($token);
}
}
$db = getDB();
// 检查是否已注册
$stmt = $db->prepare("SELECT 1 FROM users WHERE player_name = :name");
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
$result = $stmt->execute();
if ($result->fetchArray()) {
error('账号已存在: ' . $player);
}
if ($isPreview) {
preview([
'player' => $player,
'email' => $email,
'would_register' => true
], '预览模式 - 注册不会生效');
}
// 执行注册
$now = time();
$stmt = $db->prepare("INSERT INTO users (player_name, register_time, email) VALUES (:name, :time, :email)");
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt->bindValue(':email', $email, SQLITE3_TEXT);
$stmt->execute();
// 记录
$stmt = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, reason, created_at) VALUES (:player, 'register', 0, 'Web端注册', :time)");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt->execute();
success(['player' => $player], '注册成功');
}
// ===== Web注册请求(由Web端提交,等待插件同步)=====
function webRegister() {
$webToken = getParam('web_token');
$password = getParam('password');
$email = getParam('email', '');
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
if (!$webToken) error('缺少web_token');
if (!$password) error('缺少password');
if (strlen($password) < 6) error('密码长度至少6位');
// 验证web_token
$db = getDB();
$stmt = $db->prepare("SELECT * FROM weblogin_tokens WHERE web_token = :token");
$stmt->bindValue(':token', $webToken, SQLITE3_TEXT);
$result = $stmt->execute();
$tokenRow = $result->fetchArray(SQLITE3_ASSOC);
if (!$tokenRow) error('无效的Web登录Token');
$createdAt = (int)($tokenRow['created_at'] ?? 0);
$expireSeconds = (int)($tokenRow['expire_seconds'] ?? 600);
if (time() - $createdAt > $expireSeconds) error('Web登录Token已过期');
$playerName = $tokenRow['player_name'];
// 检查是否已注册
if (isPlayerRegistered($playerName)) {
error('玩家已注册: ' . $playerName);
}
// 生成密码凭证(与插件相同的SHA-256算法)
$saltBytes = random_bytes(16);
$salt = base64_encode($saltBytes);
$passwordHash = base64_encode(hash('sha256', $saltBytes . $password, true));
// ★ 创建web_register_requests表记录(等待插件同步)
$db->exec("CREATE TABLE IF NOT EXISTS web_register_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_name TEXT NOT NULL,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
email TEXT DEFAULT '',
ip_address TEXT DEFAULT '',
created_at INTEGER NOT NULL,
status TEXT DEFAULT 'pending',
processed_at INTEGER DEFAULT 0
)");
// 插入注册请求
$stmt = $db->prepare("INSERT INTO web_register_requests (player_name, password_hash, salt, email, ip_address, created_at) VALUES (:player, :hash, :salt, :email, :ip, :time)");
$stmt->bindValue(':player', $playerName, SQLITE3_TEXT);
$stmt->bindValue(':hash', $passwordHash, SQLITE3_TEXT);
$stmt->bindValue(':salt', $salt, SQLITE3_TEXT);
$stmt->bindValue(':email', $email, SQLITE3_TEXT);
$stmt->bindValue(':ip', $ipAddress, SQLITE3_TEXT);
$stmt->bindValue(':time', time(), SQLITE3_INTEGER);
$stmt->execute();
$requestId = $db->lastInsertRowID();
success([
'player' => $playerName,
'request_id' => $requestId,
'email' => $email
], '注册请求已提交,等待游戏同步');
}
// ===== 查询账号信息 =====
function registerQuery($token) {
$player = getParam('player');
if (!$player) error('缺少player');
$db = getDB();
// 只查询users表实际存在的列
$stmt = $db->prepare("SELECT player_name, register_time, last_login_time, email, points, gift_stage, total_online_time FROM users WHERE player_name = :name");
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if (!$row) {
error('账号不存在: ' . $player);
}
// 处理Java插件推送的毫秒级时间戳,转换为秒级
if ($row['register_time'] && $row['register_time'] > 1000000000000) {
$row['register_time'] = (int)($row['register_time'] / 1000);
}
if ($row['last_login_time'] && $row['last_login_time'] > 1000000000000) {
$row['last_login_time'] = (int)($row['last_login_time'] / 1000);
}
// ★ 获取债券余额
$bonds = 0;
try {
$bondStmt = $db->prepare("SELECT amount FROM bond_cache WHERE player_name = :name");
$bondStmt->bindValue(':name', $player, SQLITE3_TEXT);
$bondResult = $bondStmt->execute();
$bondRow = $bondResult->fetchArray(SQLITE3_ASSOC);
if ($bondRow) $bonds = (int)($bondRow['amount'] ?? 0);
} catch (\Throwable $e) {}
// ★ 获取累计在线天数(不重复日期计数)
$totalOnlineDays = 0;
try {
$odStmt = $db->prepare("SELECT COUNT(DISTINCT login_date) as cnt FROM player_daily_logins WHERE player_name = :name");
$odStmt->bindValue(':name', $player, SQLITE3_TEXT);
$odResult = $odStmt->execute();
$odRow = $odResult->fetchArray(SQLITE3_ASSOC);
if ($odRow) $totalOnlineDays = (int)($odRow['cnt'] ?? 0);
} catch (\Throwable $e) {}
// ★ 获取累计签到天数
$totalCheckinDays = 0;
try {
$ciStmt = $db->prepare("SELECT COUNT(DISTINCT checkin_date) as cnt FROM player_checkins WHERE player_name = :name");
$ciStmt->bindValue(':name', $player, SQLITE3_TEXT);
$ciResult = $ciStmt->execute();
$ciRow = $ciResult->fetchArray(SQLITE3_ASSOC);
if ($ciRow) $totalCheckinDays = (int)($ciRow['cnt'] ?? 0);
} catch (\Throwable $e) {}
// 合并额外数据
$row['bonds'] = $bonds;
$row['total_online_days'] = $totalOnlineDays;
$row['total_checkin_days'] = $totalCheckinDays;
// 邮箱脱敏
if (!empty($row['email'])) {
$parts = explode('@', $row['email']);
if (count($parts) === 2) {
$row['masked_email'] = str_repeat('*', min(strlen($parts[0]), 3)) . '@' . $parts[1];
} else {
$row['masked_email'] = '***';
}
} else {
$row['masked_email'] = '';
}
// ★ 严格校验:必须有有效token且token对应的玩家必须与查询的玩家匹配(除非是管理员token)
if (!$token) {
// 无token:隐藏敏感信息
$previewRow = [
'player_name' => $row['player_name'],
'register_time' => $row['register_time'],
'last_login_time' => $row['last_login_time'],
'email' => '',
'masked_email' => '',
'points' => 0,
'gift_stage' => 0,
'total_online_time' => 0,
'bonds' => 0,
'total_online_days' => 0,
'total_checkin_days' => 0
];
preview($previewRow, '预览模式 - 部分信息已隐藏');
return;
}
$tokenInfo = validateTokenSilent($token);
if (!$tokenInfo) {
error('无效token,请先登录', 401);
}
$tokenPurpose = $tokenInfo['purpose'] ?? '';
if ($tokenPurpose === 'admin' || $tokenPurpose === 'all') {
// 管理员token:返回完整信息
success($row);
} elseif ($tokenInfo['player'] === $player) {
// 普通token:只能查自己的完整信息
success($row);
} else {
// 不是自己的账号,返回脱敏信息
$maskedRow = [
'player_name' => $row['player_name'],
'register_time' => $row['register_time'],
'last_login_time' => $row['last_login_time'],
'email' => '',
'masked_email' => '***',
'points' => 0,
'gift_stage' => 0,
'total_online_time' => 0,
'bonds' => 0,
'total_online_days' => 0,
'total_checkin_days' => 0
];
preview($maskedRow, '预览模式 - 您只能查看自己的账号信息');
}
}
+263
View File
@@ -0,0 +1,263 @@
<?php
/**
* 商城API
* GET ?action=list - 商品列表(无需token,公开接口)
* POST ?action=buy - 购买商品(需token)
* GET ?action=categories - 分类列表(无需token,公开接口)
*/
// 防止任何输出污染JSON响应
while (ob_get_level() > 0) { ob_end_clean(); }
require_once __DIR__ . '/../core.php';
// 再次确保清理前置输出
while (ob_get_level() > 0) { ob_end_clean(); }
$action = getParam('action', 'list');
$token = getParam('token');
try {
switch ($action) {
case 'list':
shopList($token);
break;
case 'buy':
shopBuy($token);
break;
case 'categories':
shopCategories($token);
break;
default:
error('未知操作: ' . $action);
}
} catch (\Throwable $e) {
while (ob_get_level() > 0) ob_end_clean();
error('服务器内部错误: ' . $e->getMessage(), 500);
}
// ===== 商品列表(公开,无需token) =====
function shopList($token = null) {
try {
$db = getDB();
$category = getParam('category');
if ($category) {
$stmt = $db->prepare("SELECT * FROM shop_items WHERE category = :cat ORDER BY id");
$stmt->bindValue(':cat', $category, SQLITE3_TEXT);
$result = $stmt->execute();
} else {
$result = $db->query("SELECT * FROM shop_items ORDER BY category, id");
}
$items = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$items[] = $row;
}
// 直接返回数据,不验证token(公开接口)
$isPreview = !$token;
if ($token && validateTokenSilent($token)) {
$isPreview = false;
}
if ($isPreview) {
preview($items, '预览模式 - 无有效token');
} else {
success($items);
}
} catch (Exception $e) {
error('商城加载失败: ' . $e->getMessage(), 500);
}
}
// validateTokenSilent() 已在 core.php 中定义,不再重复声明
// ===== 分类列表 =====
function shopCategories() {
$db = getDB();
$result = $db->query("SELECT DISTINCT category, COUNT(*) as item_count FROM shop_items GROUP BY category");
$cats = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$cats[] = $row;
}
success($cats);
}
// ===== 购买商品 =====
function shopBuy($token) {
$itemId = getParam('item_id');
$amount = (int)getParam('amount', 1);
$player = getParam('player');
$password = getParam('password');
$ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
if (!$itemId) error('缺少item_id');
if (!$player) error('缺少player');
if ($amount < 1 || $amount > 64) error('数量无效');
// ★ 双保险验证:购买操作需要密码确认
$isPreview = true;
$isOnline = false;
$isRegistered = false;
if ($token) {
// 先检查是否为普通管理token
$tokenInfo = validateToken($token);
if ($tokenInfo && ($tokenInfo['purpose'] === 'admin' || $tokenInfo['purpose'] === 'all')) {
$isPreview = false;
if (validateToken($token)) {
validateAndUseToken($token);
}
} else {
// weblogin token → 走双保险验证
$accessResult = validateWebAccess($token, 'buy', $password, $ipAddress);
if ($accessResult['ok']) {
$isPreview = false;
$player = $accessResult['player'];
$isOnline = $accessResult['online'] ?? false;
$isRegistered = $accessResult['registered'] ?? false;
} elseif ($accessResult['mode'] === 'need_password') {
jsonResponse([
'success' => false,
'need_password' => true,
'player' => $accessResult['player'],
'message' => $accessResult['message']
], 401);
} elseif ($accessResult['mode'] === 'need_game_login') {
jsonResponse([
'success' => false,
'need_game_login' => true,
'player' => $accessResult['player'],
'message' => $accessResult['message']
], 401);
} elseif ($accessResult['mode'] === 'need_register') {
jsonResponse([
'success' => false,
'need_register' => true,
'player' => $accessResult['player'],
'message' => $accessResult['message']
], 401);
} else {
error($accessResult['message'], 401);
}
}
}
// 查询商品
$db = getDB();
$stmt = $db->prepare("SELECT * FROM shop_items WHERE id = :id");
$stmt->bindValue(':id', $itemId, SQLITE3_TEXT);
$result = $stmt->execute();
$item = $result->fetchArray(SQLITE3_ASSOC);
if (!$item) error('商品不存在: ' . $itemId);
// 检查库存
if ($item['stock'] == 0) error('商品已售罄');
if ($item['stock'] > 0 && $item['stock'] < $amount) error('库存不足');
$totalPrice = $item['buy_price'] * $amount;
// 检查玩家余额
if (!$isPreview && $totalPrice > 0) {
$balanceStmt = $db->prepare("SELECT amount FROM bond_cache WHERE player_name = :player");
$balanceStmt->bindValue(':player', $player, SQLITE3_TEXT);
$balanceResult = $balanceStmt->execute();
$balanceRow = $balanceResult->fetchArray(SQLITE3_ASSOC);
if (!$balanceRow) {
error('玩家余额查询失败');
}
$balance = (int)$balanceRow['amount'];
if ($balance < $totalPrice) {
error('余额不足,当前余额: ' . $balance . ' 债券,需要: ' . $totalPrice . ' 债券');
}
}
if ($isPreview) {
// 预览模式:返回计算结果但不执行
preview([
'item_id' => $itemId,
'item_name' => $item['display_name'],
'amount' => $amount,
'unit_price' => $item['buy_price'],
'total_price' => $totalPrice,
'stock_before' => $item['stock'],
'stock_after' => $item['stock'] > 0 ? $item['stock'] - $amount : -1,
'player' => $player
], '预览模式 - 购买不会生效');
}
// ★ 实际购买 — 包装在事务中,防止database is locked
$db->exec('BEGIN IMMEDIATE');
try {
// 更新库存(同时写admin_stock确保Java同步)
if ($item['stock'] > 0) {
$newStock = $item['stock'] - $amount;
$stmt = $db->prepare("UPDATE shop_items SET stock = :ns, admin_stock = :ns, hourly_sales = hourly_sales + :amount, total_sales = total_sales + :amount WHERE id = :id");
$stmt->bindValue(':ns', $newStock, SQLITE3_INTEGER);
$stmt->bindValue(':amount', $amount, SQLITE3_INTEGER);
$stmt->bindValue(':id', $itemId, SQLITE3_TEXT);
$stmt->execute();
} else {
// 无限库存,只更新销量
$stmt = $db->prepare("UPDATE shop_items SET hourly_sales = hourly_sales + :amount, total_sales = total_sales + :amount WHERE id = :id");
$stmt->bindValue(':amount', $amount, SQLITE3_INTEGER);
$stmt->bindValue(':id', $itemId, SQLITE3_TEXT);
$stmt->execute();
}
// 记录交易
$stmt = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, reason, detail, status, created_at) VALUES (:player, 'shop_buy', :amount, :reason, :detail, 'pending', :time)");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$stmt->bindValue(':amount', $totalPrice, SQLITE3_INTEGER);
$stmt->bindValue(':reason', "购买: {$item['display_name']} x{$amount}", SQLITE3_TEXT);
$stmt->bindValue(':detail', json_encode(['item_id' => $itemId, 'amount' => $amount, 'unit_price' => $item['buy_price']], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
$stmt->bindValue(':time', time(), SQLITE3_INTEGER);
$stmt->execute();
// ★ 写入sync_requests(必须在success/exit之前!)
$db->exec("CREATE TABLE IF NOT EXISTS sync_requests (player_name TEXT PRIMARY KEY, created_at INTEGER NOT NULL)");
$stmt2 = $db->prepare("INSERT OR REPLACE INTO sync_requests (player_name, created_at) VALUES (:player, :time)");
$stmt2->bindValue(':player', $player, SQLITE3_TEXT);
$stmt2->bindValue(':time', time(), SQLITE3_INTEGER);
$stmt2->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
try { $db->exec('ROLLBACK'); } catch (Exception $e2) {}
error('购买失败: ' . $e->getMessage(), 500);
}
success([
'item_id' => $itemId,
'item_name' => $item['display_name'],
'amount' => $amount,
'total_price' => $totalPrice,
'player' => $player
], '购买成功');
}
// ===== Weblogin Token验证(用于商城购买)=====
function validateWebloginTokenForShop($webToken) {
$db = getDB();
try {
$stmt = $db->prepare("SELECT * FROM weblogin_tokens WHERE web_token = :token");
$stmt->bindValue(':token', $webToken, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if (!$row) return false;
$createdAt = (int)$row['created_at'];
$expireSeconds = (int)$row['expire_seconds'];
if (time() - $createdAt > $expireSeconds) return false;
return [
'player' => $row['player_name'],
'purpose' => 'weblogin',
'created_at' => $createdAt,
'expires_at' => $createdAt + $expireSeconds
];
} catch (Exception $e) {
return false;
}
}
+3600
View File
File diff suppressed because it is too large Load Diff
+825
View File
@@ -0,0 +1,825 @@
<?php
/**
* 工单系统API
*
* 用户端:提交工单、查看我的工单、查看详情、回复(MD)、撤销
* 管理员端:查看所有工单、回复、驳回、分配服务商
* 服务商端:抢单大厅、我处理的工单、回复、标记完结
*/
while (ob_get_level() > 0) { @ob_end_clean(); }
ob_start();
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
header('Cache-Control: no-store, no-cache, must-revalidate');
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
// 仅刷新当前文件的OPcache(不要opcache_reset——会重置整个服务器缓存导致竞态条件)
if (function_exists('opcache_invalidate')) { @opcache_invalidate(__FILE__); }
register_shutdown_function(function() {
while (ob_get_level() > 0) { @ob_end_flush(); }
});
require_once __DIR__ . '/../core.php';
if (session_status() === PHP_SESSION_NONE) { session_start(); }
$action = getParam('action', '');
try {
switch ($action) {
// ===== 用户端 =====
case 'create': ticketCreate(); break;
case 'my_list': ticketMyList(); break;
case 'detail': ticketDetail(); break;
case 'reply': ticketReply(); break;
case 'withdraw': ticketWithdraw(); break;
// ===== 管理员端 =====
case 'list_all': ticketListAll(); break;
case 'admin_reply': ticketAdminReply(); break;
case 'reject': ticketReject(); break;
case 'assign': ticketAssign(); break;
case 'admin_create': ticketAdminCreate(); break;
case 'admin_complete': ticketAdminComplete(); break;
// ===== 服务商端 =====
case 'provider_list': ticketProviderList(); break;
case 'available': ticketAvailable(); break;
case 'grab': ticketGrab(); break;
case 'provider_reply': ticketProviderReply(); break;
case 'complete': ticketProviderComplete(); break;
// ===== 通用 =====
case 'providers': ticketProviders(); break;
case 'provider_check': ticketProviderCheck(); break;
default:
error('未知操作: ' . $action);
}
} catch (\Throwable $e) {
error('服务器错误: ' . $e->getMessage());
}
// ============================================================
// 工具函数
// ============================================================
function requireWebToken() {
$token = getParam('token');
if (!$token) error('缺少token', 401);
$info = validateTokenSilent($token);
if (!$info) error('token无效或已过期', 401);
return $info;
}
function requireAdmin() {
if (!isAdminLoggedIn()) error('未登录管理后台', 401);
}
/**
* 检查是否为管理员或授权的服务商用户
* 管理员通过session认证,用户通过token认证
*/
function getAuthUser() {
if (isAdminLoggedIn()) return ['player' => 'admin', 'role' => 'admin'];
return requireWebToken();
}
function isServiceProviderInGame($playerName) {
// 优先从Web.db的web_service_providers表读取(Java同步数据)
$db = getDB();
try {
$stmt = $db->prepare("SELECT 1 FROM web_service_providers WHERE player_name = :p AND active = 1");
$stmt->bindValue(':p', $playerName, SQLITE3_TEXT);
$row = $stmt->execute()->fetchArray();
if ($row !== false) return true;
} catch (\Throwable $e) { /* 表可能不存在 */ }
// 回退:尝试读取Java的login.db(本地开发环境)
$loginDbPath = GAME_LOGIN_DB;
if (!file_exists($loginDbPath)) return false;
try {
$loginDb = new SQLite3($loginDbPath);
$loginDb->enableExceptions(true);
$stmt = $loginDb->prepare("SELECT 1 FROM service_providers WHERE player_name = :p AND active = 1");
$stmt->bindValue(':p', $playerName, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray();
$loginDb->close();
return $row !== false;
} catch (Exception $e) {
return false;
}
}
function getServiceProviderRole($playerName) {
// 优先从Web.db读取
$db = getDB();
try {
$stmt = $db->prepare("SELECT role FROM web_service_providers WHERE player_name = :p AND active = 1");
$stmt->bindValue(':p', $playerName, SQLITE3_TEXT);
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if ($row) return $row['role'];
} catch (\Throwable $e) { /* 表可能不存在 */ }
// 回退:读取Java的login.db
$loginDbPath = GAME_LOGIN_DB;
if (!file_exists($loginDbPath)) return null;
try {
$loginDb = new SQLite3($loginDbPath);
$loginDb->enableExceptions(true);
$stmt = $loginDb->prepare("SELECT role FROM service_providers WHERE player_name = :p AND active = 1");
$stmt->bindValue(':p', $playerName, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
$loginDb->close();
return $row ? $row['role'] : null;
} catch (Exception $e) {
return null;
}
}
function ticketCreate() {
$info = requireWebToken();
$player = $info['player'];
$type = trim(getParam('type', ''));
$title = trim(getParam('title', ''));
$description = trim(getParam('description', ''));
if (empty($type)) error('请选择工单类型');
if (empty($title)) error('请输入工单标题');
if (mb_strlen($title) > 100) error('标题不能超过100字');
if (mb_strlen($description) > 5000) error('描述不能超过5000字');
$validTypes = ['bug', 'help', 'report', 'apply', 'other'];
if (!in_array($type, $validTypes)) error('无效的工单类型');
$db = getDB();
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("INSERT INTO web_tickets (type, status, requester, title, description, created_at, updated_at) VALUES (:type, 'submitted', :player, :title, :desc, :now, :now)");
$stmt->bindValue(':type', $type, SQLITE3_TEXT);
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$stmt->bindValue(':title', $title, SQLITE3_TEXT);
$stmt->bindValue(':desc', $description, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
$ticketId = $db->lastInsertRowID();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('创建失败: ' . $e->getMessage());
}
success(['id' => $ticketId], '工单提交成功');
}
function ticketMyList() {
$info = requireWebToken();
$player = $info['player'];
$db = getDB();
$status = getParam('status', '');
$page = max(1, (int)getParam('page', 1));
$limit = 20;
$offset = ($page - 1) * $limit;
$where = "WHERE requester = :player";
$params = [':player' => $player];
if ($status && $status !== 'all') {
$where .= " AND status = :status";
$params[':status'] = $status;
}
$countStmt = $db->prepare("SELECT COUNT(*) as total FROM web_tickets $where");
foreach ($params as $k => $v) $countStmt->bindValue($k, $v, SQLITE3_TEXT);
$total = $countStmt->execute()->fetchArray(SQLITE3_ASSOC)['total'];
$stmt = $db->prepare("SELECT * FROM web_tickets $where ORDER BY created_at DESC LIMIT :limit OFFSET :offset");
foreach ($params as $k => $v) $stmt->bindValue($k, $v, SQLITE3_TEXT);
$stmt->bindValue(':limit', $limit, SQLITE3_INTEGER);
$stmt->bindValue(':offset', $offset, SQLITE3_INTEGER);
$result = $stmt->execute();
$list = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$list[] = $row;
}
success([
'list' => $list,
'total' => (int)$total,
'page' => $page,
'pages' => ceil($total / $limit)
]);
}
function ticketDetail() {
$id = (int)getParam('id', 0);
if ($id <= 0) error('无效的工单ID');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$row) error('工单不存在');
// 权限检查:管理员可以直接查看,普通用户只能看自己的工单
if (isAdminLoggedIn()) {
// 管理员,直接放行
} else {
$info = requireWebToken();
$player = $info['player'];
if ($row['requester'] !== $player && $row['assigned_to'] !== $player) {
error('无权查看此工单');
}
}
// 获取回复列表
$replies = getTicketReplies($id);
$row['replies'] = $replies;
success($row);
}
function ticketReply() {
$info = requireWebToken();
$player = $info['player'];
$id = (int)getParam('id', 0);
$message = trim(getParam('message', ''));
if ($id <= 0) error('无效的工单ID');
if (empty($message)) error('回复内容不能为空');
if (mb_strlen($message) > 5000) error('回复内容不能超过5000字');
$db = getDB();
// 检查工单状态和权限
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if ($ticket['requester'] !== $player) error('只能回复自己的工单');
if (in_array($ticket['status'], ['withdrawn', 'rejected'])) error('该工单已关闭,无法回复');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
// 插入回复
$stmt = $db->prepare("INSERT INTO web_ticket_replies (ticket_id, sender, role, message, created_at) VALUES (:tid, :sender, 'user', :msg, :now)");
$stmt->bindValue(':tid', $id, SQLITE3_INTEGER);
$stmt->bindValue(':sender', $player, SQLITE3_TEXT);
$stmt->bindValue(':msg', $message, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
// 更新工单状态为已回复
$stmt = $db->prepare("UPDATE web_tickets SET status = 'replied', updated_at = :now WHERE id = :id");
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('回复失败');
}
success(null, '回复成功');
}
function ticketWithdraw() {
$info = requireWebToken();
$player = $info['player'];
$id = (int)getParam('id', 0);
if ($id <= 0) error('无效的工单ID');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if ($ticket['requester'] !== $player) error('无权操作此工单');
if (in_array($ticket['status'], ['completed', 'withdrawn', 'rejected'])) error('该工单已关闭');
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("UPDATE web_tickets SET status = 'withdrawn', updated_at = :now WHERE id = :id");
$stmt->bindValue(':now', time(), SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('撤销失败');
}
success(null, '工单已撤销');
}
// ===== 管理员端 =====
function ticketListAll() {
requireAdmin();
$db = getDB();
// 兜底建表(服务器core.php可能版本旧)
try {
$db->exec("CREATE TABLE IF NOT EXISTS web_tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
status TEXT DEFAULT 'submitted',
requester TEXT NOT NULL,
assigned_to TEXT DEFAULT '',
title TEXT NOT NULL,
description TEXT DEFAULT '',
reject_reason TEXT DEFAULT '',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
)");
$db->exec("CREATE TABLE IF NOT EXISTS web_ticket_replies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ticket_id INTEGER NOT NULL,
sender TEXT NOT NULL,
role TEXT DEFAULT 'user',
message TEXT NOT NULL,
created_at INTEGER NOT NULL
)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_wt_requester ON web_tickets(requester)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_wt_status ON web_tickets(status)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_wt_assigned ON web_tickets(assigned_to)");
$db->exec("CREATE INDEX IF NOT EXISTS idx_wtr_ticket ON web_ticket_replies(ticket_id)");
} catch (\Throwable $e) {
error('工单表初始化失败: ' . $e->getMessage());
}
try {
$status = getParam('status', 'all');
$page = max(1, (int)getParam('page', 1));
$search = trim(getParam('search', ''));
$limit = 20;
$offset = ($page - 1) * $limit;
$where = "WHERE 1=1";
$params = [];
if ($status && $status !== 'all') {
$where .= " AND status = :status";
$params[':status'] = $status;
}
if ($search) {
$where .= " AND (requester LIKE :search OR title LIKE :search2)";
$params[':search'] = "%$search%";
$params[':search2'] = "%$search%";
}
$countStmt = $db->prepare("SELECT COUNT(*) as total FROM web_tickets $where");
foreach ($params as $k => $v) $countStmt->bindValue($k, $v, SQLITE3_TEXT);
$total = $countStmt->execute()->fetchArray(SQLITE3_ASSOC)['total'];
$stmt = $db->prepare("SELECT * FROM web_tickets $where ORDER BY created_at DESC LIMIT :limit OFFSET :offset");
foreach ($params as $k => $v) $stmt->bindValue($k, $v, SQLITE3_TEXT);
$stmt->bindValue(':limit', $limit, SQLITE3_INTEGER);
$stmt->bindValue(':offset', $offset, SQLITE3_INTEGER);
$result = $stmt->execute();
$list = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$list[] = $row;
}
// 统计各状态数量
$stats = [];
foreach (['submitted', 'replied', 'completed', 'withdrawn', 'rejected'] as $s) {
$cnt = $db->querySingle("SELECT COUNT(*) FROM web_tickets WHERE status = '$s'");
$stats[$s] = (int)$cnt;
}
$stats['all'] = (int)$db->querySingle("SELECT COUNT(*) FROM web_tickets");
success([
'list' => $list,
'total' => (int)$total,
'page' => $page,
'pages' => ceil($total / $limit),
'stats' => $stats
]);
} catch (\Throwable $e) {
error('查询工单失败: ' . $e->getMessage());
}
}
function ticketAdminReply() {
requireAdmin();
$id = (int)getParam('id', 0);
$message = trim(getParam('message', ''));
if ($id <= 0) error('无效的工单ID');
if (empty($message)) error('回复内容不能为空');
if (mb_strlen($message) > 5000) error('回复内容不能超过5000字');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if (in_array($ticket['status'], ['completed', 'withdrawn', 'rejected'])) error('该工单已关闭');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("INSERT INTO web_ticket_replies (ticket_id, sender, role, message, created_at) VALUES (:tid, 'admin', 'admin', :msg, :now)");
$stmt->bindValue(':tid', $id, SQLITE3_INTEGER);
$stmt->bindValue(':msg', $message, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
// 管理员回复 → 状态更新为已回复(不改变assigned_to,保留原服务商所有权)
$stmt = $db->prepare("UPDATE web_tickets SET status = 'replied', updated_at = :now WHERE id = :id");
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('回复失败');
}
success(null, '回复成功');
}
function ticketReject() {
requireAdmin();
$id = (int)getParam('id', 0);
$reason = trim(getParam('reason', ''));
if ($id <= 0) error('无效的工单ID');
if (empty($reason)) error('请输入驳回原因');
if (mb_strlen($reason) > 500) error('驳回原因不能超过500字');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if ($ticket['status'] === 'withdrawn' || $ticket['status'] === 'rejected') error('该工单已关闭');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("UPDATE web_tickets SET status = 'rejected', reject_reason = :reason, updated_at = :now WHERE id = :id");
$stmt->bindValue(':reason', $reason, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('驳回失败');
}
success(null, '工单已驳回');
}
function ticketAssign() {
requireAdmin();
$id = (int)getParam('id', 0);
$provider = trim(getParam('provider', ''));
if ($id <= 0) error('无效的工单ID');
if (empty($provider)) error('请输入服务商名称');
// 验证是否为服务商
if (!isServiceProviderInGame($provider)) error("$provider 不是服务商");
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if (in_array($ticket['status'], ['completed', 'withdrawn', 'rejected'])) error('该工单已关闭');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("UPDATE web_tickets SET assigned_to = :provider, status = 'replied', updated_at = :now WHERE id = :id");
$stmt->bindValue(':provider', $provider, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('分配失败');
}
success(null, "已分配给 $provider");
}
function ticketAdminCreate() {
requireAdmin();
$type = trim(getParam('type', ''));
$title = trim(getParam('title', ''));
$description = trim(getParam('description', ''));
$provider = trim(getParam('provider', ''));
if (empty($type)) error('请选择工单类型');
if (empty($title)) error('请输入工单标题');
if (mb_strlen($title) > 100) error('标题不能超过100字');
if (mb_strlen($description) > 5000) error('描述不能超过5000字');
$validTypes = ['bug', 'help', 'report', 'apply', 'other'];
if (!in_array($type, $validTypes)) error('无效的工单类型');
// 如果指定了服务商,验证身份
if ($provider && !isServiceProviderInGame($provider)) {
error("$provider 不是服务商");
}
$db = getDB();
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("INSERT INTO web_tickets (type, status, requester, assigned_to, title, description, created_at, updated_at) VALUES (:type, 'replied', 'admin', :provider, :title, :desc, :now, :now)");
$stmt->bindValue(':type', $type, SQLITE3_TEXT);
$stmt->bindValue(':provider', $provider, SQLITE3_TEXT);
$stmt->bindValue(':title', $title, SQLITE3_TEXT);
$stmt->bindValue(':desc', $description, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
$ticketId = $db->lastInsertRowID();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('创建失败: ' . $e->getMessage());
}
$msg = '工单已创建';
if ($provider) $msg .= ",已派发给 $provider";
success(['id' => $ticketId], $msg);
}
function ticketAdminComplete() {
requireAdmin();
$id = (int)getParam('id', 0);
if ($id <= 0) error('无效的工单ID');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if (in_array($ticket['status'], ['completed', 'withdrawn', 'rejected'])) error('该工单已关闭');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("INSERT INTO web_ticket_replies (ticket_id, sender, role, message, created_at) VALUES (:tid, 'admin', 'admin', '✅ 工单已由管理员标记完结', :now)");
$stmt->bindValue(':tid', $id, SQLITE3_INTEGER);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
$stmt = $db->prepare("UPDATE web_tickets SET status = 'completed', updated_at = :now WHERE id = :id");
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('操作失败');
}
success(null, '已标记完结');
}
// ===== 服务商端 =====
function ticketProviderList() {
$info = requireWebToken();
$player = $info['player'];
// 验证服务商身份
$role = getServiceProviderRole($player);
if (!$role) error('你不是服务商');
$db = getDB();
$type = getParam('type', 'all');
// processing: 活跃(已提交/已回复)
// completed: 已完结/已撤销/已驳回
// all: 全部跟过的工单
$where = "WHERE assigned_to = :player";
$params = [':player' => $player];
if ($type === 'processing') {
$where .= " AND status IN ('submitted', 'replied')";
} elseif ($type === 'completed') {
$where .= " AND status IN ('completed', 'withdrawn', 'rejected')";
}
// 'all' 不加status过滤
$stmt = $db->prepare("SELECT * FROM web_tickets $where ORDER BY updated_at DESC LIMIT 100");
foreach ($params as $k => $v) $stmt->bindValue($k, $v, SQLITE3_TEXT);
$result = $stmt->execute();
$list = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$list[] = $row;
}
success(['list' => $list, 'role' => $role]);
}
function ticketAvailable() {
$info = requireWebToken();
$player = $info['player'];
if (!isServiceProviderInGame($player)) error('你不是服务商');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE status = 'submitted' AND assigned_to = '' ORDER BY created_at DESC LIMIT 50");
$result = $stmt->execute();
$list = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$list[] = $row;
}
success(['list' => $list]);
}
function ticketGrab() {
$info = requireWebToken();
$player = $info['player'];
$id = (int)getParam('id', 0);
if ($id <= 0) error('无效的工单ID');
if (!isServiceProviderInGame($player)) error('你不是服务商');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if ($ticket['status'] !== 'submitted') error('该工单已被接单');
if (!empty($ticket['assigned_to'])) error('该工单已被分配');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("UPDATE web_tickets SET assigned_to = :provider, updated_at = :now WHERE id = :id");
$stmt->bindValue(':provider', $player, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('抢单失败');
}
success(null, '抢单成功');
}
function ticketProviderReply() {
$info = requireWebToken();
$player = $info['player'];
$id = (int)getParam('id', 0);
$message = trim(getParam('message', ''));
if ($id <= 0) error('无效的工单ID');
if (empty($message)) error('回复内容不能为空');
if (mb_strlen($message) > 5000) error('回复内容不能超过5000字');
if (!isServiceProviderInGame($player)) error('你不是服务商');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if ($ticket['assigned_to'] !== $player) error('这不是分配给你的工单');
if (in_array($ticket['status'], ['completed', 'withdrawn', 'rejected'])) error('该工单已关闭');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
$stmt = $db->prepare("INSERT INTO web_ticket_replies (ticket_id, sender, role, message, created_at) VALUES (:tid, :sender, 'provider', :msg, :now)");
$stmt->bindValue(':tid', $id, SQLITE3_INTEGER);
$stmt->bindValue(':sender', $player, SQLITE3_TEXT);
$stmt->bindValue(':msg', $message, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
$stmt = $db->prepare("UPDATE web_tickets SET status = 'replied', updated_at = :now WHERE id = :id");
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('回复失败');
}
success(null, '回复成功');
}
function ticketProviderComplete() {
$info = requireWebToken();
$player = $info['player'];
$id = (int)getParam('id', 0);
if ($id <= 0) error('无效的工单ID');
if (!isServiceProviderInGame($player)) error('你不是服务商');
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_tickets WHERE id = :id");
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$ticket = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$ticket) error('工单不存在');
if ($ticket['assigned_to'] !== $player) error('这不是分配给你的工单');
if (in_array($ticket['status'], ['completed', 'withdrawn', 'rejected'])) error('该工单已关闭');
$now = time();
$db->exec('BEGIN IMMEDIATE');
try {
// 插入完结回复
$stmt = $db->prepare("INSERT INTO web_ticket_replies (ticket_id, sender, role, message, created_at) VALUES (:tid, :sender, 'provider', '✅ 工单已处理完毕', :now)");
$stmt->bindValue(':tid', $id, SQLITE3_INTEGER);
$stmt->bindValue(':sender', $player, SQLITE3_TEXT);
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->execute();
// 状态改为completed
$stmt = $db->prepare("UPDATE web_tickets SET status = 'completed', updated_at = :now WHERE id = :id");
$stmt->bindValue(':now', $now, SQLITE3_INTEGER);
$stmt->bindValue(':id', $id, SQLITE3_INTEGER);
$stmt->execute();
$db->exec('COMMIT');
} catch (Exception $e) {
$db->exec('ROLLBACK');
error('操作失败');
}
success(null, '已标记完结');
}
// ===== 通用 =====
function ticketProviders() {
if (!isAdminLoggedIn()) error('未登录管理后台');
$loginDbPath = GAME_LOGIN_DB;
if (!file_exists($loginDbPath)) success(['list' => []]);
try {
$loginDb = new SQLite3($loginDbPath);
$result = $loginDb->query("SELECT player_name, role, join_time FROM service_providers WHERE active = 1");
$list = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$list[] = $row;
}
$loginDb->close();
success(['list' => $list]);
} catch (Exception $e) {
success(['list' => []]);
}
}
function ticketProviderCheck() {
$info = requireWebToken();
$player = $info['player'];
$isProvider = isServiceProviderInGame($player);
$role = $isProvider ? getServiceProviderRole($player) : null;
success(['is_provider' => $isProvider, 'role' => $role]);
}
// ===== 辅助函数 =====
function getTicketReplies($ticketId) {
$db = getDB();
$stmt = $db->prepare("SELECT * FROM web_ticket_replies WHERE ticket_id = :tid ORDER BY created_at ASC");
$stmt->bindValue(':tid', $ticketId, SQLITE3_INTEGER);
$result = $stmt->execute();
$list = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$list[] = $row;
}
return $list;
}
+41
View File
@@ -0,0 +1,41 @@
<?php
/**
* Web通信系统 - 配置文件
*/
// ===== 安全密钥(插件和Web端必须一致) =====
define('SECRET_KEY', 'sdf1_web_comm_2026_ypshidifu');
// ===== Token配置 =====
define('TOKEN_EXPIRE_SECONDS', 600); // 10分钟
// ===== 数据库路径 =====
define('DB_PATH', __DIR__ . '/db/web.db');
// ===== 管理员认证 =====
define('ADMIN_USER', 'admin');
define('ADMIN_PASS', 'ypshidifu2026'); // 管理员密码
// ===== 游戏数据库路径(用于同步) =====
define('GAME_BOND_DB', 'D:/服务器/插件/bond.db');
define('GAME_LOGIN_DB', 'D:/服务器/插件/login.db');
define('GAME_SHOP_DIR', 'D:/服务器/插件/shop/');
// ===== Web子目录路径(如 /plugin 或 /test1,根目录则留空) =====
define('WEBSUB_DIR', '/plugin');
// ===== Java插件回调端口 =====
define('CALLBACK_PORT', 9090);
// ===== 辅助函数:构建相对路径 =====
function webPath($path = '') {
return trim(WEBSUB_DIR, '/') . ($path ? '/' . trim($path, '/') : '');
}
// ===== SMTP邮件配置 =====
define('SMTP_HOST', 'hwsmtp.exmail.qq.com');
define('SMTP_PORT', '465');
define('SMTP_USER', 'mcserver@ypshidifu.cn');
define('SMTP_PASS', 'sQ2ZiCZGq96xi9Sv');
define('SMTP_SENDER_NAME', 'Sdf1_login');
define('SMTP_USE_SSL', true);
+1064
View File
File diff suppressed because it is too large Load Diff
+178
View File
@@ -0,0 +1,178 @@
<?php
// ★ IP归属地清理脚本 v2 - 使用统一校验函数
error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('max_execution_time', 300);
require_once __DIR__ . '/../core.php';
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>IP归属地清理 v2</title>
<style>
body { font-family: monospace; padding: 20px; background: #1a1a1a; color: #0f0; }
.ok { color: #0f0; }
.err { color: #f00; }
.warn { color: #ff0; }
.info { color: #00f; }
pre { background: #333; padding: 10px; overflow-x: auto; max-height: 300px; overflow-y: auto; }
table { border-collapse: collapse; width: 100%; margin: 10px 0; }
th, td { border: 1px solid #444; padding: 4px 8px; text-align: left; font-size: 12px; }
th { background: #333; }
.row-invalid { background: #300; }
.row-valid { background: #030; }
</style>
</head>
<body>
<h1>🔍 IP归属地清理工具 v2</h1>
<?php
try {
$db = getDB();
echo "<h2>0. 当前数据库状态</h2>";
$totalBefore = $db->query("SELECT COUNT(*) as cnt FROM player_ip_locations")->fetchArray(SQLITE3_ASSOC)['cnt'] ?? 0;
echo "<p>清理前总记录数: $totalBefore</p>";
// ★ 列出所有记录 + 逐条验证
echo "<h2>📋 全部记录逐条验证(使用统一校验函数 isValidIpLocationFormat)</h2>";
echo "<table><tr><th>IP</th><th>位置</th><th>验证结果</th><th>删除原因</th></tr>";
$allStmt = $db->query("SELECT ip_address, location FROM player_ip_locations ORDER BY ip_address");
$records = [];
while ($row = $allStmt->fetchArray(SQLITE3_ASSOC)) {
$records[] = $row;
}
$deletedCount = 0;
$checkedCount = 0;
foreach ($records as $row) {
$ip = $row['ip_address'];
$loc = $row['location'];
$checkedCount++;
// ★ 直接使用统一校验函数
$isValid = isValidIpLocationFormat($loc);
// 判断删除原因(用于显示)
$reason = '';
if (!$isValid) {
if ($loc === '' || $loc === null || $loc === 'NULL' || $loc === '-' || $loc === '--') {
$reason = "空值/短杠";
} elseif ($loc === '查询失败' || $loc === '查询中' || $loc === '内网IP') {
$reason = "特殊标记";
} elseif (stripos($loc, 'China') === 0) {
$reason = "英文格式";
} elseif (strpos($loc, '中国 - -') === 0 || strpos($loc, '中国 - -') === 0) {
$reason = "不完整中文格式";
} elseif (preg_match('/^中国\s*$/u', $loc) || preg_match('/^中国\s+[^\s省市区州盟]+$/u', $loc)) {
$reason = "不完整中文格式";
} elseif (strpos($loc, ' ') === false) {
$reason = "无空格";
} else {
$reason = "省市格式不符合";
}
}
$shouldDelete = !$isValid;
$funcStr = $isValid ? '✅' : '❌';
$rowClass = $shouldDelete ? 'row-invalid' : 'row-valid';
echo "<tr class='$rowClass'>";
echo "<td>$ip</td>";
echo "<td>" . htmlspecialchars($loc) . "</td>";
echo "<td>$funcStr</td>";
echo "<td>" . ($shouldDelete ? "<b class='err'>$reason</b>" : '-') . "</td>";
echo "</tr>";
if ($shouldDelete) {
$delStmt = $db->prepare("DELETE FROM player_ip_locations WHERE ip_address = :ip");
$delStmt->bindValue(':ip', $ip, SQLITE3_TEXT);
$delStmt->execute();
$deletedCount++;
}
}
echo "</table>";
echo "<p class='info'>检查了 $checkedCount 条记录</p>";
if ($deletedCount > 0) {
echo "<p class='ok'>✅ 删除 $deletedCount 条无效记录</p>";
} else {
echo "<p class='ok'>无无效记录</p>";
}
// 统计
$totalAfter = $db->query("SELECT COUNT(*) as cnt FROM player_ip_locations")->fetchArray(SQLITE3_ASSOC)['cnt'] ?? 0;
echo "<p>清理前: $totalBefore → 清理后: $totalAfter</p>";
// 需要重新查询的IP(过滤掉短杠和空值)
echo "<hr><h2>6. 需要重新查询的IP(player_ip_changes 中有但 player_ip_locations 中没有的)</h2>";
$needQuery = $db->query("
SELECT DISTINCT pic.new_ip
FROM player_ip_changes pic
LEFT JOIN player_ip_locations pil ON pic.new_ip = pil.ip_address
WHERE pil.ip_address IS NULL
AND pic.new_ip IS NOT NULL
AND pic.new_ip != ''
AND pic.new_ip NOT IN ('-', '--')
AND pic.new_ip LIKE '%.%'
ORDER BY pic.new_ip
LIMIT 50
");
$needQueryList = [];
while ($row = $needQuery->fetchArray(SQLITE3_ASSOC)) {
$needQueryList[] = $row['new_ip'];
}
echo "<p>找到 " . count($needQueryList) . " 个需要查询的IP</p>";
if (!empty($needQueryList)) {
echo "<pre>";
foreach ($needQueryList as $ip) {
echo $ip . "\n";
}
echo "</pre>";
}
// 额外:player_ip_locations 中存储了短杠位置的记录
echo "<h2>7. 数据库中存储了短杠位置的记录</h2>";
$dashLocQuery = $db->query("SELECT ip_address, location FROM player_ip_locations WHERE location IN ('-', '--', '') OR location IS NULL");
$dashLocCount = 0;
while ($row = $dashLocQuery->fetchArray(SQLITE3_ASSOC)) {
echo "<p>" . $row['ip_address'] . " => " . htmlspecialchars($row['location'] ?? 'NULL') . "</p>";
$dashLocCount++;
}
if ($dashLocCount === 0) {
echo "<p class='ok'>无短杠位置记录</p>";
} else {
echo "<p class='warn'>找到 $dashLocCount 条短杠位置记录,将清理</p>";
$db->exec("DELETE FROM player_ip_locations WHERE location IN ('-', '--', '') OR location IS NULL");
echo "<p class='ok'>已清理短杠位置记录</p>";
}
echo "<hr><p class='ok'>✅ 清理完成!请刷新用户管理页面查看效果。</p>";
} catch (Exception $e) {
echo "<p class='err'>错误: " . $e->getMessage() . "</p>";
echo "<pre>" . $e->getTraceAsString() . "</pre>";
}
?>
<hr>
<p>💡 清理规则(使用统一校验函数 isValidIpLocationFormat):</p>
<ol>
<li>空值、短杠、双短杠、NULL</li>
<li>查询失败、查询中、内网IP</li>
<li>英文格式(China - xxx)</li>
<li>不完整的中文格式(中国 - -xxx)</li>
<li>无空格(缺少运营商分隔)</li>
<li>直辖市缺少"市"(如"北京 移动"→应为"北京市 移动")</li>
<li>非直辖市缺少城市后缀(如"湖北省 移动"→应为"湖北省武汉市 移动")</li>
<li>支持无后缀格式(如"湖北武汉 移通")</li>
</ol>
</body>
</html>
+615
View File
@@ -0,0 +1,615 @@
<?php
// ★ IP查询调试页面 v2 - 完整追踪查询流程
error_reporting(E_ALL);
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('max_execution_time', 120);
// ★ 关键:在任何输出之前设置缓冲区
ob_start();
// 不要包含 admin.php(它会输出JSON并exit)
// 只包含 core.php
require_once __DIR__ . '/../core.php';
// 清空输出缓冲区
while (ob_get_level() > 0) {
ob_end_clean();
}
// 现在开始输出HTML
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>IP查询调试 v2</title>
<style>
body { font-family: monospace; padding: 20px; background: #1a1a1a; color: #0f0; }
.ok { color: #0f0; }
.err { color: #f00; }
.warn { color: #ff0; }
.info { color: #00f; }
pre { background: #333; padding: 10px; overflow-x: auto; max-height: 400px; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word; }
.step { margin: 10px 0; padding: 10px; border-left: 3px solid #0f0; }
.step.error { border-left-color: #f00; }
.step.warn { border-left-color: #ff0; }
.api-result { margin: 5px 0; padding: 5px; background: #222; }
</style>
</head>
<body>
<h1>🔍 IP查询调试页面 v2</h1>
<?php
// 获取要测试的IP(支持逗号分隔多IP)
$allIps = [];
$ipParam = trim($_GET['ip'] ?? '');
if ($ipParam) {
$allIps = array_map('trim', explode(',', $ipParam));
}
$ip2Param = trim($_GET['ip2'] ?? '');
if ($ip2Param) {
$allIps = array_merge($allIps, array_map('trim', explode(',', $ip2Param)));
}
// 去重并过滤空值
$allIps = array_unique(array_filter($allIps));
if (empty($allIps)) {
$allIps = ['36.148.150.193', '120.231.236.80'];
}
echo "<h2>测试IP (" . count($allIps) . "个): " . implode(', ', $allIps) . "</h2>";
// ===== 第1步:检查数据库中的当前状态 =====
echo "<div class='step'>";
echo "<h3>📊 步骤1: 检查数据库当前状态</h3>";
try {
$db = getDB();
foreach ($allIps as $ip) {
echo "<p><strong>$ip:</strong></p>";
$stmt = $db->prepare("SELECT * FROM player_ip_locations WHERE ip_address = :ip");
$stmt->bindValue(':ip', $ip, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row) {
echo "<pre>" . json_encode($row, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "</pre>";
} else {
echo "<p class='warn'>数据库中无此IP记录</p>";
}
}
} catch (Exception $e) {
echo "<p class='err'>错误: " . $e->getMessage() . "</p>";
}
echo "</div>";
// ===== 第2步:测试4个API接口(直接调用,不包含admin.php) =====
echo "<div class='step'>";
echo "<h3>🌐 步骤2: 测试4个API接口</h3>";
// ★ 直接定义函数,不包含 admin.php
function testFetchPconline($ip) {
$url = 'https://whois.pconline.com.cn/ipJson.jsp?ip=' . urlencode($ip) . '&json=true';
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 5,
'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"
]
]);
$response = @file_get_contents($url, false, $ctx);
$result = [
'api' => 'PConline',
'url' => $url,
'raw_response' => $response ?: 'FAILED',
'parsed' => null,
'location' => null,
'error' => null
];
if ($response === false || empty(trim($response))) {
$result['error'] = '请求失败';
return $result;
}
// ★ 修复编码问题:太平洋返回GBK编码,需要转换为UTF-8
$response = trim($response);
if (mb_detect_encoding($response, 'UTF-8', true) === false) {
$response = mb_convert_encoding($response, 'UTF-8', 'GBK');
$result['raw_response'] = $response; // 更新原始响应为转换后的
}
$json = $response;
if (preg_match('/^\s*\(?[\'"]?(.*?)[\'"]?\)?\s*$/s', $json, $matches)) {
$json = $matches[1];
}
$data = json_decode($json, true);
$result['parsed'] = $data;
if (!$data || !is_array($data)) {
$result['error'] = 'JSON解析失败';
return $result;
}
// 优先使用 addr 字段
if (isset($data['addr']) && trim($data['addr']) !== '') {
$addr = trim($data['addr']);
if (strpos($addr, '中国 ') !== 0) {
$result['location'] = $addr;
return $result;
}
}
// 尝试用 pro + city 手动拼接
if (isset($data['pro']) && trim($data['pro']) !== '') {
$loc = trim($data['pro']);
if (isset($data['city']) && trim($data['city']) !== '') {
$loc .= trim($data['city']);
}
if (isset($data['addr'])) {
$parts = preg_split('/\s+/', trim($data['addr']), -1, PREG_SPLIT_NO_EMPTY);
if (!empty($parts)) {
$op = end($parts);
if ($op !== '中国') {
$loc .= ' ' . $op;
}
}
}
$result['location'] = strlen($loc) > 1 ? $loc : null;
}
return $result;
}
function testFetchBaidu($ip) {
$url = 'https://opendata.baidu.com/api.php?query=' . urlencode($ip) . '&resource_id=6006&oe=utf8&format=json';
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 5,
'ignore_errors' => true,
'header' => "Accept: application/json\r\nUser-Agent: Mozilla/5.0"
]
]);
$response = @file_get_contents($url, false, $ctx);
$result = [
'api' => 'Baidu',
'url' => $url,
'raw_response' => $response ?: 'FAILED',
'parsed' => null,
'location' => null,
'error' => null
];
if ($response === false || empty(trim($response))) {
$result['error'] = '请求失败';
return $result;
}
$data = json_decode(trim($response), true);
$result['parsed'] = $data;
if ($data && isset($data['data']) && is_array($data['data']) && !empty($data['data'])) {
$loc = $data['data'][0]['location'] ?? null;
if ($loc && $loc !== '--' && strpos($loc, '中国 ') !== 0) {
$result['location'] = $loc;
}
}
return $result;
}
function testFetchAa1($ip) {
$url = 'https://v.api.aa1.cn/api/ipcha-baidu/?ip=' . urlencode($ip);
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 5,
'ignore_errors' => true,
'header' => "Accept: application/json\r\nUser-Agent: Mozilla/5.0"
]
]);
$response = @file_get_contents($url, false, $ctx);
$result = [
'api' => 'aa1.cn',
'url' => $url,
'raw_response' => $response ?: 'FAILED',
'parsed' => null,
'location' => null,
'error' => null
];
if ($response === false || empty(trim($response))) {
$result['error'] = '请求失败';
return $result;
}
$data = json_decode(trim($response), true);
$result['parsed'] = $data;
if ($data && isset($data['code']) && $data['code'] == 1) {
$loc = $data['ip_add'] ?? null;
if ($loc && $loc !== '未知' && strpos($loc, '中国 ') !== 0) {
$result['location'] = $loc;
}
}
return $result;
}
function testFetchIp9($ip) {
// ★ 使用正确的 API 地址:ip9.com.cn/get
$url = 'https://ip9.com.cn/get?ip=' . urlencode($ip);
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'timeout' => 5,
'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"
],
'ssl' => [
'verify_peer' => false,
'verify_peer_name' => false
]
]);
$response = @file_get_contents($url, false, $ctx);
$result = [
'api' => 'IP9',
'url' => $url,
'raw_response' => $response ?: 'FAILED',
'parsed' => null,
'location' => null,
'error' => null
];
if ($response === false || empty(trim($response))) {
$result['error'] = '请求失败';
return $result;
}
$data = json_decode(trim($response), true);
$result['parsed'] = $data;
$debug = [];
// IP9 返回格式有两种可能:
// 旧版: {"ret":"200","data":[{"ip":"...","prov":"广东","city":"深圳","isp":"中国移动"}]}
// 新版: {"ret":"200","data":{"ip":"...","prov":"广东","city":"深圳","isp":"中国移动"}}
// ★ 兼容两种格式
$item = null;
if (isset($data['data'][0]) && is_array($data['data'][0])) {
$item = $data['data'][0];
$debug['format'] = 'array';
} elseif (isset($data['data']) && is_array($data['data']) && isset($data['data']['prov'])) {
$item = $data['data'];
$debug['format'] = 'object';
} else {
$debug['format'] = 'unknown';
$debug['data_type'] = gettype($data['data'] ?? null);
}
if ($item) {
$loc = '';
$debug['item_prov'] = $item['prov'] ?? 'NOT_SET';
$debug['item_city'] = $item['city'] ?? 'NOT_SET';
$debug['item_isp'] = $item['isp'] ?? 'NOT_SET';
if (isset($item['prov'])) $loc .= $item['prov'];
if (isset($item['city'])) $loc .= $item['city'];
if (isset($item['isp'])) $loc .= ' ' . $item['isp'];
$debug['loc'] = $loc;
$debug['loc_len'] = strlen($loc);
if (strlen($loc) > 0) {
$result['location'] = $loc;
}
}
$result['debug'] = $debug;
return $result;
}
// 测试每个IP
foreach ($allIps as $ip) {
echo "<h4>测试IP: $ip</h4>";
// API1: PConline
echo "<div class='api-result'>";
echo "<p><strong>API1 - PConline:</strong></p>";
$start = microtime(true);
$r1 = testFetchPconline($ip);
$time1 = (microtime(true) - $start) * 1000;
echo "<pre>";
echo "耗时: " . round($time1, 2) . "ms\n";
echo "原始响应: " . substr($r1['raw_response'], 0, 500) . "\n";
echo "解析结果: " . json_encode($r1['parsed'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
echo "提取位置: " . ($r1['location'] ?? 'null') . "\n";
echo "错误: " . ($r1['error'] ?? '无');
echo "</pre>";
echo "</div>";
// API2: Baidu
echo "<div class='api-result'>";
echo "<p><strong>API2 - Baidu:</strong></p>";
$start = microtime(true);
$r2 = testFetchBaidu($ip);
$time2 = (microtime(true) - $start) * 1000;
echo "<pre>";
echo "耗时: " . round($time2, 2) . "ms\n";
echo "原始响应: " . substr($r2['raw_response'], 0, 500) . "\n";
echo "解析结果: " . json_encode($r2['parsed'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
echo "提取位置: " . ($r2['location'] ?? 'null') . "\n";
echo "错误: " . ($r2['error'] ?? '无');
echo "</pre>";
echo "</div>";
// API3: aa1.cn
echo "<div class='api-result'>";
echo "<p><strong>API3 - aa1.cn:</strong></p>";
$start = microtime(true);
$r3 = testFetchAa1($ip);
$time3 = (microtime(true) - $start) * 1000;
echo "<pre>";
echo "耗时: " . round($time3, 2) . "ms\n";
echo "原始响应: " . substr($r3['raw_response'], 0, 500) . "\n";
echo "解析结果: " . json_encode($r3['parsed'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
echo "提取位置: " . ($r3['location'] ?? 'null') . "\n";
echo "错误: " . ($r3['error'] ?? '无');
echo "</pre>";
echo "</div>";
// API4: IP9
echo "<div class='api-result'>";
echo "<p><strong>API4 - IP9:</strong></p>";
$start = microtime(true);
$r4 = testFetchIp9($ip);
$time4 = (microtime(true) - $start) * 1000;
echo "<pre>";
echo "耗时: " . round($time4, 2) . "ms\n";
echo "原始响应: " . substr($r4['raw_response'], 0, 500) . "\n";
echo "解析结果: " . json_encode($r4['parsed'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
echo "提取位置: " . ($r4['location'] ?? 'null') . "\n";
echo "错误: " . ($r4['error'] ?? '无') . "\n";
echo "调试信息: " . json_encode($r4['debug'] ?? [], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
echo "</pre>";
echo "</div>";
echo "<hr>";
}
echo "</div>";
// ===== 第3步:模拟入库逻辑 =====
echo "<div class='step'>";
echo "<h3>🔄 步骤3: 模拟入库逻辑</h3>";
echo "<p>以下是服务器处理IP归属地的完整流程:</p>";
// 模拟 queryIpLocation 函数的逻辑
foreach ($allIps as $ip) {
echo "<h4>模拟IP: $ip</h4>";
echo "<p><strong>1. 检查是否是内网IP</strong></p>";
if (!$ip || $ip === '-' || $ip === '127.0.0.1' || strpos($ip, '10.') === 0 || strpos($ip, '192.168.') === 0) {
echo "<p class='warn'>是内网IP,返回 '内网IP'</p>";
continue;
}
echo "<p>不是内网IP,继续查询</p>";
echo "<p><strong>2. 检查数据库缓存</strong></p>";
$stmt = $db->prepare("SELECT location FROM player_ip_locations WHERE ip_address = :ip LIMIT 1");
$stmt->bindValue(':ip', $ip, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row && isset($row['location'])) {
$cachedLoc = $row['location'];
echo "<p>找到缓存: $cachedLoc</p>";
// ★ 使用统一校验函数
$isValidFormat = isValidIpLocationFormat($cachedLoc);
if (!$isValidFormat) {
echo "<p class='err'>无效格式: " . htmlspecialchars($cachedLoc) . "</p>";
}
echo "<p>格式验证结果: " . ($isValidFormat ? "✅ 有效" : "❌ 无效") . "</p>";
if (!$isValidFormat) {
echo "<p>将删除此缓存并重新查询</p>";
$stmt2 = $db->prepare("DELETE FROM player_ip_locations WHERE ip_address = :ip");
$stmt2->bindValue(':ip', $ip, SQLITE3_TEXT);
$stmt2->execute();
} else {
echo "<p class='ok'>格式有效,直接使用缓存</p>";
continue;
}
} else {
echo "<p>无缓存,需要查询</p>";
}
echo "<p><strong>3. 调用API查询(格式无效则继续下一个)</strong></p>";
// ★ 与 admin.php 的 doQueryIpLocation() 一致:每个API返回后先验证格式
$location = null;
$bestResult = null;
// API1: PConline
echo "<p>尝试 API1 (PConline)...</p>";
$r1 = testFetchPconline($ip);
if ($r1['location']) {
echo "<p>返回: " . htmlspecialchars($r1['location']) . "</p>";
if (isValidIpLocationFormat($r1['location'])) {
$location = $r1['location'];
echo "<p class='ok'>✅ 格式有效: $location</p>";
} else {
echo "<p class='warn'>⚠️ 格式无效,继续尝试下一个API</p>";
$bestResult = $r1['location'];
}
} else {
echo "<p class='err'>❌ API1 无结果</p>";
}
// API2: Baidu(仅当前面没有有效结果时才尝试)
if (!$location) {
echo "<p>尝试 API2 (Baidu)...</p>";
$r2 = testFetchBaidu($ip);
if ($r2['location']) {
echo "<p>返回: " . htmlspecialchars($r2['location']) . "</p>";
if (isValidIpLocationFormat($r2['location'])) {
$location = $r2['location'];
echo "<p class='ok'>✅ 格式有效: $location</p>";
} else {
echo "<p class='warn'>⚠️ 格式无效,继续尝试下一个API</p>";
if (!$bestResult) $bestResult = $r2['location'];
}
} else {
echo "<p class='err'>❌ API2 无结果</p>";
}
}
// API3: aa1.cn(仅当前面没有有效结果时才尝试)
if (!$location) {
echo "<p>尝试 API3 (aa1.cn)...</p>";
$r3 = testFetchAa1($ip);
if ($r3['location']) {
echo "<p>返回: " . htmlspecialchars($r3['location']) . "</p>";
if (isValidIpLocationFormat($r3['location'])) {
$location = $r3['location'];
echo "<p class='ok'>✅ 格式有效: $location</p>";
} else {
echo "<p class='warn'>⚠️ 格式无效,继续尝试下一个API</p>";
if (!$bestResult) $bestResult = $r3['location'];
}
} else {
echo "<p class='err'>❌ API3 无结果</p>";
}
}
// API4: IP9(仅当前面没有有效结果时才尝试)
if (!$location) {
echo "<p>尝试 API4 (IP9)...</p>";
$r4 = testFetchIp9($ip);
if ($r4['location']) {
echo "<p>返回: " . htmlspecialchars($r4['location']) . "</p>";
if (isValidIpLocationFormat($r4['location'])) {
$location = $r4['location'];
echo "<p class='ok'>✅ 格式有效: $location</p>";
} else {
echo "<p class='warn'>⚠️ 格式无效,继续尝试下一个API</p>";
if (!$bestResult) $bestResult = $r4['location'];
}
} else {
echo "<p class='err'>❌ API4 无结果</p>";
}
}
// 所有API都没返回有效格式:标记为查询失败
if (!$location) {
$location = '查询失败';
echo '<p class="err">❌ 所有API均失败或格式无效,标记为"查询失败"</p>';
}
echo "<p><strong>4. 格式验证和修复(与 admin.php 一致)</strong></p>";
echo "<p>原始位置: $location</p>";
// 格式修复逻辑
if ($location && $location !== '查询失败') {
$loc = $location;
// 1. 过滤英文格式
if (strpos($loc, 'China - ') === 0 || strpos($loc, 'China -') === 0) {
echo "<p class='warn'>拒绝英文格式: $loc</p>";
$location = null;
}
// 2. 过滤不完整的中文格式
if (strpos($loc, '中国 - -') === 0 || strpos($loc, '中国 - -') === 0) {
echo "<p class='warn'>拒绝不完整格式: $loc</p>";
$location = null;
}
// 3. 格式修复:确保"位置"和"运营商"之间有空格
// 仅在没有空格时修复(如"广东省移动"→"广东省 移动")
if ($location && strpos($loc, ' ') === false) {
// 无空格,尝试添加空格
if (preg_match('/^(.*?(?:省|市|州|盟|地区|区|县))(.+)$/u', $loc, $matches)) {
$loc = $matches[1] . ' ' . $matches[2];
echo "<p>格式修复: $location => $loc</p>";
$location = $loc;
}
}
// ★ 4. 存储前格式验证(使用统一校验函数)
if ($location) {
$shouldStore = isValidIpLocationFormat($location);
if (!$shouldStore) {
echo "<p class='err'>格式无效(不满足入库条件): " . htmlspecialchars($location) . "</p>";
$location = null;
}
}
}
echo "<p><strong>5. 存入数据库</strong></p>";
if ($location && $location !== '查询失败') {
echo "<p class='ok'>将存储: $location</p>";
// 模拟存储(不实际执行)
echo "<pre>INSERT OR REPLACE INTO player_ip_locations (player_name, ip_address, location, updated_at) VALUES ('global', '$ip', '$location', " . time() . ")</pre>";
} else {
echo "<p class='err'>不存储(查询失败或格式无效)</p>";
}
echo "<hr>";
}
echo "</div>";
// ===== 第4步:显示所有无效格式的记录 =====
echo "<div class='step'>";
echo "<h3>❌ 步骤4: 显示所有无效格式的记录</h3>";
$invalidStmt = $db->query("
SELECT ip_address, location
FROM player_ip_locations
WHERE location LIKE 'China%'
OR location = '-'
OR location = '--'
OR location = '查询失败'
OR location = ''
OR ip_address LIKE '127.%'
OR ip_address LIKE '192.168.%'
OR ip_address LIKE '10.%'
OR location LIKE '中国 - -%'
OR location LIKE '中国 - -%'
OR (location NOT LIKE '% %' AND (location LIKE '%省%' OR location LIKE '%市%'))
");
$invalidCount = 0;
while ($row = $invalidStmt->fetchArray(SQLITE3_ASSOC)) {
echo "<p>" . $row['ip_address'] . " => " . $row['location'] . "</p>";
$invalidCount++;
}
if ($invalidCount == 0) {
echo "<p class='ok'>没有发现无效格式的记录</p>";
} else {
echo "<p class='warn'>发现 $invalidCount 条无效格式的记录</p>";
}
echo "</div>";
echo "<hr>";
echo "<p>💡 此页面用于调试IP查询流程,检查每个API的返回结果和入库逻辑。</p>";
?>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
1781490108
+144
View File
@@ -0,0 +1,144 @@
<?php
// 管理后台API诊断脚本
while (ob_get_level() > 0) { @ob_end_clean(); }
header('Content-Type: text/plain; charset=utf-8');
error_reporting(E_ALL);
ini_set('display_errors', '1');
echo "=== Admin API Diagnostic ===\n\n";
require_once __DIR__ . '/core.php';
echo "1. Core loaded: " . (function_exists('getDB') ? 'YES' : 'NO') . "\n";
echo "2. ADMIN_PASS defined: " . (defined('ADMIN_PASS') ? 'YES' : 'NO') . "\n";
// Test database connection
echo "\n--- Database ---\n";
try {
$db = getDB();
echo "DB opened OK\n";
// List all tables
$result = $db->query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
$tables = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$tables[] = $row['name'];
}
echo "Tables (" . count($tables) . "): " . implode(', ', $tables) . "\n";
// Check player_ip_changes structure
if (in_array('player_ip_changes', $tables)) {
$result = $db->query("PRAGMA table_info(player_ip_changes)");
echo "\nplayer_ip_changes columns:\n";
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
echo " - {$row['name']} ({$row['type']}) PK=" . ($row['pk'] ? 'YES' : 'no') . "\n";
}
$cnt = $db->query("SELECT COUNT(*) as c FROM player_ip_changes")->fetchArray(SQLITE3_ASSOC)['c'];
echo " Rows: $cnt\n";
}
// Check player_ip_locations structure
if (in_array('player_ip_locations', $tables)) {
$result = $db->query("PRAGMA table_info(player_ip_locations)");
echo "\nplayer_ip_locations columns:\n";
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
echo " - {$row['name']} ({$row['type']}) PK=" . ($row['pk'] ? 'YES' : 'no') . "\n";
}
$cnt = $db->query("SELECT COUNT(*) as c FROM player_ip_locations")->fetchArray(SQLITE3_ASSOC)['c'];
echo " Rows: $cnt\n";
// Show a sample
$result = $db->query("SELECT ip_address, location FROM player_ip_locations LIMIT 5");
if ($result) {
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
echo " Sample: {$row['ip_address']} => {$row['location']}\n";
}
}
}
// Check users table
if (in_array('users', $tables)) {
$cnt = $db->query("SELECT COUNT(*) as c FROM users")->fetchArray(SQLITE3_ASSOC)['c'];
echo "\nusers rows: $cnt\n";
}
// Check online_players
if (in_array('online_players', $tables)) {
$cnt = $db->query("SELECT COUNT(*) as c FROM online_players")->fetchArray(SQLITE3_ASSOC)['c'];
echo "online_players rows: $cnt\n";
}
// Check web_session_log
if (in_array('web_session_log', $tables)) {
$cnt = $db->query("SELECT COUNT(*) as c FROM web_session_log")->fetchArray(SQLITE3_ASSOC)['c'];
echo "web_session_log rows: $cnt\n";
}
// Test batchGetPlayerIps
echo "\n--- Test batchGetPlayerIps ---\n";
if (function_exists('batchGetPlayerIps') || class_exists('adminListUsersPaginated')) {
echo "batchGetPlayerIps function NOT available in this scope\n";
} else {
echo "Function not available (defined in api/admin.php, not core.php)\n";
}
// Test simple queries that adminListUsersPaginated would run
echo "\n--- Test Query Simulation ---\n";
try {
$sql = "SELECT player_name, register_time, last_login_time, points, total_online_time, email FROM users ORDER BY register_time DESC LIMIT 10 OFFSET 0";
$result = $db->query($sql);
$count = 0;
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$count++;
}
echo "Users query OK, got $count rows\n";
} catch (\Throwable $e) {
echo "Users query FAILED: " . $e->getMessage() . "\n";
}
// Test player_ip_changes query
try {
$stmt = $db->prepare("SELECT player_name, new_ip AS ip_address FROM player_ip_changes WHERE player_name IN ('test') ORDER BY changed_at DESC");
echo "player_ip_changes simple query: OK (prepared)\n";
} catch (\Throwable $e) {
echo "player_ip_changes query FAILED: " . $e->getMessage() . "\n";
}
// Test player_ip_locations query
try {
$stmt = $db->prepare("SELECT ip_address, location FROM player_ip_locations WHERE ip_address IN ('1.2.3.4')");
echo "player_ip_locations query: OK (prepared)\n";
} catch (\Throwable $e) {
echo "player_ip_locations query FAILED: " . $e->getMessage() . "\n";
}
// Test location search query
try {
$stmt = $db->prepare("SELECT player_name, new_ip FROM player_ip_changes WHERE new_ip != '' AND new_ip != '-' AND new_ip NOT LIKE '10.%' ORDER BY changed_at DESC LIMIT 5");
$result = $stmt->execute();
$ips = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$ips[] = $row['new_ip'];
}
echo "Location search pre-query: OK, got " . count($ips) . " IPs\n";
} catch (\Throwable $e) {
echo "Location search pre-query FAILED: " . $e->getMessage() . "\n";
}
// Test region search SQL
try {
$searchBound = "%长沙%";
$stmt = $db->prepare("SELECT COUNT(*) as cnt FROM player_ip_locations WHERE location LIKE :region");
$stmt->bindValue(':region', $searchBound, SQLITE3_TEXT);
$cnt = $stmt->execute()->fetchArray(SQLITE3_ASSOC)['cnt'];
echo "Region cache check: $cnt matching records\n";
} catch (\Throwable $e) {
echo "Region cache check FAILED: " . $e->getMessage() . "\n";
}
} catch (\Throwable $e) {
echo "\nFATAL ERROR: " . $e->getMessage() . "\n";
echo "File: " . $e->getFile() . ":" . $e->getLine() . "\n";
echo "Stack: " . $e->getTraceAsString() . "\n";
}
echo "\n=== Done ===\n";
+210
View File
@@ -0,0 +1,210 @@
<?php
/**
* 调试页面 - 检查online_players表状态
*/
header('Content-Type: text/html; charset=utf-8');
require_once __DIR__ . '/core.php';
$db = getDB();
$result = [];
// 1. online_players表内容
try {
$rows = $db->query("SELECT * FROM online_players ORDER BY login_time DESC")->fetchArray(SQLITE3_ASSOC);
$onlinePlayers = [];
while ($row = $db->query("SELECT * FROM online_players ORDER BY login_time DESC")->fetchArray(SQLITE3_ASSOC)) {
$onlinePlayers[] = $row;
}
// 重新查询
$stmt = $db->query("SELECT * FROM online_players ORDER BY login_time DESC");
$onlinePlayers = [];
while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) {
$onlinePlayers[] = $row;
}
$result['online_players'] = $onlinePlayers;
$result['online_players_count'] = count($onlinePlayers);
} catch (Exception $e) {
$result['online_players_error'] = $e->getMessage();
}
// 2. online_player_hb表(心跳)
try {
$stmt = $db->query("SELECT * FROM online_player_hb");
$hb = $stmt->fetchArray(SQLITE3_ASSOC);
$result['heartbeat'] = $hb;
if ($hb) {
$result['heartbeat_ago'] = time() - (int)$hb['last_seen'] . '秒前';
}
} catch (Exception $e) {
$result['heartbeat_error'] = $e->getMessage();
}
// 3. weblogin_tokens表
try {
$stmt = $db->query("SELECT player_name, created_at, expire_seconds, datetime(created_at, 'unixepoch', 'localtime') as created_time FROM weblogin_tokens ORDER BY created_at DESC LIMIT 20");
$tokens = [];
while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) {
$tokens[] = $row;
}
$result['weblogin_tokens'] = $tokens;
} catch (Exception $e) {
$result['tokens_error'] = $e->getMessage();
}
// 4. web_session_log表
try {
$stmt = $db->query("SELECT player_name, login_time, datetime(login_time, 'unixepoch', 'localtime') as login_time_str FROM web_session_log ORDER BY login_time DESC LIMIT 20");
$sessions = [];
while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) {
$sessions[] = $row;
}
$result['web_session_log'] = $sessions;
} catch (Exception $e) {
$result['sessions_error'] = $e->getMessage();
}
// 5. web_login_verified表
try {
$stmt = $db->query("SELECT * FROM web_login_verified ORDER BY verified_at DESC LIMIT 20");
$verified = [];
while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) {
$verified[] = $row;
}
$result['web_login_verified'] = $verified;
} catch (Exception $e) {
$result['verified_error'] = $e->getMessage();
}
// 6. users表(部分)
try {
$stmt = $db->query("SELECT player_name, logged_in FROM users LIMIT 20");
$users = [];
while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) {
$users[] = $row;
}
$result['users'] = $users;
} catch (Exception $e) {
$result['users_error'] = $e->getMessage();
}
?>
<!DOCTYPE html>
<html>
<head>
<title>调试 - Online Players</title>
<style>
body { font-family: monospace; background: #1a1a2e; color: #e0e0e0; padding: 20px; }
h1 { color: #00d4ff; }
h2 { color: #ff6b6b; margin-top: 30px; border-bottom: 1px solid #333; padding-bottom: 5px; }
table { border-collapse: collapse; width: 100%; margin: 10px 0; }
th, td { border: 1px solid #444; padding: 8px 12px; text-align: left; }
th { background: #16213e; color: #00d4ff; }
td { background: #0f3460; }
.empty { color: #ff6b6b; font-weight: bold; }
.ok { color: #4ecca3; }
.warn { color: #ffd93d; }
pre { background: #0a0a23; padding: 15px; border-radius: 5px; overflow-x: auto; }
.refresh { background: #00d4ff; color: #000; padding: 10px 20px; border: none; cursor: pointer; font-size: 16px; border-radius: 5px; }
.refresh:hover { background: #00b8d4; }
</style>
</head>
<body>
<h1>🔍 Online Players 调试页面</h1>
<button class="refresh" onclick="location.reload()">🔄 刷新</button>
<p>当前时间: <?php echo date('Y-m-d H:i:s') . ' (Unix: ' . time() . ')'; ?></p>
<h2>1. online_players 表 (Java推送的游戏登录状态)</h2>
<?php if (empty($result['online_players'])): ?>
<p class="empty">⚠️ 表为空!没有玩家被标记为游戏中已登录</p>
<?php else: ?>
<table>
<tr><th>玩家名</th><th>登录时间</th><th>时间戳</th></tr>
<?php foreach ($result['online_players'] as $p): ?>
<tr>
<td><?= htmlspecialchars($p['player_name']) ?></td>
<td><?= date('Y-m-d H:i:s', (int)$p['login_time']) ?></td>
<td><?= $p['login_time'] ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<p>总数: <span class="<?= $result['online_players_count'] > 0 ? 'ok' : 'empty' ?>"><?= $result['online_players_count'] ?></span></p>
<h2>2. 心跳 (online_player_hb)</h2>
<?php if (isset($result['heartbeat']) && $result['heartbeat']): ?>
<p>上次心跳: <?= date('Y-m-d H:i:s', (int)$result['heartbeat']['last_seen']) ?> (<span class="warn"><?= $result['heartbeat_ago'] ?></span>)</p>
<?php
$ago = time() - (int)$result['heartbeat']['last_seen'];
if ($ago > 120) echo '<p class="empty">⚠️ 心跳超过120秒,下次同步时PHP会清空online_players表!</p>';
elseif ($ago > 60) echo '<p class="warn">⚠️ 心跳超过60秒</p>';
else echo '<p class="ok">✅ 心跳正常</p>';
?>
<?php else: ?>
<p class="empty">⚠️ 无心跳记录</p>
<?php endif; ?>
<h2>3. weblogin_tokens (Web登录Token)</h2>
<?php if (empty($result['weblogin_tokens'])): ?>
<p class="empty">⚠️ 无Token记录</p>
<?php else: ?>
<table>
<tr><th>玩家</th><th>创建时间</th><th>有效期(秒)</th></tr>
<?php foreach ($result['weblogin_tokens'] as $t): ?>
<tr>
<td><?= htmlspecialchars($t['player_name']) ?></td>
<td><?= $t['created_time'] ?></td>
<td><?= $t['expire_seconds'] ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<h2>4. web_session_log (Web会话记录)</h2>
<?php if (empty($result['web_session_log'])): ?>
<p class="empty">⚠️ 无会话记录</p>
<?php else: ?>
<table>
<tr><th>玩家</th><th>登录时间</th><th>时间戳</th></tr>
<?php foreach ($result['web_session_log'] as $s): ?>
<tr>
<td><?= htmlspecialchars($s['player_name']) ?></td>
<td><?= $s['login_time_str'] ?></td>
<td><?= $s['login_time'] ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<h2>5. web_login_verified (Web密码验证)</h2>
<?php if (empty($result['web_login_verified'])): ?>
<p class="empty">⚠️ 无验证记录</p>
<?php else: ?>
<table>
<tr><th>玩家</th><th>验证时间</th></tr>
<?php foreach ($result['web_login_verified'] as $v): ?>
<tr>
<td><?= htmlspecialchars($v['player_name']) ?></td>
<td><?= date('Y-m-d H:i:s', (int)$v['verified_at']) ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<h2>6. users表 (玩家注册状态)</h2>
<?php if (empty($result['users'])): ?>
<p class="empty">⚠️ 无注册玩家</p>
<?php else: ?>
<table>
<tr><th>玩家名</th><th>logged_in</th></tr>
<?php foreach ($result['users'] as $u): ?>
<tr>
<td><?= htmlspecialchars($u['player_name']) ?></td>
<td class="<?= $u['logged_in'] ? 'ok' : 'warn' ?>"><?= $u['logged_in'] ? '✅ 已登录' : '❌ 未登录' ?></td>
</tr>
<?php endforeach; ?>
</table>
<?php endif; ?>
<h2>原始JSON</h2>
<pre><?= json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) ?></pre>
</body>
</html>
+142
View File
@@ -0,0 +1,142 @@
<?php
/**
* 调试地区搜索问题
* 显示 player_ip_locations 和 player_ip_changes 表内容,以及地区搜索SQL测试结果
*/
header('Content-Type: text/html; charset=utf-8');
require_once 'core.php';
$db = getDB();
echo "<h2>调试地区搜索</h2>";
// 1. player_ip_locations 表内容
echo "<h3>player_ip_locations 表</h3>";
try {
$result = $db->query("SELECT COUNT(*) as cnt FROM player_ip_locations");
$row = $result->fetchArray(SQLITE3_ASSOC);
echo "总记录数: " . $row['cnt'] . "<br>";
if ($row['cnt'] > 0) {
echo "<table border='1' cellpadding='5'>";
echo "<tr><th>ip_address</th><th>location</th><th>player_name</th><th>updated_at</th></tr>";
$result = $db->query("SELECT * FROM player_ip_locations ORDER BY updated_at DESC LIMIT 20");
while ($r = $result->fetchArray(SQLITE3_ASSOC)) {
echo "<tr>";
echo "<td>" . htmlspecialchars($r['ip_address']) . "</td>";
echo "<td>" . htmlspecialchars($r['location']) . "</td>";
echo "<td>" . htmlspecialchars($r['player_name']) . "</td>";
echo "<td>" . date('Y-m-d H:i:s', $r['updated_at']) . "</td>";
echo "</tr>";
}
echo "</table>";
} else {
echo "表为空!<br>";
}
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "<br>";
}
// 2. player_ip_changes 表内容
echo "<h3>player_ip_changes 表</h3>";
try {
$result = $db->query("SELECT COUNT(*) as cnt FROM player_ip_changes");
$row = $result->fetchArray(SQLITE3_ASSOC);
echo "总记录数: " . $row['cnt'] . "<br>";
if ($row['cnt'] > 0) {
echo "<table border='1' cellpadding='5'>";
echo "<tr><th>player_name</th><th>new_ip</th><th>changed_at</th></tr>";
$result = $db->query("SELECT * FROM player_ip_changes ORDER BY changed_at DESC LIMIT 20");
while ($r = $result->fetchArray(SQLITE3_ASSOC)) {
echo "<tr>";
echo "<td>" . htmlspecialchars($r['player_name']) . "</td>";
echo "<td>" . htmlspecialchars($r['new_ip']) . "</td>";
echo "<td>" . date('Y-m-d H:i:s', $r['changed_at']) . "</td>";
echo "</tr>";
}
echo "</table>";
} else {
echo "表为空!<br>";
}
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "<br>";
}
// 3. 地区搜索SQL测试
echo "<h3>地区搜索SQL测试</h3>";
$search = '长沙';
$regionKey = $search;
// 测试JOIN
$sql = "SELECT c.player_name FROM player_ip_changes c
INNER JOIN player_ip_locations l ON c.new_ip = l.ip_address
WHERE l.location LIKE '%$regionKey%' AND c.new_ip != '' AND c.new_ip != '-'
GROUP BY c.player_name";
echo "SQL: <pre>" . htmlspecialchars($sql) . "</pre>";
try {
$result = $db->query($sql);
$count = 0;
$players = [];
while ($r = $result->fetchArray(SQLITE3_ASSOC)) {
$count++;
$players[] = $r['player_name'];
}
echo "匹配玩家数: $count<br>";
if ($count > 0) {
echo "匹配玩家: " . implode(', ', $players) . "<br>";
}
} catch (Exception $e) {
echo "SQL错误: " . $e->getMessage() . "<br>";
}
// 4. 检查所有location中包含"长沙"的
echo "<h3>包含'长沙'的location记录</h3>";
try {
$result = $db->query("SELECT ip_address, location, player_name FROM player_ip_locations WHERE location LIKE '%长沙%'");
$found = false;
while ($r = $result->fetchArray(SQLITE3_ASSOC)) {
$found = true;
echo "IP: " . htmlspecialchars($r['ip_address']) . " → " . htmlspecialchars($r['location']) . "<br>";
}
if (!$found) {
echo "没有找到包含'长沙'的location记录<br>";
}
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "<br>";
}
// 5. 检查users表中是否有来自长沙的玩家(通过IP推断)
echo "<h3>测试:直接搜索users表</h3>";
try {
$result = $db->query("SELECT player_name, email FROM users LIMIT 10");
while ($r = $result->fetchArray(SQLITE3_ASSOC)) {
echo "玩家: " . htmlspecialchars($r['player_name']) . " (email: " . htmlspecialchars($r['email']) . ")<br>";
}
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "<br>";
}
// 6. 尝试手动查询一个IP的归属地
echo "<h3>测试:手动查询IP归属地</h3>";
try {
$result = $db->query("SELECT DISTINCT new_ip FROM player_ip_changes WHERE new_ip != '' AND new_ip != '-' AND new_ip NOT LIKE '10.%' AND new_ip NOT LIKE '192.168.%' AND new_ip != '127.0.0.1' LIMIT 5");
$ips = [];
while ($r = $result->fetchArray(SQLITE3_ASSOC)) {
$ips[] = $r['new_ip'];
}
if (!empty($ips)) {
echo "测试查询以下IP的归属地: " . implode(', ', $ips) . "<br>";
require_once 'api/admin.php';
foreach ($ips as $ip) {
$loc = queryIpLocationWithTimeout($ip, 3);
echo "IP: $ip → " . ($loc ? $loc['location'] : 'null') . "<br>";
}
} else {
echo "没有找到可用的外部IP<br>";
}
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "<br>";
}
?>
+167
View File
@@ -0,0 +1,167 @@
<?php
// 地区搜索诊断脚本 - 直接测试PHP两步法
while (ob_get_level() > 0) { @ob_end_clean(); }
header('Content-Type: text/plain; charset=utf-8');
error_reporting(E_ALL);
ini_set('display_errors', '1');
echo "=== Region Search Debug v2 ===\n\n";
require_once __DIR__ . '/core.php';
try {
$db = getDB();
echo "DB opened OK\n\n";
// 1. Check tables and data
echo "--- Data Overview ---\n";
$tables = ['users', 'player_ip_changes', 'player_ip_locations', 'web_session_log'];
foreach ($tables as $table) {
try {
$cnt = $db->query("SELECT COUNT(*) as c FROM $table")->fetchArray(SQLITE3_ASSOC)['c'];
echo "$table: $cnt rows\n";
} catch (\Throwable $e) {
echo "$table: ERROR - " . $e->getMessage() . "\n";
}
}
// 2. Check player_ip_locations samples
echo "\n--- player_ip_locations Samples ---\n";
try {
$result = $db->query("SELECT ip_address, location, player_name FROM player_ip_locations LIMIT 10");
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
echo " {$row['ip_address']} => '{$row['location']}' (player: {$row['player_name']})\n";
}
} catch (\Throwable $e) {
echo " ERROR: " . $e->getMessage() . "\n";
}
// 3. Test region search for "长沙"
$regionKey = '长沙';
echo "\n--- Testing Region Search: '$regionKey' ---\n";
// Step 1: Find IPs with matching location
try {
$locStmt = $db->prepare("SELECT ip_address FROM player_ip_locations WHERE location LIKE :region");
$locStmt->bindValue(':region', "%$regionKey%", SQLITE3_TEXT);
$locResult = $locStmt->execute();
$matchedIps = [];
while ($locRow = $locResult->fetchArray(SQLITE3_ASSOC)) {
$matchedIps[] = $locRow['ip_address'];
}
echo "Step 1: Found " . count($matchedIps) . " IPs with '$regionKey' location\n";
if (!empty($matchedIps)) {
echo " IPs: " . implode(', ', array_slice($matchedIps, 0, 10)) . "\n";
}
} catch (\Throwable $e) {
echo "Step 1 ERROR: " . $e->getMessage() . "\n";
$matchedIps = [];
}
// Step 2: Find players using those IPs
$matchedPlayers = [];
if (!empty($matchedIps)) {
$ipPlaceholders = implode(',', array_fill(0, count($matchedIps), '?'));
// From player_ip_changes
try {
$pStmt = $db->prepare("SELECT DISTINCT player_name FROM player_ip_changes WHERE new_ip IN ($ipPlaceholders)");
$idx = 1;
foreach ($matchedIps as $ip) { $pStmt->bindValue($idx++, $ip, SQLITE3_TEXT); }
$pResult = $pStmt->execute();
while ($pRow = $pResult->fetchArray(SQLITE3_ASSOC)) {
$matchedPlayers[] = $pRow['player_name'];
}
echo "Step 2a (player_ip_changes): Found " . count($matchedPlayers) . " players\n";
} catch (\Throwable $e) {
echo "Step 2a ERROR: " . $e->getMessage() . "\n";
}
// From web_session_log
try {
$wStmt = $db->prepare("SELECT DISTINCT player_name FROM web_session_log WHERE ip_address IN ($ipPlaceholders)");
$idx = 1;
foreach ($matchedIps as $ip) { $wStmt->bindValue($idx++, $ip, SQLITE3_TEXT); }
$wResult = $wStmt->execute();
$beforeCount = count($matchedPlayers);
while ($wRow = $wResult->fetchArray(SQLITE3_ASSOC)) {
if (!in_array($wRow['player_name'], $matchedPlayers)) {
$matchedPlayers[] = $wRow['player_name'];
}
}
echo "Step 2b (web_session_log): Found " . (count($matchedPlayers) - $beforeCount) . " new players\n";
} catch (\Throwable $e) {
echo "Step 2b ERROR: " . $e->getMessage() . "\n";
}
echo "Step 2 Total: " . count($matchedPlayers) . " players\n";
if (!empty($matchedPlayers)) {
echo " Players: " . implode(', ', array_slice($matchedPlayers, 0, 10)) . "\n";
}
}
// Step 3: Query users
if (!empty($matchedPlayers)) {
$namePlaceholders = implode(',', array_fill(0, count($matchedPlayers), '?'));
try {
$uStmt = $db->prepare("SELECT player_name, email FROM users WHERE player_name IN ($namePlaceholders) LIMIT 5");
$idx = 1;
foreach ($matchedPlayers as $pn) { $uStmt->bindValue($idx++, $pn, SQLITE3_TEXT); }
$uResult = $uStmt->execute();
$users = [];
while ($uRow = $uResult->fetchArray(SQLITE3_ASSOC)) {
$users[] = $uRow;
}
echo "Step 3: Found " . count($users) . " users\n";
foreach ($users as $u) {
echo " {$u['player_name']} ({$u['email']})\n";
}
} catch (\Throwable $e) {
echo "Step 3 ERROR: " . $e->getMessage() . "\n";
}
} else {
echo "Step 3: Skipped (no matching players)\n";
}
// 4. Also check what batchGetPlayerIps would return for all users
echo "\n--- batchGetPlayerIps Check ---\n";
try {
$allUsers = $db->query("SELECT player_name FROM users")->fetchArray(SQLITE3_ASSOC);
// Get a sample user
$sampleUser = 'youpaishidifu';
$stmt = $db->prepare("SELECT player_name, new_ip FROM player_ip_changes WHERE player_name = :name ORDER BY changed_at DESC LIMIT 1");
$stmt->bindValue(':name', $sampleUser, SQLITE3_TEXT);
$result = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if ($result) {
echo "$sampleUser IP from player_ip_changes: {$result['new_ip']}\n";
// Check if this IP has location
$locStmt = $db->prepare("SELECT location FROM player_ip_locations WHERE ip_address = :ip");
$locStmt->bindValue(':ip', $result['new_ip'], SQLITE3_TEXT);
$locResult = $locStmt->execute()->fetchArray(SQLITE3_ASSOC);
echo " Location: " . ($locResult ? $locResult['location'] : 'NOT FOUND') . "\n";
} else {
echo "$sampleUser NOT in player_ip_changes\n";
// Check web_session_log
$stmt2 = $db->prepare("SELECT ip_address FROM web_session_log WHERE player_name = :name ORDER BY login_time DESC LIMIT 1");
$stmt2->bindValue(':name', $sampleUser, SQLITE3_TEXT);
$result2 = $stmt2->execute()->fetchArray(SQLITE3_ASSOC);
if ($result2) {
echo "$sampleUser IP from web_session_log: {$result2['ip_address']}\n";
$locStmt2 = $db->prepare("SELECT location FROM player_ip_locations WHERE ip_address = :ip");
$locStmt2->bindValue(':ip', $result2['ip_address'], SQLITE3_TEXT);
$locResult2 = $locStmt2->execute()->fetchArray(SQLITE3_ASSOC);
echo " Location: " . ($locResult2 ? $locResult2['location'] : 'NOT FOUND') . "\n";
} else {
echo "$sampleUser NOT in web_session_log either\n";
}
}
} catch (\Throwable $e) {
echo "batchGetPlayerIps check ERROR: " . $e->getMessage() . "\n";
}
} catch (\Throwable $e) {
echo "\nFATAL ERROR: " . $e->getMessage() . "\n";
}
echo "\n=== Done ===\n";
?>
+115
View File
@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - 页面不存在</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #0a0a0f;
color: #e0e0e0;
font-family: 'Segoe UI', system-ui, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
}
.container {
text-align: center;
position: relative;
}
.glitch {
font-size: 120px;
font-weight: 900;
color: #ff2d55;
text-shadow: 0 0 10px #ff2d55, 0 0 40px #ff2d55, 0 0 80px #ff2d55;
animation: glitch 2s infinite;
position: relative;
}
.glitch::before, .glitch::after {
content: '404';
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
}
.glitch::before {
color: #0ff;
animation: glitch-top 3s infinite;
clip-path: polygon(0 0, 100% 0, 100% 33%, 0 33%);
text-shadow: 0 0 10px #0ff;
}
.glitch::after {
color: #f0f;
animation: glitch-bottom 2.5s infinite;
clip-path: polygon(0 67%, 100% 67%, 100% 100%, 0 100%);
text-shadow: 0 0 10px #f0f;
}
@keyframes glitch {
0%, 90%, 100% { transform: translate(0); }
92% { transform: translate(-5px, 2px); }
94% { transform: translate(5px, -2px); }
96% { transform: translate(-3px, -1px); }
98% { transform: translate(3px, 1px); }
}
@keyframes glitch-top {
0%, 85%, 100% { transform: translate(0); }
87% { transform: translate(3px, -2px); }
89% { transform: translate(-3px, 1px); }
}
@keyframes glitch-bottom {
0%, 88%, 100% { transform: translate(0); }
90% { transform: translate(-4px, 2px); }
93% { transform: translate(4px, -1px); }
}
.msg {
font-size: 18px;
color: #888;
margin-top: 30px;
letter-spacing: 2px;
}
.links {
margin-top: 40px;
display: flex;
gap: 20px;
justify-content: center;
}
.links a {
color: #ff2d55;
text-decoration: none;
padding: 10px 24px;
border: 1px solid #ff2d55;
border-radius: 6px;
transition: all 0.3s;
}
.links a:hover {
background: #ff2d55;
color: #fff;
box-shadow: 0 0 20px rgba(255,45,85,0.4);
}
.scanline {
position: fixed;
top: 0; left: 0; right: 0;
height: 2px;
background: rgba(255,255,255,0.03);
animation: scan 8s linear infinite;
}
@keyframes scan {
0% { top: 0; }
100% { top: 100vh; }
}
</style>
</head>
<body>
<div class="scanline"></div>
<div class="container">
<div class="glitch">404</div>
<div class="msg">页面不存在或已被移除</div>
<div class="links">
<a href="player.php">玩家商城</a>
<a href="admin_login.php">管理后台</a>
</div>
</div>
</body>
</html>
+401
View File
@@ -0,0 +1,401 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SDF1 - Web登录</title>
<style>
:root {
--bg: #0d1117; --card: #161b22; --border: #30363d;
--text: #e6edf3; --dim: #8b949e; --accent: #58a6ff;
--green: #3fb950; --red: #f85149; --yellow: #d29922;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg); color: var(--text);
font-family: 'Segoe UI', system-ui, sans-serif;
min-height: 100vh; display: flex; justify-content: center; align-items: center;
}
.login-card {
background: var(--card); border: 1px solid var(--border);
border-radius: 12px; padding: 40px; width: 400px; max-width: 90%;
text-align: center;
}
.login-card h1 { color: var(--accent); margin-bottom: 8px; font-size: 24px; }
.login-card .subtitle { color: var(--dim); margin-bottom: 32px; font-size: 14px; }
.status-box {
padding: 20px; border-radius: 8px; margin-bottom: 24px;
font-size: 14px; line-height: 1.8;
}
.status-box.loading { background: rgba(88,166,255,0.1); border: 1px solid var(--accent); }
.status-box.success { background: rgba(63,185,80,0.1); border: 1px solid var(--green); }
.status-box.error { background: rgba(248,81,73,0.1); border: 1px solid var(--red); }
.spinner {
display: inline-block; width: 20px; height: 20px;
border: 2px solid var(--dim); border-top-color: var(--accent);
border-radius: 50%; animation: spin 0.8s linear infinite;
vertical-align: middle; margin-right: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.btn {
padding: 12px 24px; border: none; border-radius: 6px;
cursor: pointer; font-size: 14px; font-weight: 600;
transition: all 0.2s; text-decoration: none; display: inline-block;
}
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:hover { background: #79c0ff; }
.btn-dim { background: var(--border); color: var(--dim); }
.btn-dim:hover { background: #484f58; color: var(--text); }
.actions { display: flex; gap: 8px; justify-content: center; }
.player-name { color: var(--green); font-weight: 600; font-size: 16px; }
</style>
</head>
<body>
<div class="login-card">
<h1>SDF1 Web登录</h1>
<p class="subtitle" id="subtitle">Token验证中...</p>
<!-- Token验证状态 -->
<div id="tokenSection">
<div id="statusBox" class="status-box loading">
<span class="spinner"></span> 正在验证登录Token...
</div>
</div>
<!-- 密码验证(Token通过后显示) -->
<div id="passwordSection" style="display:none">
<div id="pwdStatusBox" class="status-box" style="display:none"></div>
<div style="text-align:left;margin-bottom:16px">
<label style="color:var(--dim);font-size:13px;display:block;margin-bottom:4px">玩家名</label>
<input type="text" id="playerName" placeholder="玩家名" readonly
style="width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px;outline:none;box-sizing:border-box;opacity:0.7">
</div>
<!-- ★ 切换标签:密码/邮箱 -->
<div style="display:flex;gap:8px;margin-bottom:16px">
<button class="btn btn-primary" id="tabPassword" onclick="switchLoginTab('password')" style="flex:1">游戏密码</button>
<button class="btn btn-dim" id="tabEmail" onclick="switchLoginTab('email')" style="flex:1">邮箱验证码</button>
</div>
<!-- 密码登录 -->
<div id="loginPasswordTab">
<div style="text-align:left;margin-bottom:20px">
<label style="color:var(--dim);font-size:13px;display:block;margin-bottom:4px">游戏内密码</label>
<input type="password" id="password" placeholder="输入游戏内密码" maxlength="32"
style="width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px;outline:none;box-sizing:border-box">
<p style="color:var(--dim);font-size:12px;margin-top:8px;text-align:left">密码由游戏服务器验证,Web端不存储密码</p>
</div>
<button class="btn btn-primary" style="width:100%" onclick="doPasswordLogin()">登录到Web端</button>
</div>
<!-- 邮箱验证码登录 -->
<div id="loginEmailTab" style="display:none">
<div style="text-align:left;margin-bottom:12px">
<label style="color:var(--dim);font-size:13px;display:block;margin-bottom:4px">绑定邮箱</label>
<input type="text" id="maskedEmail" readonly placeholder="加载中..."
style="width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px;outline:none;box-sizing:border-box;opacity:0.7">
</div>
<div style="display:flex;gap:8px;margin-bottom:20px">
<input type="text" id="emailCode" placeholder="6位验证码" maxlength="6"
style="flex:1;padding:10px 14px;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:14px;outline:none;box-sizing:border-box">
<button class="btn btn-primary" id="sendCodeBtn" onclick="sendEmailCode()" style="white-space:nowrap">发送验证码</button>
</div>
<button class="btn btn-primary" style="width:100%" onclick="doEmailLogin()">登录到Web端</button>
</div>
</div>
<div class="actions" id="actions" style="display:none"></div>
</div>
<script>
const TOKEN = new URLSearchParams(location.search).get('token') || '';
const statusBox = document.getElementById('statusBox');
const actionsDiv = document.getElementById('actions');
const tokenSection = document.getElementById('tokenSection');
const passwordSection = document.getElementById('passwordSection');
const subtitle = document.getElementById('subtitle');
const pwdStatusBox = document.getElementById('pwdStatusBox');
// ===== 安全拦截:无Token则拒绝访问 =====
if (!TOKEN) {
document.body.innerHTML = `
<div style="text-align:center;padding:60px 20px;">
<h1 style="color:#f85149;margin-bottom:20px">🚫 访问被拒绝</h1>
<p style="color:var(--dim);font-size:16px;line-height:1.8">
Web登录必须通过游戏内Token验证。<br>
请在游戏中执行 <b style="color:var(--accent)">/sdf1_login weblogin</b> 或 <b>/web</b><br>
然后点击上方链接登录Web端。
</p>
</div>
`;
console.error('[登录页面] 无Token访问被拒绝');
throw new Error('NoToken');
}
// ===== 有Token → 验证Token =====
tokenSection.style.display = 'block';
doTokenLogin();
// ===== Token验证 =====
async function doTokenLogin() {
try {
const apiUrl = './api/sync.php?action=validate_weblogin_token';
const res = await fetch(apiUrl, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({web_token: TOKEN})
});
const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch (parseErr) {
statusBox.className = 'status-box error';
statusBox.innerHTML = '❌ 服务器响应异常<br><br><code style="font-size:11px;word-break:break-all;color:var(--dim)">' + text.substring(0, 300) + '</code>';
actionsDiv.style.display = 'flex';
actionsDiv.innerHTML = '<span style="color:var(--dim);font-size:13px">请在游戏中重新执行 <b>/sdf1_login weblogin</b></span>';
return;
}
if (data.success && data.data) {
const player = data.data.player;
localStorage.setItem('sdf1_player', player);
localStorage.setItem('sdf1_token', TOKEN);
localStorage.setItem('sdf1_login_time', Date.now());
// ★ Token验证成功 → 直接跳转到player.php(跳过密码,token本身即身份证明)
statusBox.className = 'status-box success';
statusBox.innerHTML = '✅ Token验证成功!<br><br>欢迎, <span class="player-name">' + player + '</span><br><small style="color:var(--dim)">正在跳转到玩家商城...</small>';
// ★ 同时写入web_session_log(让validateWebAccess能识别在线状态)
fetch('./api/sync.php?action=web_access_check', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({web_token: TOKEN, access_action: 'view'})
}).catch(() => {});
setTimeout(() => {
// ★ 跳转时带上token,确保player.php一定能读到
window.location.href = 'player.php?login=' + encodeURIComponent(player) + '&token=' + encodeURIComponent(TOKEN);
}, 800);
} else {
statusBox.className = 'status-box error';
statusBox.innerHTML = '❌ 登录失败<br><br>' + (data.message || 'Token无效或已过期');
actionsDiv.style.display = 'flex';
actionsDiv.innerHTML = '<span style="color:var(--dim);font-size:13px">请在游戏中重新执行 <b>/sdf1_login weblogin</b></span>';
}
} catch (e) {
statusBox.className = 'status-box error';
statusBox.innerHTML = '❌ 连接失败<br><br>' + e.message;
}
}
// ===== 密码登录 =====
async function doPasswordLogin() {
const player = document.getElementById('playerName').value.trim();
const password = document.getElementById('password').value;
if (!password) { showPwdError('请输入密码'); return; }
pwdStatusBox.className = 'status-box loading';
pwdStatusBox.style.display = 'block';
pwdStatusBox.innerHTML = '<span class="spinner"></span> 正在验证密码...';
try {
// 1. 提交登录请求
const apiUrl = './api/sync.php';
const reqRes = await fetch(apiUrl + '?action=web_login_request', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({player: player, password: password})
});
const reqData = await reqRes.json();
if (!reqData.success) {
showPwdError(reqData.message || '提交失败');
return;
}
const requestId = reqData.data.request_id;
// 2. 轮询结果
pwdStatusBox.innerHTML = '<span class="spinner"></span> 等待游戏服务器验证密码...';
let attempts = 0;
const maxAttempts = 60; // 最多等60秒
const pollInterval = setInterval(async () => {
attempts++;
if (attempts > maxAttempts) {
clearInterval(pollInterval);
showPwdError('验证超时,请稍后重试');
return;
}
try {
const pollRes = await fetch(apiUrl + '?action=check_web_login_result&player=' + encodeURIComponent(player) + '&request_id=' + requestId);
const pollText = await pollRes.text();
const pollData = JSON.parse(pollText);
console.log('[poll] status:', pollData.status || pollData.data?.status, 'data:', JSON.stringify(pollData.data).substring(0, 100));
if (pollData.success && pollData.data) {
const result = pollData.data;
if (result.status === 'success') {
clearInterval(pollInterval);
// 登录成功 → 进入玩家商城
pwdStatusBox.className = 'status-box success';
pwdStatusBox.innerHTML = '✅ 登录成功!<br><br>欢迎, <span class="player-name">' + player + '</span><br><small style="color:var(--dim)">正在跳转...</small>';
// ★ token已存入localStorage,跳转player.php带token参数
setTimeout(() => {
window.location.href = 'player.php?login=' + encodeURIComponent(player) + '&token=' + encodeURIComponent(TOKEN);
}, 500);
} else if (result.status === 'failed') {
clearInterval(pollInterval);
showPwdError(result.message || '密码错误');
}
// status === 'pending' → 继续轮询
} else if (pollData.data && pollData.data.status === 'pending') {
// 仍在pending状态,可能是请求过期,继续轮询
console.log('[poll] 请求pending或过期,继续等待...');
} else {
console.warn('[poll] 轮询响应异常:', JSON.stringify(pollData).substring(0, 200));
}
} catch (pollErr) {
console.error('[poll] 轮询异常:', pollErr);
}
}, 1000);
} catch (e) {
showPwdError('连接失败: ' + e.message);
}
}
function showPwdError(msg) {
pwdStatusBox.className = 'status-box error';
pwdStatusBox.style.display = 'block';
pwdStatusBox.innerHTML = '❌ ' + msg;
}
// 回车提交
document.getElementById('password').addEventListener('keydown', function(e) {
if (e.key === 'Enter') doPasswordLogin();
});
document.getElementById('emailCode').addEventListener('keydown', function(e) {
if (e.key === 'Enter') doEmailLogin();
});
// ===== 标签切换 =====
let currentLoginTab = 'password';
function switchLoginTab(tab) {
currentLoginTab = tab;
document.getElementById('loginPasswordTab').style.display = tab === 'password' ? 'block' : 'none';
document.getElementById('loginEmailTab').style.display = tab === 'email' ? 'block' : 'none';
document.getElementById('tabPassword').className = tab === 'password' ? 'btn btn-primary' : 'btn btn-dim';
document.getElementById('tabEmail').className = tab === 'email' ? 'btn btn-primary' : 'btn btn-dim';
document.getElementById('pwdStatusBox').style.display = 'none';
// 切到邮箱时加载脱敏邮箱
if (tab === 'email') loadMaskedEmail();
}
// ===== 加载脱敏邮箱 =====
async function loadMaskedEmail() {
const player = document.getElementById('playerName').value;
try {
const url = new URL('./api/sync.php', location.href);
url.searchParams.set('action', 'get_player_email');
url.searchParams.set('player', player);
const res = await fetch(url, { method: 'POST', headers: {'Content-Type': 'application/json'} });
const data = await res.json();
if (data.success && data.data) {
document.getElementById('maskedEmail').value = data.data.masked_email || '未绑定邮箱';
} else {
document.getElementById('maskedEmail').value = '未绑定邮箱';
}
} catch (e) {
document.getElementById('maskedEmail').value = '加载失败';
}
}
// ===== 发送邮箱验证码 =====
async function sendEmailCode() {
const player = document.getElementById('playerName').value;
const btn = document.getElementById('sendCodeBtn');
btn.disabled = true;
btn.textContent = '发送中...';
try {
const url = new URL('./api/sync.php', location.href);
url.searchParams.set('action', 'send_email_code');
const res = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({player: player, web_token: TOKEN})
});
const data = await res.json();
if (data.success) {
showPwdSuccess('验证码已发送至 ' + (data.data.masked_email || '**'));
let countdown = 60;
const timer = setInterval(() => {
btn.textContent = countdown + '秒后重发';
countdown--;
if (countdown <= 0) {
clearInterval(timer);
btn.disabled = false;
btn.textContent = '发送验证码';
}
}, 1000);
} else {
showPwdError(data.message || '发送失败');
btn.disabled = false;
btn.textContent = '发送验证码';
}
} catch (e) {
showPwdError('连接失败: ' + e.message);
btn.disabled = false;
btn.textContent = '发送验证码';
}
}
// ===== 邮箱验证码登录 =====
async function doEmailLogin() {
const player = document.getElementById('playerName').value;
const code = document.getElementById('emailCode').value.trim();
if (!code) { showPwdError('请输入验证码'); return; }
if (code.length !== 6) { showPwdError('验证码为6位数字'); return; }
pwdStatusBox.className = 'status-box loading';
pwdStatusBox.style.display = 'block';
pwdStatusBox.innerHTML = '<span class="spinner"></span> 正在验证邮箱验证码...';
try {
const url = new URL('./api/sync.php', location.href);
url.searchParams.set('action', 'verify_email_code');
const res = await fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({player: player, code: code, web_token: TOKEN})
});
const data = await res.json();
if (data.success) {
localStorage.setItem('sdf1_player', player);
localStorage.setItem('sdf1_token', TOKEN);
pwdStatusBox.className = 'status-box success';
pwdStatusBox.innerHTML = '✅ 邮箱验证成功!<br><br>欢迎, <span class="player-name">' + player + '</span><br><small style="color:var(--dim)">正在跳转...</small>';
setTimeout(() => {
window.location.href = 'player.php?login=' + encodeURIComponent(player);
}, 500);
} else {
showPwdError(data.message || '验证失败');
}
} catch (e) {
showPwdError('连接失败: ' + e.message);
}
}
function showPwdSuccess(msg) {
pwdStatusBox.className = 'status-box success';
pwdStatusBox.style.display = 'block';
pwdStatusBox.innerHTML = '✅ ' + msg;
}
</script>
</body>
</html>
+2001
View File
File diff suppressed because it is too large Load Diff
+266
View File
@@ -0,0 +1,266 @@
<?php
/**
* 密码重置页面
* 玩家通过邮件链接访问此页面重置密码
*/
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
require_once __DIR__ . '/core.php';
require_once __DIR__ . '/api/sync.php';
// 获取token参数
$token = isset($_GET['token']) ? $_GET['token'] : '';
if (!$token) {
die('无效的重置链接');
}
// 验证token
$db = getDB();
$stmt = $db->prepare("SELECT * FROM password_reset_tokens WHERE token = :token");
$stmt->bindValue(':token', $token, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if (!$row) {
die('重置链接无效或已过期');
}
if (time() > $row['expire_at']) {
die('重置链接已过期,请重新申请');
}
$player = $row['player_name'];
// 处理表单提交
$message = '';
$error = '';
$success = false;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$newPassword = isset($_POST['password']) ? $_POST['password'] : '';
$confirmPassword = isset($_POST['confirm_password']) ? $_POST['confirm_password'] : '';
if (!$newPassword || strlen($newPassword) < 6) {
$error = '密码长度至少6位';
} elseif ($newPassword !== $confirmPassword) {
$error = '两次输入的密码不一致';
} else {
// 生成新的密码凭证
$saltBytes = random_bytes(16);
$salt = base64_encode($saltBytes);
$passwordHash = base64_encode(hash('sha256', $saltBytes . $newPassword, true));
// 更新密码凭证
storeWebLoginCredentials($player, $passwordHash, $salt);
// 删除重置token
$stmt = $db->prepare("DELETE FROM password_reset_tokens WHERE token = :token");
$stmt->bindValue(':token', $token, SQLITE3_TEXT);
$stmt->execute();
$success = true;
$message = '密码重置成功!请在游戏中使用新密码登录';
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>重置密码 - Sdf1 Minecraft</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
max-width: 420px;
width: 100%;
padding: 40px;
}
h1 {
color: #333;
font-size: 28px;
text-align: center;
margin-bottom: 8px;
}
.subtitle {
color: #666;
font-size: 14px;
text-align: center;
margin-bottom: 32px;
}
.player-info {
background: #f5f5f5;
border-radius: 8px;
padding: 16px;
text-align: center;
margin-bottom: 24px;
}
.player-info strong {
color: #667eea;
font-size: 18px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
color: #333;
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
}
input[type="password"] {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
input[type="password"]:focus {
outline: none;
border-color: #667eea;
}
.btn {
width: 100%;
padding: 14px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.btn:active {
transform: translateY(0);
}
.alert {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
}
.alert-error {
background: #fee;
color: #c33;
border: 1px solid #fcc;
}
.alert-success {
background: #efe;
color: #363;
border: 1px solid #cfc;
}
.success-container {
text-align: center;
}
.success-icon {
font-size: 64px;
margin-bottom: 20px;
}
.success-text {
color: #363;
font-size: 18px;
margin-bottom: 24px;
}
.info-text {
color: #666;
font-size: 13px;
line-height: 1.6;
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #e0e0e0;
}
</style>
</head>
<body>
<div class="container">
<?php if ($success): ?>
<div class="success-container">
<div class="success-icon">✅</div>
<h1>重置成功</h1>
<p class="success-text"><?php echo htmlspecialchars($message); ?></p>
<div class="info-text">
<strong>提示:</strong><br>
请在Minecraft游戏中使用新密码重新登录Web端
</div>
</div>
<?php else: ?>
<h1>重置密码</h1>
<p class="subtitle">为您的账号设置新密码</p>
<div class="player-info">
<div style="color: #666; font-size: 13px; margin-bottom: 4px;">玩家</div>
<strong><?php echo htmlspecialchars($player); ?></strong>
</div>
<?php if ($error): ?>
<div class="alert alert-error">
<?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<form method="POST" action="">
<div class="form-group">
<label for="password">新密码</label>
<input type="password" id="password" name="password" required minlength="6" placeholder="至少6位字符">
</div>
<div class="form-group">
<label for="confirm_password">确认密码</label>
<input type="password" id="confirm_password" name="confirm_password" required minlength="6" placeholder="再次输入密码">
</div>
<button type="submit" class="btn">重置密码</button>
</form>
<div class="info-text">
<strong>注意:</strong><br>
• 密码重置后,请使用新密码登录Web端<br>
• 此链接30分钟内有效,使用后自动失效<br>
• 如果这不是您的操作,请忽略此邮件
</div>
<?php endif; ?>
</div>
<script>
// 密码确认
const form = document.querySelector('form');
if (form) {
form.addEventListener('submit', function(e) {
const password = document.getElementById('password').value;
const confirmPassword = document.getElementById('confirm_password').value;
if (password !== confirmPassword) {
e.preventDefault();
alert('两次输入的密码不一致');
}
});
}
</script>
</body>
</html>
+98
View File
@@ -0,0 +1,98 @@
<?php
/**
* 自更新脚本 - 通过HTTP POST上传文件内容
* 用法: curl -X POST -d "secret=KEY&action=update_file&target=api/admin.php" --data-binary @admin.php https://.../self_update.php
* 或: curl -X POST -F "secret=KEY" -F "action=update_file" -F "target=api/admin.php" -F "file=@admin.php" https://.../self_update.php
*/
header('Content-Type: text/plain; charset=utf-8');
@ob_start();
require_once __DIR__ . '/config.php';
$secret = $_POST['secret'] ?? $_GET['secret'] ?? '';
$action = $_POST['action'] ?? $_GET['action'] ?? '';
if ($secret !== SECRET_KEY) {
echo "ERROR: Invalid secret key\n";
exit;
}
$timestamp = date('Y-m-d H:i:s');
if ($action === 'update_file') {
$target = $_POST['target'] ?? '';
$allowedTargets = ['api/admin.php', 'api/sync.php', 'core.php', 'config.php'];
if (!in_array($target, $allowedTargets)) {
echo "ERROR: Target '$target' not in allowed list: " . implode(', ', $allowedTargets) . "\n";
exit;
}
$targetPath = __DIR__ . '/' . $target;
// Handle file upload
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$content = file_get_contents($_FILES['file']['tmp_name']);
} elseif (isset($_POST['content'])) {
$content = $_POST['content'];
} else {
// Read from raw POST body
$content = file_get_contents('php://input');
}
if (empty($content)) {
echo "ERROR: No file content received\n";
exit;
}
// Backup
$backupPath = $targetPath . '.bak';
if (file_exists($targetPath)) {
copy($targetPath, $backupPath);
}
// Write
$bytesWritten = file_put_contents($targetPath, $content);
if ($bytesWritten !== false) {
echo "SUCCESS: Updated $target ($bytesWritten bytes) at $timestamp\n";
echo "Backup: $backupPath\n";
// Verify
echo "Verify MD5: " . md5_file($targetPath) . "\n";
// Clear opcache
if (function_exists('opcache_reset')) {
@opcache_reset();
echo "OPCache: cleared\n";
}
if (function_exists('opcache_invalidate')) {
@opcache_invalidate($targetPath, true);
echo "OPCache: invalidated $target\n";
}
} else {
echo "ERROR: Failed to write file\n";
}
} elseif ($action === 'verify') {
echo "=== File Verification ===\n";
$files = ['api/admin.php', 'api/sync.php', 'core.php'];
foreach ($files as $f) {
$path = __DIR__ . '/' . $f;
if (file_exists($path)) {
$size = filesize($path);
$md5 = md5_file($path);
$mtime = date('Y-m-d H:i:s', filemtime($path));
echo "$f: $size bytes, MD5: $md5, modified: $mtime\n";
} else {
echo "$f: NOT FOUND\n";
}
}
} else {
echo "Usage: ?action=update_file|verify&secret=KEY\n";
}
echo "\nDone at $timestamp\n";
@ob_end_flush();
?>
+503
View File
@@ -0,0 +1,503 @@
<?php
header('Cache-Control: no-cache, no-store, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: 0');
$BUILD_VERSION = 'v' . filemtime(__FILE__);
$currentVersion = isset($_GET['_v']) ? $_GET['_v'] : '';
if ($currentVersion !== $BUILD_VERSION) {
$params = $_GET;
unset($params['_v']);
$queryString = http_build_query($params);
$path = strtok($_SERVER['REQUEST_URI'], '?');
$newUrl = $path . ($queryString ? '?' . $queryString . '&' : '?') . '_v=' . $BUILD_VERSION;
header('Location: ' . $newUrl, true, 302);
exit;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>服务商面板 - 草原探险</title>
<style>
:root { --bg:#0d1117; --card:#161b22; --border:#30363d; --text:#e6edf3; --dim:#8b949e; --accent:#58a6ff; --green:#3fb950; --red:#f85149; --yellow:#d29922; --purple:#bc8cff; }
* { margin:0; padding:0; box-sizing:border-box; }
body { background:var(--bg); color:var(--text); font-family:'Segoe UI',system-ui,sans-serif; min-height:100vh; }
.header { background:linear-gradient(135deg,#1a2e1a 0%,#0d1117 100%); border-bottom:1px solid var(--border); padding:14px 24px; display:flex; justify-content:space-between; align-items:center; }
.header h1 { font-size:18px; color:var(--green); }
.container { max-width:900px; margin:20px auto; padding:0 16px; }
.card { background:var(--card); border:1px solid var(--border); border-radius:8px; padding:20px; margin-bottom:16px; }
.card h2 { font-size:16px; margin-bottom:12px; color:var(--green); }
.btn { padding:8px 16px; border:none; border-radius:6px; cursor:pointer; font-size:13px; font-weight:600; transition:all 0.2s; }
.btn-primary { background:var(--accent); color:#fff; }
.btn-primary:hover { background:#79c0ff; }
.btn-green { background:var(--green); color:#fff; }
.btn-dim { background:var(--card); color:var(--dim); border:1px solid var(--border); }
.btn-dim:hover { color:var(--text); border-color:var(--accent); }
input, textarea, select { width:100%; padding:10px; border:1px solid var(--border); border-radius:6px; background:var(--bg); color:var(--text); font-size:14px; }
textarea { resize:vertical; font-family:monospace; }
.tabs { display:flex; gap:6px; margin-bottom:16px; flex-wrap:wrap; }
.tab { padding:6px 14px; background:var(--bg); border:1px solid var(--border); border-radius:4px; cursor:pointer; font-size:12px; color:var(--dim); }
.tab.active { background:var(--accent); color:#fff; border-color:var(--accent); }
.ticket-item { background:rgba(63,185,80,0.05); border:1px solid var(--border); border-radius:8px; padding:14px 16px; margin-bottom:8px; cursor:pointer; transition:all 0.2s; }
.ticket-item:hover { border-color:var(--green); }
.status-dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:6px; }
.md-content { font-size:14px; line-height:1.6; }
.md-content pre { background:rgba(63,185,80,0.1); padding:10px; border-radius:4px; overflow-x:auto; font-size:12px; }
.md-content code { background:rgba(63,185,80,0.15); padding:1px 4px; border-radius:2px; font-size:12px; }
.reply-box { background:rgba(63,185,80,0.05); border-radius:6px; padding:10px 14px; margin-bottom:8px; }
.login-box { max-width:400px; margin:100px auto; }
.login-box h2 { text-align:center; margin-bottom:20px; color:var(--green); }
.error { color:var(--red); font-size:13px; margin-top:8px; }
</style>
</head>
<body>
<div class="header">
<h1>🧑‍💻 服务商面板</h1>
<div style="display:flex;gap:8px;align-items:center">
<span id="providerInfo" style="color:var(--dim);font-size:13px"></span>
<button class="btn btn-dim" onclick="doLogout()" style="font-size:12px;padding:4px 12px">退出</button>
</div>
</div>
<div class="container" id="mainContainer">
<!-- 登录/面板 动态渲染 -->
</div>
<script>
const API = 'api/';
let TOKEN = localStorage.getItem('sdf1_provider_token') || '';
let PLAYER = localStorage.getItem('sdf1_provider_player') || '';
let PROVIDER_ROLE = '';
(async function() {
if (TOKEN && PLAYER) {
await checkProviderAuth();
} else {
renderLogin();
}
})();
function renderLogin() {
document.getElementById('mainContainer').innerHTML = `
<div class="card login-box">
<h2>🧑‍💻 服务商登录</h2>
<p style="color:var(--dim);text-align:center;font-size:13px;margin-bottom:16px">使用游戏账号密码登录(仅限已授权的服务商)</p>
<div style="margin-bottom:12px">
<label style="color:var(--dim);font-size:12px;display:block;margin-bottom:4px">玩家名</label>
<input id="spUser" placeholder="游戏内玩家名" value="${escH(PLAYER)}">
</div>
<div style="margin-bottom:12px">
<label style="color:var(--dim);font-size:12px;display:block;margin-bottom:4px">密码</label>
<input id="spPass" type="password" placeholder="游戏登录密码" onkeydown="if(event.key==='Enter')doProviderLogin()">
</div>
<button class="btn btn-primary" style="width:100%;padding:12px" onclick="doProviderLogin()">登录</button>
<p id="spError" class="error"></p>
</div>`;
}
async function doProviderLogin() {
const user = document.getElementById('spUser').value.trim();
const pass = document.getElementById('spPass').value;
const errEl = document.getElementById('spError');
if (!user || !pass) { errEl.textContent = '请输入用户名和密码'; return; }
try {
// 0. 先清理可能残留的旧请求
const cancelUrl = new URL(API + 'sync.php', location.href);
cancelUrl.searchParams.set('action', 'cancel_web_login');
cancelUrl.searchParams.set('player', user);
await fetch(cancelUrl);
// 1. 先创建weblogin token
const regUrl = new URL(API + 'sync.php', location.href);
regUrl.searchParams.set('action', 'web_login_request');
const regRes = await fetch(regUrl, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({player: user, password: pass})
});
const regData = await regRes.json();
if (!regData.success) { errEl.textContent = regData.message || '登录请求失败'; return; }
// 2. 等待Java端处理(轮询)
const requestId = regData.data && regData.data.request_id;
if (!requestId) { errEl.textContent = '未获取请求ID'; return; }
let verified = false;
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 1000));
const checkUrl = new URL(API + 'sync.php', location.href);
checkUrl.searchParams.set('action', 'check_web_login_result');
checkUrl.searchParams.set('request_id', requestId);
checkUrl.searchParams.set('player', user);
const checkRes = await fetch(checkUrl);
const checkData = await checkRes.json();
if (checkData.success && checkData.data && checkData.data.status === 'success') {
TOKEN = checkData.data.token || checkData.data.web_token || '';
verified = true;
break;
}
if (checkData.data && checkData.data.status === 'failed') {
// 密码错误,清理pending请求
const cancelUrl3 = new URL(API + 'sync.php', location.href);
cancelUrl3.searchParams.set('action', 'cancel_web_login');
cancelUrl3.searchParams.set('player', user);
await fetch(cancelUrl3);
errEl.textContent = checkData.data.message || '密码错误';
return;
}
}
if (!verified) {
// 超时,清理pending请求
const cancelUrl2 = new URL(API + 'sync.php', location.href);
cancelUrl2.searchParams.set('action', 'cancel_web_login');
cancelUrl2.searchParams.set('player', user);
await fetch(cancelUrl2);
errEl.textContent = '验证超时,请确认游戏服务器在线';
return;
}
// 3. 验证服务商身份
PLAYER = user;
localStorage.setItem('sdf1_provider_token', TOKEN);
localStorage.setItem('sdf1_provider_player', PLAYER);
await checkProviderAuth();
} catch (e) {
errEl.textContent = '登录失败: ' + e.message;
}
}
async function checkProviderAuth() {
try {
const url = new URL(API + 'ticket.php', location.href);
url.searchParams.set('action', 'provider_check');
url.searchParams.set('token', TOKEN);
const res = await fetch(url);
const data = await res.json();
if (data.success && data.data.is_provider) {
PROVIDER_ROLE = data.data.role || 'waiter';
document.getElementById('providerInfo').textContent = `${PLAYER} (${PROVIDER_ROLE})`;
renderPanel();
} else {
TOKEN = '';
localStorage.removeItem('sdf1_provider_token');
renderLogin();
setTimeout(() => {
const errEl = document.getElementById('spError');
if (errEl) errEl.textContent = '你不是授权的服务商,请联系管理员';
}, 100);
}
} catch (e) {
TOKEN = '';
localStorage.removeItem('sdf1_provider_token');
renderLogin();
}
}
let spView = 'available';
function renderPanel() {
const c = document.getElementById('mainContainer');
c.innerHTML = `
<div class="tabs">
<div class="tab ${spView==='available'?'active':''}" onclick="spView='available';renderPanel()">📋 抢单大厅</div>
<div class="tab ${spView==='my'?'active':''}" onclick="spView='my';renderPanel()">👤 我的工单</div>
</div>
<div id="spContent"><p style="color:var(--dim)">加载中...</p></div>`;
if (spView === 'available') loadAvailableTickets();
else loadMyProviderTickets();
}
async function loadAvailableTickets() {
const el = document.getElementById('spContent');
try {
const url = new URL(API + 'ticket.php', location.href);
url.searchParams.set('action', 'available');
url.searchParams.set('token', TOKEN);
const res = await fetch(url);
const data = await res.json();
if (!data.success) { el.innerHTML = `<p style="color:var(--red)">${data.message}</p>`; return; }
const list = data.data.list;
const typeMap = {bug:'🐛 Bug反馈',help:'❓ 求助',report:'📢 举报',apply:'📝 申请',other:'📎 其他'};
if (list.length === 0) {
el.innerHTML = `<div class="card"><p style="color:var(--dim);text-align:center;padding:30px">暂无待接工单</p></div>`;
return;
}
let html = '';
for (const t of list) {
const date = new Date(t.created_at * 1000).toLocaleString('zh-CN');
html += `<div class="ticket-item" onclick="viewSPTicket(${t.id},'available')">
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
<span style="font-weight:600;font-size:14px">#${t.id} ${escH(t.title)}</span>
<span style="font-size:12px;color:var(--yellow)">⏳ 待接</span>
</div>
<div style="font-size:12px;color:var(--dim)">${typeMap[t.type]||t.type} | 👤 ${escH(t.requester)} | 📅 ${date}</div>
</div>`;
}
el.innerHTML = html;
} catch (e) { el.innerHTML = `<p style="color:var(--red)">加载失败: ${e.message}</p>`; }
}
async function loadMyProviderTickets() {
const el = document.getElementById('spContent');
try {
const url = new URL(API + 'ticket.php', location.href);
url.searchParams.set('action', 'provider_list');
url.searchParams.set('token', TOKEN);
url.searchParams.set('type', 'processing');
const res = await fetch(url);
const data = await res.json();
if (!data.success) { el.innerHTML = `<p style="color:var(--red)">${data.message}</p>`; return; }
const list = data.data.list;
const typeMap = {bug:'🐛 Bug反馈',help:'❓ 求助',report:'📢 举报',apply:'📝 申请',other:'📎 其他'};
const statusMap = {submitted:{text:'⏳ 已提交',color:'var(--yellow)'},replied:{text:'💬 已回复',color:'var(--accent)'},completed:{text:'✅ 已完结',color:'var(--green)'},withdrawn:{text:'🔙 已撤销',color:'var(--dim)'},rejected:{text:'🚫 已驳回',color:'var(--red)'}};
if (list.length === 0) {
el.innerHTML = `<div class="card"><p style="color:var(--dim);text-align:center;padding:30px">暂无处理中的工单</p></div>`;
return;
}
let html = '';
for (const t of list) {
const s = statusMap[t.status] || {text: t.status, color: 'var(--dim)'};
const date = new Date(t.created_at * 1000).toLocaleString('zh-CN');
html += `<div class="ticket-item" onclick="viewSPTicket(${t.id},'my')">
<div style="display:flex;justify-content:space-between;margin-bottom:4px">
<span style="font-weight:600;font-size:14px">#${t.id} ${escH(t.title)}</span>
<span style="font-size:12px;color:${s.color}">${s.text}</span>
</div>
<div style="font-size:12px;color:var(--dim)">${typeMap[t.type]||t.type} | 👤 ${escH(t.requester)} | 📅 ${date}</div>
</div>`;
}
el.innerHTML = html;
} catch (e) { el.innerHTML = `<p style="color:var(--red)">加载失败: ${e.message}</p>`; }
}
async function viewSPTicket(id, from) {
const el = document.getElementById('spContent');
el.innerHTML = '<p style="color:var(--dim)">加载中...</p>';
try {
const url = new URL(API + 'ticket.php', location.href);
url.searchParams.set('action', 'detail');
url.searchParams.set('token', TOKEN);
url.searchParams.set('id', id);
const res = await fetch(url);
const data = await res.json();
if (!data.success) { el.innerHTML = `<p style="color:var(--red)">${data.message}</p>`; return; }
const t = data.data;
const typeMap = {bug:'🐛 Bug反馈',help:'❓ 求助',report:'📢 举报',apply:'📝 申请',other:'📎 其他'};
const statusMap = {submitted:{text:'⏳ 已提交',color:'var(--yellow)'},replied:{text:'💬 已回复',color:'var(--accent)'},completed:{text:'✅ 已完结',color:'var(--green)'},withdrawn:{text:'🔙 已撤销',color:'var(--dim)'},rejected:{text:'🚫 已驳回',color:'var(--red)'}};
const s = statusMap[t.status] || {text: t.status, color: 'var(--dim)'};
const date = new Date(t.created_at * 1000).toLocaleString('zh-CN');
const isMyTicket = t.assigned_to === PLAYER;
const canReply = !['withdrawn','rejected','completed'].includes(t.status) && isMyTicket;
const canGrab = t.status === 'submitted' && !t.assigned_to;
let html = `<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<h2 style="margin:0">#${t.id} ${escH(t.title)}</h2>
<span style="color:${s.color};font-size:13px">${s.text}</span>
</div>
<div style="font-size:12px;color:var(--dim);margin-bottom:12px">
${typeMap[t.type]||t.type} | 提交者: ${escH(t.requester)} | 处理人: ${t.assigned_to ? escH(t.assigned_to) : '未分配'} | ${date}
</div>
<div style="background:rgba(63,185,80,0.05);border:1px solid var(--border);border-radius:6px;padding:12px;margin-bottom:12px">
<div style="color:var(--dim);font-size:11px;margin-bottom:4px">描述</div>
<div class="md-content">${spRenderMd(t.description || '无描述')}</div>
</div>`;
if (t.reject_reason) {
html += `<div style="background:rgba(248,81,73,0.1);border:1px solid var(--red);border-radius:6px;padding:12px;margin-bottom:12px">
<div style="color:var(--red);font-size:11px;margin-bottom:4px">🚫 驳回原因</div>
<div class="md-content">${spRenderMd(t.reject_reason)}</div></div>`;
}
// 回复列表
html += `<div style="margin-bottom:12px"><div style="font-size:11px;color:var(--dim);margin-bottom:6px">💬 回复 (${t.replies ? t.replies.length : 0})</div>`;
if (t.replies && t.replies.length > 0) {
for (const r of t.replies) {
const rDate = new Date(r.created_at * 1000).toLocaleString('zh-CN');
const roleColors = {user:'var(--accent)',admin:'var(--red)',provider:'var(--green)'};
const roleNames = {user:'玩家',admin:'管理员',provider:'服务商'};
const bgColor = r.role === 'admin' ? 'rgba(248,81,73,0.08)' : r.role === 'provider' ? 'rgba(63,185,80,0.08)' : 'rgba(88,166,255,0.08)';
html += `<div class="reply-box" style="background:${bgColor}">
<div style="display:flex;justify-content:space-between;font-size:11px;margin-bottom:4px">
<span><b style="color:${roleColors[r.role]||'var(--text)'}">${escH(r.sender)}</b> <span style="color:var(--dim)">(${roleNames[r.role]||r.role})</span></span>
<span style="color:var(--dim)">${rDate}</span>
</div>
<div class="md-content">${spRenderMd(r.message)}</div>
</div>`;
}
} else {
html += `<p style="color:var(--dim);font-size:12px">暂无回复</p>`;
}
html += `</div>`;
// 操作
html += `<div style="display:flex;gap:8px;flex-wrap:wrap">`;
if (canGrab) {
html += `<button class="btn btn-green" onclick="grabSPTicket(${t.id})">📩 接单</button>`;
}
if (canReply) {
html += `<div style="width:100%;margin-top:8px">
<textarea id="spReplyMsg" rows="2" placeholder="输入回复内容(支持Markdown)" style="font-family:monospace"></textarea>
<div style="margin-top:6px;display:flex;gap:8px">
<button class="btn btn-primary" onclick="spReply(${t.id})">💬 回复</button>
<button class="btn btn-green" onclick="spComplete(${t.id})">✅ 标记完结</button>
</div>
</div>`;
}
html += `<button class="btn btn-dim" onclick="spView='${from}';renderPanel()" style="margin-top:8px">← 返回</button></div>`;
html += `</div>`;
el.innerHTML = html;
} catch (e) { el.innerHTML = `<p style="color:var(--red)">加载失败: ${e.message}</p>`; }
}
async function grabSPTicket(id) {
try {
const url = new URL(API + 'ticket.php', location.href);
url.searchParams.set('action', 'grab');
url.searchParams.set('token', TOKEN);
url.searchParams.set('id', id);
const res = await fetch(url);
const data = await res.json();
if (data.success) { viewSPTicket(id, 'available'); } else { alert(data.message); }
} catch (e) { alert('接单失败: ' + e.message); }
}
async function spReply(id) {
const msg = document.getElementById('spReplyMsg').value.trim();
if (!msg) { alert('请输入回复内容'); return; }
try {
const url = new URL(API + 'ticket.php', location.href);
url.searchParams.set('action', 'provider_reply');
url.searchParams.set('token', TOKEN);
url.searchParams.set('id', id);
const res = await fetch(url, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({message: msg})});
const data = await res.json();
if (data.success) { viewSPTicket(id, 'my'); } else { alert(data.message); }
} catch (e) { alert('回复失败: ' + e.message); }
}
async function spComplete(id) {
if (!confirm('确定标记此工单已完结?')) return;
try {
const url = new URL(API + 'ticket.php', location.href);
url.searchParams.set('action', 'complete');
url.searchParams.set('token', TOKEN);
url.searchParams.set('id', id);
const res = await fetch(url);
const data = await res.json();
if (data.success) { viewSPTicket(id, 'my'); } else { alert(data.message); }
} catch (e) { alert('操作失败: ' + e.message); }
}
function doLogout() {
TOKEN = '';
PLAYER = '';
localStorage.removeItem('sdf1_provider_token');
localStorage.removeItem('sdf1_provider_player');
renderLogin();
}
function escH(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function spRenderMd(text) {
if (!text) return '';
text = text.replace(/\r/g, '');
try {
let s = escH(text);
// 代码块(提取保护,防止<br>污染)
let cbs = [];
s = s.replace(/```(\w*)\n?([\s\S]*?)```/g, function(m, lang, code) {
let idx = cbs.length;
cbs.push('<pre style="background:rgba(63,185,80,0.1);padding:10px;border-radius:4px;overflow-x:auto;font-size:12px;white-space:pre-wrap">' + code.replace(/\n$/, '') + '</pre>');
return '\x00CB' + idx + '\x00';
});
// 行内代码
s = s.replace(/`([^`]+)`/g, '<code style="background:rgba(63,185,80,0.15);padding:1px 4px;border-radius:2px;font-size:12px">$1</code>');
// 删除线 ~~text~~
s = s.replace(/~~(.+?)~~/g, '<del>$1</del>');
// 粗体+斜体
s = s.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
s = s.replace(/\*(.+?)\*/g, '<i>$1</i>');
// 图片 ![alt](url)
s = s.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width:100%;border-radius:6px;margin:6px 0">');
// 链接(排除图片)
s = s.replace(/(?<!!)\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color:var(--accent)">$1</a>');
// 标题(从多到少匹配,严格匹配空格分隔)
s = s.replace(/^######\s+(.+)$/gm, '<h6 style="margin:3px 0;font-size:11px">$1</h6>');
s = s.replace(/^#####\s+(.+)$/gm, '<h5 style="margin:4px 0;font-size:12px">$1</h5>');
s = s.replace(/^####\s+(.+)$/gm, '<h5 style="margin:4px 0 2px;font-size:12px">$1</h5>');
s = s.replace(/^###\s+(.+)$/gm, '<h4 style="margin:6px 0 2px;font-size:13px">$1</h4>');
s = s.replace(/^##\s+(.+)$/gm, '<h3 style="margin:8px 0 4px;font-size:14px">$1</h3>');
s = s.replace(/^#\s+(.+)$/gm, '<h2 style="margin:10px 0 6px;font-size:15px">$1</h2>');
// 引用块(支持嵌套)
s = s.replace(/^(?:&gt;[ \t]*)+(.*)$/gm, function(m, content) {
let nestLevel = (m.match(/&gt;/g) || []).length;
let leftPad = Math.min(nestLevel * 6, 24);
let text = content || '&nbsp;';
return '<div style="border-left:3px solid var(--accent);padding:2px 8px;margin:4px 0 4px ' + leftPad + 'px;color:#aaa;font-style:italic;background:rgba(255,255,255,0.03)">' + text + '</div>';
});
// 表格处理
let tableLines = [];
let inTable = false;
let tableHtml = '';
let lines = s.split('\n');
let result = [];
for (let i = 0; i < lines.length; i++) {
let line = lines[i].trim();
if (line.match(/^\|(.+)\|$/)) {
// 分隔行 |---|---|
if (line.match(/^\|[\s\-:|]+\|$/)) {
inTable = true;
continue;
}
if (!inTable && i + 1 < lines.length && lines[i+1].trim().match(/^\|[\s\-:|]+\|$/)) {
// 下一行是分隔行,这是表头
inTable = true;
let cells = line.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
tableHtml = '<table style="border-collapse:collapse;width:100%;margin:6px 0;font-size:12px"><tr style="background:rgba(63,185,80,0.15)">';
for (let c of cells) tableHtml += '<th style="border:1px solid var(--border);padding:4px 8px;text-align:left">' + c + '</th>';
tableHtml += '</tr>';
continue;
}
if (inTable) {
let cells = line.replace(/^\||\|$/g, '').split('|').map(c => c.trim());
tableHtml += '<tr>';
for (let c of cells) tableHtml += '<td style="border:1px solid var(--border);padding:4px 8px">' + c + '</td>';
tableHtml += '</tr>';
continue;
}
}
if (inTable && tableHtml) {
tableHtml += '</table>';
result.push(tableHtml);
tableHtml = '';
inTable = false;
}
result.push(lines[i]);
}
if (tableHtml) {
tableHtml += '</table>';
result.push(tableHtml);
}
s = result.join('\n');
// 无序列表
s = s.replace(/^- (.+)$/gm, '<li style="margin-left:16px;font-size:12px">$1</li>');
// 分割线
s = s.replace(/^---$/gm, '<hr style="border:none;border-top:1px solid var(--border);margin:8px 0">');
// 换行
s = s.replace(/\n/g, '<br>');
// 还原代码块占位符
for (let i = 0; i < cbs.length; i++) {
s = s.replace('\x00CB' + i + '\x00', cbs[i]);
}
return s;
} catch(e) { console.error('[MD_RENDER] Error:', e); return escH(text).replace(/\n/g, '<br>'); }
}
</script>
</body>
</html>
+102
View File
@@ -0,0 +1,102 @@
<?php
/**
* 测试脚本 - 模拟Java推送online_players数据
* 用于验证PHP端syncOnlinePlayers是否正常工作
*/
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/core.php';
$db = getDB();
$action = $_GET['action'] ?? '';
if ($action === 'simulate_push') {
// 模拟Java推送一个已登录的玩家
$player = $_GET['player'] ?? 'yupaishidifu';
$secret = $_GET['secret'] ?? SECRET_KEY;
$players = [
['name' => $player, 'login_time' => time()]
];
$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("BEGIN IMMEDIATE");
try {
$db->exec("DELETE FROM online_players");
$stmt = $db->prepare("INSERT OR REPLACE INTO online_players (player_name, login_time) VALUES (:name, :time)");
$stmt->bindValue(':name', $player, SQLITE3_TEXT);
$stmt->bindValue(':time', time(), SQLITE3_INTEGER);
$stmt->execute();
$hbSql = $db->prepare("INSERT OR REPLACE INTO online_player_hb (id, last_seen) VALUES (1, :time)");
$hbSql->bindValue(':time', time(), SQLITE3_INTEGER);
$hbSql->execute();
$db->exec("COMMIT");
} catch (Exception $e) {
$db->exec("ROLLBACK");
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
exit;
}
// 验证
$stmt = $db->query("SELECT * FROM online_players");
$rows = [];
while ($row = $stmt->fetchArray(SQLITE3_ASSOC)) {
$rows[] = $row;
}
echo json_encode([
'success' => true,
'message' => "模拟推送玩家 $player 成功",
'online_players' => $rows,
'timestamp' => time()
], JSON_UNESCAPED_UNICODE);
exit;
}
if ($action === 'check_token') {
// 检查指定玩家的token是否存在
$player = $_GET['player'] ?? 'yupaishidifu';
$stmt = $db->prepare("SELECT * FROM weblogin_tokens WHERE player_name = :player ORDER BY created_at DESC LIMIT 1");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
echo json_encode([
'player' => $player,
'token_found' => $row !== false,
'token_data' => $row,
'timestamp' => time()
], JSON_UNESCAPED_UNICODE);
exit;
}
if ($action === 'validate') {
// 模拟validateWebAccess调用
$player = $_GET['player'] ?? 'yupaishidifu';
$token = $_GET['token'] ?? '';
if (!$token) {
// 查找最新token
$stmt = $db->prepare("SELECT web_token FROM weblogin_tokens WHERE player_name = :player ORDER BY created_at DESC LIMIT 1");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
$token = $row ? $row['web_token'] : '';
}
$result = validateWebAccess($token, 'view');
echo json_encode($result, JSON_UNESCAPED_UNICODE);
exit;
}
echo json_encode([
'error' => '未知操作',
'usage' => [
'test_sync.php?action=simulate_push&player=yupaishidifu',
'test_sync.php?action=check_token&player=yupaishidifu',
'test_sync.php?action=validate&player=yupaishidifu'
]
], JSON_UNESCAPED_UNICODE);
+32
View File
@@ -0,0 +1,32 @@
<?php
// FTP上传脚本
$ftpServer = '43.153.203.46';
$ftpPort = 61868;
$ftpUser = 'qwed';
$ftpPass = 'z22K2Ws8r38a';
$localFile = __DIR__ . '/debug_region_search.php';
$remoteDir = '/caoyuan.ypshidifu.cn/test1/';
$remoteFile = $remoteDir . 'debug_region_search.php';
echo "Connecting to FTP...\n";
$conn = ftp_connect($ftpServer, $ftpPort);
if (!$conn) {
die("FTP connection failed\n");
}
echo "Logging in...\n";
if (!ftp_login($conn, $ftpUser, $ftpPass)) {
die("FTP login failed\n");
}
echo "Uploading $localFile to $remoteFile...\n";
if (ftp_put($conn, $remoteFile, $localFile, FTP_BINARY)) {
echo "Upload successful\n";
} else {
echo "Upload failed\n";
}
ftp_close($conn);
echo "Done\n";
?>
+295
View File
@@ -0,0 +1,295 @@
<?php
/**
* HTTP文件上传器 - 用于在FTP不可用时通过HTTP上传文件
* 访问方式: upload_via_http.php?secret=YOUR_SECRET&action=upload
*
* 安全措施:
* 1. 需要SECRET_KEY认证
* 2. 只允许覆盖特定文件
* 3. 记录所有操作
*/
header('Content-Type: text/plain; charset=utf-8');
@ob_start();
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/core.php';
$secret = getParam('secret', '');
$action = getParam('action', '');
if ($secret !== SECRET_KEY) {
echo "ERROR: Invalid secret key\n";
exit;
}
$logFile = __DIR__ . '/upload_log.txt';
file_put_contents($logFile, date('Y-m-d H:i:s') . " | Action: $action | IP: " . $_SERVER['REMOTE_ADDR'] . "\n", FILE_APPEND);
if ($action === 'status') {
echo "=== Server Status ===\n";
echo "PHP Version: " . phpversion() . "\n";
echo "SQLite Version: " . SQLite3::version()['versionString'] . "\n";
echo "Time: " . date('Y-m-d H:i:s') . "\n";
echo "DB Path: " . (defined('DB_PATH') ? DB_PATH : 'undefined') . "\n";
if (defined('DB_PATH') && file_exists(DB_PATH)) {
$db = new SQLite3(DB_PATH);
$db->enableExceptions(false);
echo "\n=== Table Row Counts ===\n";
$tables = ['users', 'player_ip_changes', 'player_ip_locations', 'web_session_log', 'online_players', 'player_daily_logins', 'player_checkins'];
foreach ($tables as $t) {
try {
$cnt = $db->querySingle("SELECT COUNT(*) FROM $t");
echo "$t: $cnt rows\n";
} catch (\Throwable $e) {
echo "$t: ERROR - " . $e->getMessage() . "\n";
}
}
// 检查 player_ip_locations 中的格式
echo "\n=== IP Location Samples (first 10) ===\n";
try {
$r = $db->query("SELECT ip_address, location, player_name FROM player_ip_locations LIMIT 10");
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
echo $row['ip_address'] . " => " . $row['location'] . " (player: " . $row['player_name'] . ")\n";
}
} catch (\Throwable $e) {
echo "ERROR: " . $e->getMessage() . "\n";
}
// 检查 player_ip_changes 中的玩家IP
echo "\n=== Player IP Changes Samples (first 10) ===\n";
try {
$r = $db->query("SELECT player_name, new_ip, changed_at FROM player_ip_changes ORDER BY changed_at DESC LIMIT 10");
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
echo $row['player_name'] . " => " . $row['new_ip'] . " (at: " . date('Y-m-d H:i:s', $row['changed_at']) . ")\n";
}
} catch (\Throwable $e) {
echo "ERROR: " . $e->getMessage() . "\n";
}
// 检查 web_session_log 中的玩家IP
echo "\n=== Web Session Log Samples (first 10) ===\n";
try {
$r = $db->query("SELECT player_name, ip_address, login_time FROM web_session_log ORDER BY login_time DESC LIMIT 10");
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
echo $row['player_name'] . " => " . $row['ip_address'] . " (at: " . date('Y-m-d H:i:s', $row['login_time']) . ")\n";
}
} catch (\Throwable $e) {
echo "ERROR: " . $e->getMessage() . "\n";
}
// 检查 region 搜索模拟
echo "\n=== Region Search Simulation for '长沙' ===\n";
echo "Step 1: player_ip_locations with '长沙':\n";
try {
$r = $db->query("SELECT ip_address, location FROM player_ip_locations WHERE location LIKE '%长沙%'");
$cnt = 0;
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
echo " " . $row['ip_address'] . " => " . $row['location'] . "\n";
$cnt++;
}
echo " Found: $cnt IPs\n";
} catch (\Throwable $e) {
echo " ERROR: " . $e->getMessage() . "\n";
}
// 查找有长沙IP的玩家
echo "\nStep 2: Players with 长沙 IPs:\n";
try {
$r = $db->query("
SELECT DISTINCT c.player_name
FROM player_ip_changes c
INNER JOIN player_ip_locations l ON c.new_ip = l.ip_address
WHERE l.location LIKE '%长沙%'
UNION
SELECT DISTINCT w.player_name
FROM web_session_log w
INNER JOIN player_ip_locations l ON w.ip_address = l.ip_address
WHERE l.location LIKE '%长沙%'
");
$cnt = 0;
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
echo " " . $row['player_name'] . "\n";
$cnt++;
}
echo " Found: $cnt players\n";
} catch (\Throwable $e) {
echo " ERROR: " . $e->getMessage() . "\n";
}
$db->close();
} else {
echo "Database not found!\n";
}
echo "\n=== admin.php Version ===\n";
$adminFile = __DIR__ . '/api/admin.php';
if (file_exists($adminFile)) {
$content = file_get_contents($adminFile);
echo "File size: " . filesize($adminFile) . " bytes\n";
echo "Has _region_debug: " . (strpos($content, '_region_debug') !== false ? 'YES' : 'NO') . "\n";
echo "Has regionDebugEnabled: " . (strpos($content, 'regionDebugEnabled') !== false ? 'YES' : 'NO') . "\n";
echo "Has batchGetPlayerIps: " . (strpos($content, 'function batchGetPlayerIps') !== false ? 'YES' : 'NO') . "\n";
} else {
echo "admin.php not found!\n";
}
// ★ 模拟 admin 的精确 region search 逻辑
echo "\n=== Simulate Admin Region Search Logic ===\n";
$db = new SQLite3(DB_PATH);
$db->enableExceptions(false);
$regionKey = '长沙';
// Step 1: Get all player names
$allPlayerNames = [];
$r = $db->query("SELECT player_name FROM users");
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
$allPlayerNames[] = $row['player_name'];
}
echo "All players: " . count($allPlayerNames) . "\n";
// Step 2: Build playerIpMap from player_ip_changes
$playerIpMap = [];
$stmt = $db->prepare("SELECT player_name, new_ip FROM player_ip_changes WHERE new_ip != '' AND new_ip != '-' AND new_ip NOT LIKE '10.%' AND new_ip NOT LIKE '192.168.%' AND new_ip != '127.0.0.1'");
$r = $stmt->execute();
$changesCount = 0;
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
$changesCount++;
$pn = $row['player_name'];
$ip = trim($row['new_ip']);
if (!isset($playerIpMap[$pn])) $playerIpMap[$pn] = [];
if (!in_array($ip, $playerIpMap[$pn])) $playerIpMap[$pn][] = $ip;
}
echo "player_ip_changes rows: $changesCount\n";
echo "Players with IPs from changes: " . count($playerIpMap) . "\n";
// web_session_log is empty, skip
echo "web_session_log rows: 0 (skipped)\n";
// Step 3: Get IP locations
$ipLocMap = [];
$r = $db->query("SELECT ip_address, location FROM player_ip_locations");
while ($row = $r->fetchArray(SQLITE3_ASSOC)) {
$loc = trim($row['location'] ?? '');
$ip = trim($row['ip_address'] ?? '');
if ($loc && $ip) {
$ipLocMap[$ip] = $loc;
}
}
echo "IP locations cached: " . count($ipLocMap) . "\n";
// Show some sample IP from playerIpMap vs ipLocMap
echo "\n--- IP Comparison ---\n";
$samplePlayers = array_slice(array_keys($playerIpMap), 0, 5);
foreach ($samplePlayers as $pn) {
$ips = $playerIpMap[$pn];
echo "Player: $pn => IPs: " . implode(', ', $ips) . "\n";
foreach ($ips as $ip) {
if (isset($ipLocMap[$ip])) {
echo " FOUND in cache: $ip => " . $ipLocMap[$ip] . "\n";
} else {
echo " NOT in cache: $ip\n";
}
}
}
// Step 4: PHP matching
$matchedPlayers = [];
$matchDebug = [];
foreach ($playerIpMap as $pn => $ips) {
foreach ($ips as $ip) {
if (isset($ipLocMap[$ip])) {
$loc = $ipLocMap[$ip];
if (mb_stripos($loc, $regionKey) !== false) {
if (!in_array($pn, $matchedPlayers)) $matchedPlayers[] = $pn;
$matchDebug[] = "$ip=>$loc ($pn)";
break;
}
}
}
}
echo "\n=== PHP Match Result ===\n";
echo "Matched players: " . count($matchedPlayers) . "\n";
foreach ($matchedPlayers as $pn) {
echo " $pn\n";
}
// Check if any 长沙 IPs are NOT in the playerIpMap
echo "\n--- All 长沙 IPs in cache ---\n";
foreach ($ipLocMap as $ip => $loc) {
if (mb_stripos($loc, $regionKey) !== false) {
echo " $ip => $loc\n";
// Check which player has this IP
$foundPlayer = false;
foreach ($playerIpMap as $pn => $ips) {
if (in_array($ip, $ips)) {
echo " Has player: $pn\n";
$foundPlayer = true;
break;
}
}
if (!$foundPlayer) {
echo " NO player found with this IP!\n";
// Try partial match
foreach ($playerIpMap as $pn => $ips) {
foreach ($ips as $pip) {
if (strpos($ip, $pip) !== false || strpos($pip, $ip) !== false) {
echo " Partial match: $pip (player: $pn)\n";
}
}
}
}
}
}
$db->close();
} elseif ($action === 'upload' || (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK)) {
// 通过 POST 上传文件
// 支持 target= 和 path= 两种参数名
$target = $_POST['target'] ?? $_POST['path'] ?? '';
$allowed = ['api/admin.php', 'api/sync.php', 'api/cdk.php', 'api/ticket.php', 'api/register.php', 'api/shop.php', 'api/balance.php', 'core.php', 'config.php', 'player.php', 'admin.php', 'service.php', 'upload_via_http.php'];
if (!in_array($target, $allowed)) {
echo "ERROR: Target not allowed: $target\nAllowed: " . implode(', ', $allowed) . "\n";
exit;
}
$targetPath = __DIR__ . '/' . $target;
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$content = file_get_contents($_FILES['file']['tmp_name']);
} elseif (isset($_POST['content'])) {
$content = base64_decode($_POST['content']);
} else {
$content = file_get_contents('php://input');
}
if (empty($content)) {
echo "ERROR: No content\n";
exit;
}
if (file_exists($targetPath)) {
copy($targetPath, $targetPath . '.bak');
}
$bytes = file_put_contents($targetPath, $content);
echo "Wrote $bytes bytes to $target\n";
echo "MD5: " . md5_file($targetPath) . "\n";
if (function_exists('opcache_invalidate')) {
@opcache_invalidate($targetPath, true);
}
if (function_exists('opcache_reset')) {
@opcache_reset();
}
echo "OPCache cleared\n";
}
echo "\nDone.\n";
@ob_end_flush();
?>
+8
View File
@@ -0,0 +1,8 @@
<?php
// 版本标记文件 - 用于验证上传是否成功
header('Content-Type: text/plain');
echo "admin.php version: 2026-06-19-1527\n";
echo "File size: " . filesize(__DIR__ . '/api/admin.php') . " bytes\n";
echo "MD5: " . md5_file(__DIR__ . '/api/admin.php') . "\n";
echo "Time: " . date('Y-m-d H:i:s') . "\n";
?>