CDK兑换修复:maxlength扩展+日期搜索+商品分类+同IP折叠+数据库锁修复
This commit is contained in:
+43
-9
@@ -399,9 +399,22 @@ function adminGenToken() {
|
||||
function adminGenCDK() {
|
||||
requireAdminSession();
|
||||
$amount = (int)getParam('amount', 0);
|
||||
$min = (int)getParam('min', 0);
|
||||
$max = (int)getParam('max', 0);
|
||||
$count = (int)getParam('count', 0);
|
||||
if (!$amount || !$count) exit(json_encode(['success' => false, 'message' => '缺少amount或count参数'], JSON_UNESCAPED_UNICODE));
|
||||
if ($count > 100) exit(json_encode(['success' => false, 'message' => '单次最多生成100个CDK'], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
// 验证参数
|
||||
if ($count <= 0 || $count > 100) {
|
||||
exit(json_encode(['success' => false, 'message' => '数量必须在1-100之间'], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
if ($min > 0 && $max > 0 && $min <= $max) {
|
||||
// 区间模式
|
||||
} elseif ($amount > 0) {
|
||||
// 固定金额模式
|
||||
} else {
|
||||
exit(json_encode(['success' => false, 'message' => '缺少金额参数(amount或min+max)'], JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS cdk (
|
||||
@@ -419,9 +432,15 @@ function adminGenCDK() {
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
// 生成6位随机CDK码
|
||||
$code = strtoupper(substr(str_shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, 6));
|
||||
// 根据模式生成金额
|
||||
if ($min > 0 && $max > 0 && $min <= $max) {
|
||||
$genAmount = random_int($min, $max);
|
||||
} else {
|
||||
$genAmount = $amount;
|
||||
}
|
||||
$stmt = $db->prepare("INSERT OR IGNORE INTO cdk (code, amount, used, created_at) VALUES (:code, :amount, 0, :time)");
|
||||
$stmt->bindValue(':code', $code, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':amount', $amount, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':amount', $genAmount, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
|
||||
$stmt->execute();
|
||||
$generated[] = $code;
|
||||
@@ -1176,7 +1195,15 @@ function detectSearchType($search) {
|
||||
return 'ip';
|
||||
}
|
||||
|
||||
// 2. 日期格式检测
|
||||
// 2. 日期关键词(中英文)
|
||||
if (in_array($search, ['今天', '今日', '今天', '当日', 'today', 'Today', 'TODAY'])) {
|
||||
return 'date_keyword';
|
||||
}
|
||||
if (in_array($search, ['昨天', '昨日', '前一日', 'yesterday', 'Yesterday', 'YESTERDAY'])) {
|
||||
return 'date_keyword';
|
||||
}
|
||||
|
||||
// 3. 日期格式检测
|
||||
// 中文格式:2026年6月18日、2026年06月18日
|
||||
if (preg_match('/^\d{4}年\d{1,2}月\d{1,2}日$/', $search)) {
|
||||
return 'date';
|
||||
@@ -1224,8 +1251,15 @@ function detectSearchType($search) {
|
||||
function parseSearchDate($search) {
|
||||
$year = $month = $day = 0;
|
||||
|
||||
// 中英文关键词:今天/昨天
|
||||
$searchLower = mb_strtolower($search);
|
||||
if (in_array($searchLower, ['今天', '今日', '当日', 'today'])) {
|
||||
$year = (int)date('Y'); $month = (int)date('m'); $day = (int)date('d');
|
||||
} elseif (in_array($searchLower, ['昨天', '昨日', '前一日', 'yesterday'])) {
|
||||
$year = (int)date('Y', strtotime('-1 day')); $month = (int)date('m', strtotime('-1 day')); $day = (int)date('d', strtotime('-1 day'));
|
||||
}
|
||||
// 中文格式:2026年6月18日
|
||||
if (preg_match('/(\d{4})年(\d{1,2})月(\d{1,2})日/', $search, $m)) {
|
||||
elseif (preg_match('/(\d{4})年(\d{1,2})月(\d{1,2})日/', $search, $m)) {
|
||||
$year = (int)$m[1]; $month = (int)$m[2]; $day = (int)$m[3];
|
||||
}
|
||||
// 横线格式:2026-06-18
|
||||
@@ -1278,8 +1312,8 @@ function adminListUsersPaginated() {
|
||||
$ipSearch = $search;
|
||||
$whereClause = "WHERE player_name IN (SELECT player_name FROM player_ip_changes WHERE new_ip LIKE :ip) OR player_name IN (SELECT player_name FROM player_ip_locations WHERE ip_address LIKE :ip2)";
|
||||
$searchBound = "%$ipSearch%";
|
||||
} elseif ($searchType === 'date') {
|
||||
// 日期搜索:转换为时间戳范围
|
||||
} elseif ($searchType === 'date' || $searchType === 'date_keyword') {
|
||||
// 日期搜索:转换为时间戳范围(支持"今天""昨天"关键词)
|
||||
$dateRange = parseSearchDate($search);
|
||||
if ($dateRange) {
|
||||
$whereClause = "WHERE (last_login_time >= :dateStart AND last_login_time < :dateEnd) OR (register_time >= :dateStart AND register_time < :dateEnd)";
|
||||
@@ -1354,7 +1388,7 @@ function adminListUsersPaginated() {
|
||||
$sql = "SELECT player_name, register_time, last_login_time, points, total_online_time, email FROM users";
|
||||
|
||||
if ($search) {
|
||||
if ($searchType === 'date' && $dateRange) {
|
||||
if (in_array($searchType, ['date', 'date_keyword']) && $dateRange) {
|
||||
$stmt = $db->prepare($sql . " " . $whereClause . " ORDER BY last_login_time DESC LIMIT " . (int)$limit . " OFFSET " . (int)$offset);
|
||||
$stmt->bindValue(':dateStart', $dateRange['start'], SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':dateEnd', $dateRange['end'], SQLITE3_INTEGER);
|
||||
@@ -1395,7 +1429,7 @@ function adminListUsersPaginated() {
|
||||
// 获取总数
|
||||
$countSql = "SELECT COUNT(*) as cnt FROM users";
|
||||
if ($search) {
|
||||
if ($searchType === 'date' && $dateRange) {
|
||||
if (in_array($searchType, ['date', 'date_keyword']) && $dateRange) {
|
||||
$countStmt = $db->prepare($countSql . " " . $whereClause);
|
||||
$countStmt->bindValue(':dateStart', $dateRange['start'], SQLITE3_INTEGER);
|
||||
$countStmt->bindValue(':dateEnd', $dateRange['end'], SQLITE3_INTEGER);
|
||||
|
||||
+155
-41
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* CDK兑换API
|
||||
* CDK兑换API v2 - 20260620
|
||||
* POST ?action=exchange - CDK兑换(需token,由插件请求)
|
||||
* POST ?action=create - 创建CDK(需管理token)
|
||||
* GET ?action=list - CDK列表(需管理token)
|
||||
@@ -90,7 +90,11 @@ function cdkExchange($token) {
|
||||
$cdk = $result->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
if (!$cdk) {
|
||||
error('CDK不存在: ' . $code);
|
||||
// ★ 本地没找到 → 写待验证请求,轮询Java Sdf1_login返回结果
|
||||
$cdk = tryRemoteCdkValidation($db, $code, $player);
|
||||
if (!$cdk) {
|
||||
error('CDK不存在: ' . $code);
|
||||
}
|
||||
}
|
||||
|
||||
if ($cdk['used'] == 1) {
|
||||
@@ -125,51 +129,66 @@ function cdkExchange($token) {
|
||||
|
||||
// 执行兑换
|
||||
$now = time();
|
||||
$isRemote = !empty($cdk['_remote']);
|
||||
|
||||
// 标记CDK已使用
|
||||
$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();
|
||||
|
||||
// 更新余额
|
||||
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();
|
||||
}
|
||||
|
||||
// ★ 所有DB操作包装在事务中,防止database is locked
|
||||
// ★ 全部DB操作包装在事务中,防止database is locked
|
||||
$db->exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
// 记录流水
|
||||
$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], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
|
||||
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
|
||||
$stmt->execute();
|
||||
// 标记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();
|
||||
}
|
||||
|
||||
// ★ 写入sync_requests触发Java立即拉取(必须在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', $now, SQLITE3_INTEGER);
|
||||
$stmt2->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 (Exception $e) {
|
||||
try { $db->exec('ROLLBACK'); } catch (Exception $e2) {}
|
||||
} catch (\Throwable $e) {
|
||||
try { $db->exec('ROLLBACK'); } catch (\Throwable $e2) {}
|
||||
@error_log("[cdk] 兑换事务失败: " . $e->getMessage());
|
||||
}
|
||||
|
||||
success([
|
||||
@@ -367,3 +386,98 @@ function validateWebloginTokenForCDK($webToken) {
|
||||
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;
|
||||
}
|
||||
|
||||
+448
@@ -251,6 +251,24 @@ switch ($action) {
|
||||
case 'check_pending_transactions':
|
||||
checkPendingTransactions();
|
||||
break;
|
||||
case 'validate_cdk':
|
||||
validateCdk();
|
||||
break;
|
||||
case 'check_cdk_exists':
|
||||
checkCdkExists();
|
||||
break;
|
||||
case 'cdk_redeem_remote':
|
||||
cdkRedeemRemote();
|
||||
break;
|
||||
case 'pull_cdk_validate_requests':
|
||||
pullCdkValidateRequests();
|
||||
break;
|
||||
case 'migrate_cdk':
|
||||
migrateCdkTable();
|
||||
break;
|
||||
case 'push_cdk_validate_result':
|
||||
pushCdkValidateResult();
|
||||
break;
|
||||
default:
|
||||
error('未知操作: ' . $action);
|
||||
}
|
||||
@@ -810,6 +828,15 @@ function syncOnlinePlayers() {
|
||||
@error_log("[syncOnlinePlayers] Received $playerCount players: " . implode(', ', $playerNames));
|
||||
@error_log("[syncOnlinePlayers] Login times: " . implode(', ', $playerTimes) . " | Server time: $now");
|
||||
|
||||
// ★ 确保 player_ip_changes 表存在
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS player_ip_changes (
|
||||
player_name TEXT PRIMARY KEY,
|
||||
old_ip TEXT DEFAULT '',
|
||||
new_ip TEXT NOT NULL,
|
||||
changed_at INTEGER NOT NULL,
|
||||
synced_at INTEGER DEFAULT 0
|
||||
)");
|
||||
|
||||
// ★ 使用事务批量写入,防止 "database is locked"
|
||||
$db->exec("BEGIN IMMEDIATE");
|
||||
try {
|
||||
@@ -831,6 +858,32 @@ function syncOnlinePlayers() {
|
||||
} catch (Exception $e) {
|
||||
@error_log("[syncOnlinePlayers] FAIL insert $name: " . $e->getMessage());
|
||||
}
|
||||
|
||||
// ★ 如果Java推送了IP,同步更新 player_ip_changes(实时IP来源)
|
||||
$ip = $player['ip'] ?? '';
|
||||
if (!empty($ip)) {
|
||||
try {
|
||||
// 检查是否已有记录且IP相同
|
||||
$checkStmt = $db->prepare("SELECT new_ip FROM player_ip_changes WHERE player_name = :name");
|
||||
$checkStmt->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
$checkResult = $checkStmt->execute();
|
||||
$existing = $checkResult->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
if (!$existing || $existing['new_ip'] !== $ip) {
|
||||
$oldIp = $existing ? $existing['new_ip'] : '';
|
||||
$ipStmt = $db->prepare("INSERT OR REPLACE INTO player_ip_changes (player_name, old_ip, new_ip, changed_at, synced_at) VALUES (:name, :old, :new, :time, :synced)");
|
||||
$ipStmt->bindValue(':name', $name, SQLITE3_TEXT);
|
||||
$ipStmt->bindValue(':old', $oldIp, SQLITE3_TEXT);
|
||||
$ipStmt->bindValue(':new', $ip, SQLITE3_TEXT);
|
||||
$ipStmt->bindValue(':time', $now, SQLITE3_INTEGER);
|
||||
$ipStmt->bindValue(':synced', $now, SQLITE3_INTEGER);
|
||||
$ipStmt->execute();
|
||||
@error_log("[syncOnlinePlayers] IP更新: {$name} {$oldIp}→{$ip}");
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
@error_log("[syncOnlinePlayers] IP写入失败 {$name}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新心跳时间戳(使用INSERT OR REPLACE,有主键则更新,无则插入)
|
||||
@@ -2779,3 +2832,398 @@ function completeWebRegisterRequest() {
|
||||
|
||||
success(['request_id' => $requestId, 'status' => $status]);
|
||||
}
|
||||
|
||||
// ===== CDK验证(供sdf1插件调用)=====
|
||||
// sdf1插件本地CDK未找到时,请求Web后端验证
|
||||
function validateCdk() {
|
||||
$secret = getParam('secret');
|
||||
$code = getParam('code', '');
|
||||
$player = getParam('player', ''); // ★ 必须传player,否则无法加债券
|
||||
if (!$secret || $secret !== SECRET_KEY) error('认证失败');
|
||||
if (empty($code)) error('缺少CDK码');
|
||||
if (empty($player)) error('缺少player参数');
|
||||
|
||||
$db = getDB();
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS cdk (code TEXT PRIMARY KEY, amount INTEGER DEFAULT 0, used INTEGER DEFAULT 0, used_by TEXT DEFAULT '', created_at INTEGER DEFAULT 0, used_at INTEGER DEFAULT 0)");
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS bond_cache (player_name TEXT PRIMARY KEY, amount INTEGER DEFAULT 0, updated_at INTEGER DEFAULT 0)");
|
||||
|
||||
try {
|
||||
$stmt = $db->prepare("SELECT code, amount, used, used_by FROM cdk WHERE code = :code");
|
||||
$stmt->bindValue(':code', $code, SQLITE3_TEXT);
|
||||
$result = $stmt->execute();
|
||||
$row = $result->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
success(['found' => false, 'status' => 'not_found']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($row['used'] == 1) {
|
||||
success([
|
||||
'found' => true,
|
||||
'status' => 'already_used',
|
||||
'used_by' => $row['used_by'] ?? ''
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ 原子标记:防止并发双重兑换
|
||||
$updateStmt = $db->prepare("UPDATE cdk SET used = 1, used_by = :player, used_at = :time WHERE code = :code AND used = 0");
|
||||
$updateStmt->bindValue(':time', time(), SQLITE3_INTEGER);
|
||||
$updateStmt->bindValue(':code', $code, SQLITE3_TEXT);
|
||||
$updateStmt->bindValue(':player', $player, SQLITE3_TEXT);
|
||||
$updateStmt->execute();
|
||||
|
||||
if ($db->changes() == 0) {
|
||||
// 并发竞争:已被其他请求使用
|
||||
success(['found' => true, 'status' => 'already_used']);
|
||||
return;
|
||||
}
|
||||
|
||||
// ★ CDK有效:加债券 + 写流水 + 写sync_requests
|
||||
$amount = (int)$row['amount'];
|
||||
$now = time();
|
||||
|
||||
$db->exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
// 1. 加债券到bond_cache
|
||||
$chk = $db->prepare("SELECT amount FROM bond_cache WHERE player_name = :name");
|
||||
$chk->bindValue(':name', $player, SQLITE3_TEXT);
|
||||
$chkR = $chk->execute();
|
||||
$existing = $chkR->fetchArray(SQLITE3_ASSOC);
|
||||
$curBal = $existing ? (int)$existing['amount'] : 0;
|
||||
$newBal = $curBal + $amount;
|
||||
|
||||
if ($existing) {
|
||||
$u = $db->prepare("UPDATE bond_cache SET amount = :amt, updated_at = :t WHERE player_name = :n");
|
||||
$u->bindValue(':amt', $newBal, SQLITE3_INTEGER);
|
||||
$u->bindValue(':t', $now, SQLITE3_INTEGER);
|
||||
$u->bindValue(':n', $player, SQLITE3_TEXT);
|
||||
$u->execute();
|
||||
} else {
|
||||
$ins = $db->prepare("INSERT INTO bond_cache (player_name, amount, updated_at) VALUES (:n, :amt, :t)");
|
||||
$ins->bindValue(':n', $player, SQLITE3_TEXT);
|
||||
$ins->bindValue(':amt', $newBal, SQLITE3_INTEGER);
|
||||
$ins->bindValue(':t', $now, SQLITE3_INTEGER);
|
||||
$ins->execute();
|
||||
}
|
||||
|
||||
// 2. 写web_transactions流水(completed,因为Java端已标记used)
|
||||
$tx = $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)");
|
||||
$tx->bindValue(':player', $player, SQLITE3_TEXT);
|
||||
$tx->bindValue(':amount', $amount, SQLITE3_INTEGER);
|
||||
$tx->bindValue(':reason', "CDK兑换: {$code}", SQLITE3_TEXT);
|
||||
$tx->bindValue(':detail', json_encode(['code' => $code, 'source' => 'web_validate'], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
|
||||
$tx->bindValue(':time', $now, SQLITE3_INTEGER);
|
||||
$tx->execute();
|
||||
|
||||
// 3. 写sync_requests触发Java立即拉取
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS sync_requests (player_name TEXT PRIMARY KEY, created_at INTEGER NOT NULL)");
|
||||
$sr = $db->prepare("INSERT OR REPLACE INTO sync_requests (player_name, created_at) VALUES (:player, :time)");
|
||||
$sr->bindValue(':player', $player, SQLITE3_TEXT);
|
||||
$sr->bindValue(':time', $now, SQLITE3_INTEGER);
|
||||
$sr->execute();
|
||||
|
||||
$db->exec('COMMIT');
|
||||
} catch (\Throwable $e) {
|
||||
try { $db->exec('ROLLBACK'); } catch (\Throwable $e2) {}
|
||||
@error_log("[validateCdk] 事务失败: " . $e->getMessage());
|
||||
}
|
||||
|
||||
@error_log("[validateCdk] 成功: code={$code} player={$player} amount={$amount}");
|
||||
success([
|
||||
'found' => true,
|
||||
'status' => 'success',
|
||||
'amount' => $amount
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
@error_log('[validateCdk] Error: ' . $e->getMessage());
|
||||
error('验证失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CDK存在性检查(只检查不消耗)=====
|
||||
// 供Sdf1_login轮询时判断CDK是否存在于Web,不标记已使用
|
||||
function checkCdkExists() {
|
||||
$secret = getParam('secret');
|
||||
$code = getParam('code', '');
|
||||
if (!$secret || $secret !== SECRET_KEY) error('认证失败');
|
||||
if (empty($code)) error('缺少CDK码');
|
||||
|
||||
$db = getDB();
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS cdk (code TEXT PRIMARY KEY, amount INTEGER DEFAULT 0, used INTEGER DEFAULT 0, used_by TEXT DEFAULT '', created_at INTEGER DEFAULT 0, used_at INTEGER DEFAULT 0)");
|
||||
|
||||
try {
|
||||
$stmt = $db->prepare("SELECT code, amount, used, used_by FROM cdk WHERE code = :code");
|
||||
$stmt->bindValue(':code', $code, SQLITE3_TEXT);
|
||||
$result = $stmt->execute();
|
||||
$row = $result->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
// ★ 返回扁平JSON(不用success()包装),found用字符串(Java extractJsonStr只支持字符串值)
|
||||
if (!$row) {
|
||||
jsonResponse(['found' => 'false', 'status' => 'not_found']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($row['used'] == 1) {
|
||||
jsonResponse([
|
||||
'found' => 'true',
|
||||
'status' => 'already_used',
|
||||
'used_by' => $row['used_by'] ?? ''
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// CDK存在且未使用,只返回信息,不标记已使用
|
||||
// ★ amount必须返回字符串(Java extractJsonStr只解析字符串值,数字会被跳过)
|
||||
@error_log("[checkCdkExists] found: code={$code} amount={$row['amount']}");
|
||||
jsonResponse([
|
||||
'found' => 'true',
|
||||
'status' => 'available',
|
||||
'amount' => (string)$row['amount']
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
@error_log('[checkCdkExists] Error: ' . $e->getMessage());
|
||||
error('检查失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 远程CDK兑换:标记已使用 + 写流水(不加bond_cache,由Java本地处理) =====
|
||||
function cdkRedeemRemote() {
|
||||
$secret = getParam('secret');
|
||||
$code = getParam('code', '');
|
||||
$player = getParam('player', '');
|
||||
if (!$secret || $secret !== SECRET_KEY) error('认证失败');
|
||||
if (empty($code)) error('缺少CDK码');
|
||||
if (empty($player)) error('缺少player参数');
|
||||
|
||||
$db = getDB();
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS cdk (code TEXT PRIMARY KEY, amount INTEGER DEFAULT 0, used INTEGER DEFAULT 0, used_by TEXT DEFAULT '', created_at INTEGER DEFAULT 0, used_at INTEGER DEFAULT 0)");
|
||||
// ★ web_transactions表结构必须与其他函数一致
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS web_transactions (id INTEGER PRIMARY KEY AUTOINCREMENT, player_name TEXT NOT NULL, type TEXT NOT NULL, amount INTEGER NOT NULL, operator TEXT DEFAULT '', reason TEXT, detail TEXT, status TEXT DEFAULT 'pending', created_at INTEGER NOT NULL, processed_at INTEGER)");
|
||||
|
||||
try {
|
||||
$db->exec('BEGIN IMMEDIATE');
|
||||
|
||||
// 1. 检查CDK是否存在且未使用
|
||||
$stmt = $db->prepare("SELECT code, amount, used FROM cdk WHERE code = :code");
|
||||
$stmt->bindValue(':code', $code, SQLITE3_TEXT);
|
||||
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
if (!$row) {
|
||||
$db->exec('ROLLBACK');
|
||||
jsonResponse(['success' => 'false', 'status' => 'not_found']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($row['used'] == 1) {
|
||||
$db->exec('ROLLBACK');
|
||||
jsonResponse(['success' => 'true', 'status' => 'already_used']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 原子标记为已使用
|
||||
$updateStmt = $db->prepare("UPDATE cdk SET used = 1, used_by = :player, used_at = :time WHERE code = :code AND used = 0");
|
||||
$updateStmt->bindValue(':time', time(), SQLITE3_INTEGER);
|
||||
$updateStmt->bindValue(':code', $code, SQLITE3_TEXT);
|
||||
$updateStmt->bindValue(':player', $player, SQLITE3_TEXT);
|
||||
$updateStmt->execute();
|
||||
|
||||
if ($db->changes() == 0) {
|
||||
// 并发竞争:已被其他请求使用
|
||||
$db->exec('ROLLBACK');
|
||||
jsonResponse(['success' => 'true', 'status' => 'already_used']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 写入流水记录(与validateCdk格式一致)
|
||||
$amount = (int)$row['amount'];
|
||||
$now = time();
|
||||
$ins = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, operator, reason, detail, status, created_at) VALUES (:player, :type, :amount, :operator, :reason, :detail, :status, :time)");
|
||||
$ins->bindValue(':player', $player, SQLITE3_TEXT);
|
||||
$ins->bindValue(':type', 'cdk_redeem_remote', SQLITE3_TEXT);
|
||||
$ins->bindValue(':amount', $amount, SQLITE3_INTEGER);
|
||||
$ins->bindValue(':operator', '', SQLITE3_TEXT);
|
||||
$ins->bindValue(':reason', "CDK兑换: {$code}", SQLITE3_TEXT);
|
||||
$ins->bindValue(':detail', json_encode(['code' => $code, 'source' => 'game_remote'], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
|
||||
$ins->bindValue(':status', 'completed', SQLITE3_TEXT);
|
||||
$ins->bindValue(':time', $now, SQLITE3_INTEGER);
|
||||
$ins->execute();
|
||||
|
||||
$db->exec('COMMIT');
|
||||
|
||||
@error_log("[cdkRedeemRemote] success: code={$code} amount={$amount} player={$player}");
|
||||
jsonResponse([
|
||||
'success' => 'true',
|
||||
'status' => 'success',
|
||||
'amount' => (string)$amount
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
try { $db->exec('ROLLBACK'); } catch (\Throwable $ignored) {}
|
||||
@error_log('[cdkRedeemRemote] Error: ' . $e->getMessage());
|
||||
error('兑换失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Java拉取CDK验证请求 =====
|
||||
// ===== CDK表迁移修复 =====
|
||||
function migrateCdkTable() {
|
||||
$secret = getParam('secret');
|
||||
if (!$secret || $secret !== SECRET_KEY) error('认证失败');
|
||||
|
||||
$db = getDB();
|
||||
$log = [];
|
||||
|
||||
// 1. 确保cdk_validate_requests表存在且有player_name列
|
||||
$r = $db->querySingle("SELECT name FROM sqlite_master WHERE type='table' AND name='cdk_validate_requests'");
|
||||
if (!$r) {
|
||||
$db->exec("CREATE TABLE 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
|
||||
)");
|
||||
$log[] = 'Created cdk_validate_requests table';
|
||||
} else {
|
||||
$cols = [];
|
||||
$colResult = $db->query("PRAGMA table_info(cdk_validate_requests)");
|
||||
while ($col = $colResult->fetchArray(SQLITE3_ASSOC)) { $cols[] = $col['name']; }
|
||||
$log[] = 'Existing columns: ' . implode(', ', $cols);
|
||||
if (!in_array('player_name', $cols)) {
|
||||
$db->exec("ALTER TABLE cdk_validate_requests ADD COLUMN player_name TEXT DEFAULT ''");
|
||||
$log[] = 'Added player_name column';
|
||||
} else {
|
||||
$log[] = 'player_name column already exists';
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 确保cdk_validate_results表存在
|
||||
$r2 = $db->querySingle("SELECT name FROM sqlite_master WHERE type='table' AND name='cdk_validate_results'");
|
||||
if (!$r2) {
|
||||
$db->exec("CREATE TABLE 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
|
||||
)");
|
||||
$log[] = 'Created cdk_validate_results table';
|
||||
} else {
|
||||
$log[] = 'cdk_validate_results table exists';
|
||||
}
|
||||
|
||||
// 3. 确保cdk表存在
|
||||
$r3 = $db->querySingle("SELECT name FROM sqlite_master WHERE type='table' AND name='cdk'");
|
||||
if (!$r3) {
|
||||
$db->exec("CREATE TABLE cdk (
|
||||
code TEXT PRIMARY KEY,
|
||||
amount INTEGER DEFAULT 0,
|
||||
type TEXT DEFAULT 'bond',
|
||||
used INTEGER DEFAULT 0,
|
||||
used_by TEXT DEFAULT '',
|
||||
created_at INTEGER DEFAULT 0,
|
||||
used_at INTEGER DEFAULT 0
|
||||
)");
|
||||
$log[] = 'Created cdk table';
|
||||
} else {
|
||||
$log[] = 'cdk table exists';
|
||||
}
|
||||
|
||||
success(['log' => $log], '迁移完成');
|
||||
}
|
||||
|
||||
function pullCdkValidateRequests() {
|
||||
$secret = getParam('secret');
|
||||
if (!$secret || $secret !== SECRET_KEY) error('认证失败');
|
||||
|
||||
$db = getDB();
|
||||
// ★ 迁移操作用事务包装防锁
|
||||
try {
|
||||
$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
|
||||
)");
|
||||
$colResult = $db->query("PRAGMA table_info(cdk_validate_requests)");
|
||||
$cols = [];
|
||||
while ($col = $colResult->fetchArray(SQLITE3_ASSOC)) { $cols[] = $col['name']; }
|
||||
if (!in_array('player_name', $cols)) {
|
||||
$db->exec('BEGIN IMMEDIATE');
|
||||
$db->exec("ALTER TABLE cdk_validate_requests ADD COLUMN player_name TEXT DEFAULT ''");
|
||||
$db->exec('COMMIT');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
try { $db->exec('ROLLBACK'); } catch (\Throwable $e2) {}
|
||||
}
|
||||
|
||||
try {
|
||||
// ★ 返回player_name
|
||||
$result = $db->query("SELECT request_id, code, player_name FROM cdk_validate_requests WHERE status = 'pending' ORDER BY created_at ASC LIMIT 10");
|
||||
$requests = [];
|
||||
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||
$requests[] = $row;
|
||||
}
|
||||
success(['requests' => $requests]);
|
||||
} catch (\Throwable $e) {
|
||||
@error_log('[pullCdkValidateRequests] Error: ' . $e->getMessage());
|
||||
error('拉取失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Java推送CDK验证结果 =====
|
||||
function pushCdkValidateResult() {
|
||||
$secret = getParam('secret');
|
||||
if (!$secret || $secret !== SECRET_KEY) error('认证失败');
|
||||
|
||||
$requestId = getParam('request_id', '');
|
||||
$status = getParam('status', 'not_found'); // success / not_found / already_used
|
||||
$code = getParam('code', '');
|
||||
$amount = (int)getParam('amount', 0);
|
||||
|
||||
if (empty($requestId)) error('缺少request_id');
|
||||
|
||||
$db = getDB();
|
||||
$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
|
||||
)");
|
||||
|
||||
try {
|
||||
// ★ BEGIN IMMEDIATE防database is locked
|
||||
$db->exec('BEGIN IMMEDIATE');
|
||||
$now = time();
|
||||
$stmt = $db->prepare("INSERT OR REPLACE INTO cdk_validate_results (request_id, code, status, amount, message, created_at) VALUES (:id, :code, :status, :amount, :msg, :time)");
|
||||
$stmt->bindValue(':id', $requestId, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':code', $code, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':status', $status, SQLITE3_TEXT);
|
||||
$stmt->bindValue(':amount', $amount, SQLITE3_INTEGER);
|
||||
$stmt->bindValue(':msg', '', SQLITE3_TEXT);
|
||||
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
|
||||
$stmt->execute();
|
||||
|
||||
// 同时标记请求为已完成
|
||||
$stmt2 = $db->prepare("UPDATE cdk_validate_requests SET status = 'done' WHERE request_id = :id");
|
||||
$stmt2->bindValue(':id', $requestId, SQLITE3_TEXT);
|
||||
$stmt2->execute();
|
||||
$db->exec('COMMIT');
|
||||
|
||||
@error_log("[pushCdkValidateResult] request_id={$requestId} code={$code} status={$status} amount={$amount}");
|
||||
success(['request_id' => $requestId]);
|
||||
} catch (\Throwable $e) {
|
||||
$db->exec('ROLLBACK');
|
||||
@error_log('[pushCdkValidateResult] Error: ' . $e->getMessage());
|
||||
error('推送失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user