chore: 从main分支移除web文件夹(PHP代码归php分支管辖)
This commit is contained in:
-2423
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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, '预览模式 - 您只能查看自己的账号信息');
|
||||
}
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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 +0,0 @@
|
||||
1781490108
|
||||
-115
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||
|
||||
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>');
|
||||
// 图片 
|
||||
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(/^(?:>[ \t]*)+(.*)$/gm, function(m, content) {
|
||||
let nestLevel = (m.match(/>/g) || []).length;
|
||||
let leftPad = Math.min(nestLevel * 6, 24);
|
||||
let text = content || ' ';
|
||||
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>
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user