CDK兑换修复:maxlength扩展+日期搜索+商品分类+同IP折叠+数据库锁修复
This commit is contained in:
@@ -113,7 +113,7 @@ if ('serviceWorker' in navigator) {
|
||||
}
|
||||
|
||||
const A = 'api/admin.php';
|
||||
const _BUILD_TS = 1750220000; // 版本号,用于缓存失效
|
||||
const _BUILD_TS = 1781928829; // 版本号,用于缓存失效
|
||||
console.log('[INIT] Admin panel loaded, build:', _BUILD_TS);
|
||||
|
||||
// ★ 自检:验证新代码是否加载
|
||||
@@ -232,15 +232,45 @@ async function loadDashboard(el) {
|
||||
let onlineHtml = '';
|
||||
if (onlineR.success && onlineR.data && onlineR.data.length > 0) {
|
||||
const players = onlineR.data;
|
||||
// 同IP段折叠:按/24子网分组
|
||||
const subnetGroups = {};
|
||||
const subnetOrder = [];
|
||||
players.forEach(p => {
|
||||
const ip = p.ip_address || '-';
|
||||
let subnet = ip;
|
||||
if (ip !== '-') {
|
||||
const parts = ip.split('.');
|
||||
if (parts.length === 4) {
|
||||
subnet = parts[0] + '.' + parts[1] + '.' + parts[2];
|
||||
}
|
||||
}
|
||||
if (!subnetGroups[subnet]) {
|
||||
subnetGroups[subnet] = [];
|
||||
subnetOrder.push(subnet);
|
||||
}
|
||||
subnetGroups[subnet].push(p);
|
||||
});
|
||||
|
||||
// 构建显示列表:每组只显示第一个,后面标注折叠数量
|
||||
const displayRows = [];
|
||||
subnetOrder.forEach(subnet => {
|
||||
const group = subnetGroups[subnet];
|
||||
const first = group[0];
|
||||
const hiddenCount = group.length - 1;
|
||||
displayRows.push({player: first, hiddenCount});
|
||||
});
|
||||
|
||||
onlineHtml = `
|
||||
<div class="card">
|
||||
<h2>实时在线玩家 <span style="color:var(--dim);font-size:12px">(${players.length}人)</span></h2>
|
||||
<table class="table">
|
||||
<tr><th>玩家名</th><th>登录时间</th><th>在线时长</th></tr>
|
||||
${players.map(p => {
|
||||
<tr><th>玩家名</th><th>IP地址</th><th>登录时间</th><th>在线时长</th></tr>
|
||||
${displayRows.map(({player: p, hiddenCount}) => {
|
||||
const loginTime = p.login_time ? new Date(p.login_time*1000).toLocaleString() : '-';
|
||||
const mins = Math.floor((Date.now()/1000 - p.login_time)/60);
|
||||
return `<tr><td class="player-online">🟢 ${p.player_name}</td><td>${loginTime}</td><td>${mins}分钟</td></tr>`;
|
||||
const ip = p.ip_address || '-';
|
||||
const suffix = hiddenCount > 0 ? ` <span style="color:var(--yellow);font-size:12px">(折叠同ip段玩家${hiddenCount}名)</span>` : '';
|
||||
return `<tr><td class="player-online">🟢 ${p.player_name}${suffix}</td><td style="font-size:12px;font-family:monospace">${ip}</td><td>${loginTime}</td><td>${mins}分钟</td></tr>`;
|
||||
}).join('')}
|
||||
</table>
|
||||
</div>`;
|
||||
@@ -306,21 +336,32 @@ async function doQueryBond() {
|
||||
}
|
||||
|
||||
// ===== 商品管理 =====
|
||||
let _shopItems = [];
|
||||
let _shopCategories = [];
|
||||
let _currentShopCat = '';
|
||||
|
||||
async function loadShop(el) {
|
||||
const r = await jsonApi('shop.php?action=list');
|
||||
const items = r.data || [];
|
||||
_shopItems = r.data || [];
|
||||
_shopCategories = [...new Set(_shopItems.map(i => i.category || '默认'))];
|
||||
_currentShopCat = _shopCategories[0] || '';
|
||||
|
||||
let catTabs = '';
|
||||
if (_shopCategories.length > 0) {
|
||||
catTabs = '<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:16px">';
|
||||
_shopCategories.forEach((cat, idx) => {
|
||||
const active = idx === 0 ? 'color:#fff;background:var(--accent);border-color:var(--accent)' : '';
|
||||
const catCount = _shopItems.filter(i => (i.category || '默认') === cat).length;
|
||||
catTabs += `<div onclick="switchShopCat('${cat.replace(/'/g,"\\'")}')" style="padding:6px 14px;border:1px solid var(--border);border-radius:16px;cursor:pointer;font-size:13px;transition:all 0.2s;${active}" class="shop-cat-tab" data-cat="${cat}">${cat} <span style="opacity:0.7;font-size:11px">(${catCount})</span></div>`;
|
||||
});
|
||||
catTabs += '</div>';
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="card">
|
||||
<h2>商品管理 <button class="btn btn-blue" style="float:right" onclick="showAddShop()">+ 添加商品</button></h2>
|
||||
<table class="table">
|
||||
<tr><th>ID</th><th>名称</th><th>分类</th><th>购入价</th><th>库存</th><th>销量</th><th>操作</th></tr>
|
||||
${items.map(i=>`<tr>
|
||||
<td>${i.id}</td><td>${i.display_name}</td><td>${i.category}</td>
|
||||
<td>${i.buy_price}</td><td>${i.stock==-1?'∞':i.stock}</td><td>${i.total_sales}</td>
|
||||
<td><button class="btn btn-yellow" onclick="editStock('${i.id}',${i.stock})">改库存</button>
|
||||
<button class="btn btn-red" onclick="removeShop('${i.id}')">删除</button></td>
|
||||
</tr>`).join('')}
|
||||
</table>
|
||||
${catTabs}
|
||||
<div id="shopAdminContent"></div>
|
||||
</div>
|
||||
<div class="card" id="addShopForm" style="display:none">
|
||||
<h2>添加商品</h2>
|
||||
@@ -332,6 +373,37 @@ async function loadShop(el) {
|
||||
<div class="form-row"><label>库存</label><input id="asStock" type="number" value="-1" placeholder="-1无限"></div>
|
||||
<button class="btn btn-green" onclick="doAddShop()">确认添加</button>
|
||||
</div>`;
|
||||
renderShopAdminContent();
|
||||
}
|
||||
|
||||
function switchShopCat(cat) {
|
||||
_currentShopCat = cat;
|
||||
document.querySelectorAll('.shop-cat-tab').forEach(tab => {
|
||||
const isActive = tab.dataset.cat === cat;
|
||||
tab.style.background = isActive ? 'var(--accent)' : '';
|
||||
tab.style.color = isActive ? '#fff' : '';
|
||||
tab.style.borderColor = isActive ? 'var(--accent)' : 'var(--border)';
|
||||
});
|
||||
renderShopAdminContent();
|
||||
}
|
||||
|
||||
function renderShopAdminContent() {
|
||||
const div = document.getElementById('shopAdminContent');
|
||||
if (!div) return;
|
||||
const items = _shopItems.filter(i => (i.category || '默认') === _currentShopCat);
|
||||
if (items.length === 0) {
|
||||
div.innerHTML = '<div class="empty" style="padding:20px">该分类暂无商品</div>';
|
||||
return;
|
||||
}
|
||||
div.innerHTML = `<table class="table">
|
||||
<tr><th>ID</th><th>名称</th><th>分类</th><th>购入价</th><th>库存</th><th>销量</th><th>操作</th></tr>
|
||||
${items.map(i=>`<tr>
|
||||
<td>${i.id}</td><td>${i.display_name}</td><td>${i.category || '默认'}</td>
|
||||
<td>${i.buy_price}</td><td>${i.stock==-1?'∞':i.stock}</td><td>${i.total_sales}</td>
|
||||
<td><button class="btn btn-yellow" onclick="editStock('${i.id}',${i.stock})">改库存</button>
|
||||
<button class="btn btn-red" onclick="removeShop('${i.id}')">删除</button></td>
|
||||
</tr>`).join('')}
|
||||
</table>`;
|
||||
}
|
||||
|
||||
function showAddShop() { document.getElementById('addShopForm').style.display='block'; }
|
||||
@@ -374,7 +446,7 @@ async function loadCDK(el) {
|
||||
<div style="display:flex;gap:12px;margin-bottom:12px">
|
||||
<div class="card" style="flex:1;margin:0">
|
||||
<h2>生成CDK</h2>
|
||||
<div class="form-row"><label>金额</label><input id="cAmount" type="number" value="100"></div>
|
||||
<div class="form-row"><label>金额</label><input id="cAmount" type="text" value="100" placeholder="固定金额或区间(如100-200)"></div>
|
||||
<div class="form-row"><label>数量</label><input id="cCount" type="number" value="10" min="1" max="100"></div>
|
||||
<button class="btn btn-green" onclick="doBatchCDK()">批量生成</button>
|
||||
</div>
|
||||
@@ -393,10 +465,34 @@ async function loadCDK(el) {
|
||||
}
|
||||
|
||||
async function doBatchCDK() {
|
||||
const amount = parseInt(document.getElementById('cAmount').value);
|
||||
const amountStr = document.getElementById('cAmount').value.trim();
|
||||
const count = parseInt(document.getElementById('cCount').value);
|
||||
if (!amount||!count) { toast('请填写完整','err'); return; }
|
||||
const r = await postApi('cdk_batch', {amount, count});
|
||||
if (!amountStr||!count) { toast('请填写完整','err'); return; }
|
||||
|
||||
// 解析金额:支持固定金额或区间
|
||||
let amountData = {};
|
||||
const separators = [',', ',', '-', '-', '/', '/', '~', '~'];
|
||||
let foundSep = null;
|
||||
for (const sep of separators) {
|
||||
if (amountStr.includes(sep)) {
|
||||
foundSep = sep;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (foundSep) {
|
||||
const parts = amountStr.split(foundSep).map(s => parseInt(s.trim())).filter(n => !isNaN(n));
|
||||
if (parts.length === 2 && parts[0] <= parts[1]) {
|
||||
amountData = {min: parts[0], max: parts[1]};
|
||||
} else {
|
||||
toast('区间格式错误,如100-200','err'); return;
|
||||
}
|
||||
} else {
|
||||
const amount = parseInt(amountStr);
|
||||
if (isNaN(amount) || amount <= 0) { toast('金额必须为正整数','err'); return; }
|
||||
amountData = {amount: amount};
|
||||
}
|
||||
|
||||
const r = await postApi('cdk_batch', {...amountData, count});
|
||||
if (r.success) {
|
||||
toast('生成了'+count+'个CDK','ok');
|
||||
loadCDK(document.getElementById('C'));
|
||||
@@ -718,8 +814,34 @@ function lazyLoadUsersPage(page, limit, search, onlineSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 追加用户行
|
||||
// ★ 判断是否为日期搜索:日期搜索时折叠同IP段玩家
|
||||
const s = (search || '').toLowerCase();
|
||||
const isDateSearch = /今天|昨日|today|yesterday|今天|当日|前一日/.test(s) || /^\d{4}[-\/]\d{1,2}[-\/]\d{1,2}$/.test(s);
|
||||
|
||||
// 追加用户行(日期搜索时按/24子网折叠)
|
||||
let displayUsers = users;
|
||||
if (isDateSearch && users.length > 0) {
|
||||
// 按/24子网分组
|
||||
const subnetMap = {};
|
||||
const subnetOrder = [];
|
||||
users.forEach(u => {
|
||||
const ip = u.ip_address || '-';
|
||||
let subnet = ip;
|
||||
if (ip !== '-') {
|
||||
const parts = ip.split('.');
|
||||
if (parts.length === 4) subnet = parts[0] + '.' + parts[1] + '.' + parts[2];
|
||||
}
|
||||
if (!subnetMap[subnet]) { subnetMap[subnet] = []; subnetOrder.push(subnet); }
|
||||
subnetMap[subnet].push(u);
|
||||
});
|
||||
displayUsers = [];
|
||||
subnetOrder.forEach(sub => {
|
||||
const group = subnetMap[sub];
|
||||
displayUsers.push({...group[0], _foldCount: group.length - 1});
|
||||
});
|
||||
}
|
||||
|
||||
displayUsers.forEach(u => {
|
||||
const isOnline = onlineSet && onlineSet.has((u.player_name || '').toLowerCase());
|
||||
const playerNameClass = isOnline ? 'player-online' : '';
|
||||
const regTime = u.register_time ? new Date(u.register_time * 1000).toLocaleString() : '-';
|
||||
@@ -727,11 +849,12 @@ function lazyLoadUsersPage(page, limit, search, onlineSet) {
|
||||
const hours = Math.floor((u.total_online_time || 0) / 3600);
|
||||
const ip = u.ip_address || '-';
|
||||
const ipLoc = u.ip_location || '-';
|
||||
const foldSuffix = u._foldCount > 0 ? ` <span style="color:var(--yellow);font-size:12px">(同ip段${u._foldCount}名)</span>` : '';
|
||||
|
||||
const tr = document.createElement('tr');
|
||||
tr.setAttribute('data-name', (u.player_name || '').toLowerCase());
|
||||
tr.innerHTML = `
|
||||
<td class="${playerNameClass}">${isOnline ? '🟢 ' : ''}${u.player_name}</td>
|
||||
<td class="${playerNameClass}">${isOnline ? '🟢 ' : ''}${u.player_name}${foldSuffix}</td>
|
||||
<td>${regTime}</td>
|
||||
<td>${loginTime}</td>
|
||||
<td>${u.points || 0}</td>
|
||||
|
||||
+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);
|
||||
|
||||
+124
-10
@@ -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)
|
||||
@@ -89,9 +89,13 @@ function cdkExchange($token) {
|
||||
$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']);
|
||||
@@ -125,14 +129,22 @@ function cdkExchange($token) {
|
||||
|
||||
// 执行兑换
|
||||
$now = time();
|
||||
$isRemote = !empty($cdk['_remote']);
|
||||
|
||||
// 标记CDK已使用
|
||||
// ★ 全部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");
|
||||
@@ -148,28 +160,35 @@ function cdkExchange($token) {
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
// ★ 所有DB操作包装在事务中,防止database is locked
|
||||
$db->exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
// 记录流水
|
||||
// 记录流水 + 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], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
|
||||
$stmt->bindValue(':detail', json_encode(['code' => $code, 'remote' => false], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT);
|
||||
$stmt->bindValue(':time', $now, SQLITE3_INTEGER);
|
||||
$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();
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1042,7 +1042,7 @@ if ($currentVersion !== $BUILD_VERSION) {
|
||||
<p style="color:var(--dim);font-size:13px;margin-bottom:12px">输入CDK兑换码,获得债券奖励</p>
|
||||
${IS_PREVIEW ? '<div class="preview-badge" style="margin-bottom:12px">预览模式 - 兑换不会生效</div>' : ''}
|
||||
<div class="cdk-input" style="flex-direction:column;gap:8px">
|
||||
<input type="text" id="cdkCode" placeholder="请输入CDK兑换码..." maxlength="20">
|
||||
<input type="text" id="cdkCode" placeholder="请输入CDK兑换码..." maxlength="64">
|
||||
<input type="password" id="cdkPassword" 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">
|
||||
<button class="btn btn-green" onclick="doCDKExchange()">兑换</button>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -250,7 +250,7 @@ if ($action === 'status') {
|
||||
} elseif ($action === 'upload') {
|
||||
// 通过 POST 上传文件
|
||||
$target = $_POST['target'] ?? '';
|
||||
$allowed = ['api/admin.php', 'api/sync.php', 'core.php', 'config.php'];
|
||||
$allowed = ['api/admin.php', 'api/sync.php', 'api/cdk.php', 'core.php', 'config.php'];
|
||||
|
||||
if (!in_array($target, $allowed)) {
|
||||
echo "ERROR: Target not allowed. Use: " . implode(', ', $allowed) . "\n";
|
||||
|
||||
Reference in New Issue
Block a user