chore: 从main分支移除web文件夹(PHP代码归php分支管辖)

This commit is contained in:
2026-06-24 11:17:18 +08:00
parent 302268d0da
commit 3662466434
21 changed files with 0 additions and 15561 deletions
-2423
View File
File diff suppressed because it is too large Load Diff
-88
View File
@@ -1,88 +0,0 @@
<!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
@@ -1,314 +0,0 @@
<?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
@@ -1,490 +0,0 @@
<?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
@@ -1,29 +0,0 @@
<?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
@@ -1,289 +0,0 @@
<?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
@@ -1,303 +0,0 @@
<?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
@@ -1,263 +0,0 @@
<?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
@@ -1,825 +0,0 @@
<?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
@@ -1,41 +0,0 @@
<?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
@@ -1,178 +0,0 @@
<?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>
-1
View File
@@ -1 +0,0 @@
1781490108
-115
View File
@@ -1,115 +0,0 @@
<!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
@@ -1,401 +0,0 @@
<!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
@@ -1,266 +0,0 @@
<?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>
-503
View File
@@ -1,503 +0,0 @@
<?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
@@ -1,102 +0,0 @@
<?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);