Files
Sdf1_login/api/cdk.php
T

492 lines
17 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
/**
* 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);
}
// ★ 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;
}
// ===== 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();
// ★ 第32批拉黑拦截:本地兑换前先查拉黑名单
// (web_player_blacklist 由 Java BlacklistManager 经 sync_blacklist 同步;
// 本地CDK存在也一律拒绝,与 Java CDKManager.redeem 的检查口径一致)
$blackReason = playerWebBlacklistReason($player);
if ($blackReason !== null) {
@error_log("[cdk] 拉黑拦截兑换: player={$player}");
error('你已被拉黑:' . $blackReason);
}
// 查询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();
}
// ★ PHP只负责记录交易,不更新bond_cache(Java端负责加债券)
// 记录流水:远程CDK标记completed(Java在pullWebCdkRequestsAndValidate中已直接加债券)
// 本地CDK标记pending(Java通过pullPendingTransactions拉取后处理)
$txStatus = $isRemote ? 'completed' : 'pending';
$stmt = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, reason, detail, status, created_at) VALUES (:player, 'cdk_redeem', :amount, :reason, :detail, :status, :time)");
$stmt->bindValue(':player', $player, SQLITE3_TEXT);
$stmt->bindValue(':amount', $cdk['amount'], SQLITE3_INTEGER);
$stmt->bindValue(':reason', $isRemote ? "CDK远程兑换: {$code}" : "CDK兑换: {$code}", SQLITE3_TEXT);
$stmt->bindValue(':detail', json_encode(['code' => $code, 'remote' => $isRemote], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
$stmt->bindValue(':status', $txStatus, SQLITE3_TEXT);
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
$stmt->execute();
// 只有本地CDK需要sync_requests触发Java立即同步
if (!$isRemote) {
$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();
}
$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' => $currentBalance
], 'CDK兑换成功,债券将由Java端处理');
}
// ===== 创建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']}");
// ★ 第32批:Java端拉黑拦截信号 → 直接提示被拉黑
// (若当 null 返回,上层会误报「CDK不存在」,掩盖真实原因)
if ($result['status'] === 'blacklisted') {
$blReason = playerWebBlacklistReason($player);
@error_log("[cdk] 远程验证 blacklisted: player={$player}");
error('你已被拉黑:' . ($blReason !== null ? $blReason : '违反服务器规则'));
}
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;
}