CDK兑换修复:maxlength扩展+日期搜索+商品分类+同IP折叠+数据库锁修复

This commit is contained in:
ypsdf1
2026-06-20 14:02:41 +08:00
parent 885e70f0f5
commit d7a986894c
6 changed files with 793 additions and 74 deletions
+43 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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());
}
}