Files
Sdf1_login/api/balance.php
T

319 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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);
}
// ★ WAL checkpoint:释放WAL锁,减少database is locked概率
try { walCheckpoint(); } catch (\Throwable $ignored) {}
} 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;
}
}