修复: 订单不存在问题-平台MySQL直查+同步机制+管理后台手动同步

- queryOrder: 本地订单不存在时直查平台MySQL,找到则自动同步到本地
- handleNotify: 回调找不到本地订单时从平台MySQL补建
- checkPlatformOrderStatus: 超时3s→10s,返回更多字段(trade_no/money/player)
- recharge_orders_api: 新增sync_platform action,管理员手动从平台同步全部已支付订单
- admin.php: 充值对账页新增「从平台同步」按钮
- 解决: 游戏已发钱但前端/后台显示订单不存在的问题

关闭 #1069 #1070
This commit is contained in:
ypsdf1
2026-07-15 21:58:30 +08:00
parent 9c50e58e7d
commit bed190ca8d
3 changed files with 210 additions and 10 deletions
+15
View File
@@ -3403,6 +3403,7 @@ async function loadRechargeOrders(el) {
<option value="200">200条</option>
</select>
<button class="btn btn-blue" onclick="_roReconcile()">🔄 立即对账</button>
<button class="btn btn-yellow" onclick="_roSyncPlatform()" style="margin-left:4px">⬇️ 从平台同步</button>
</div>
<div id="roList"><div class="ro-empty">加载中...</div></div>
</div>`;
@@ -3483,6 +3484,20 @@ async function _roReconcile() {
}
} catch(e) { glassAlert('对账请求失败: '+e.message); }
}
async function _roSyncPlatform() {
if (!await glassConfirm('从支付平台同步所有已支付订单到本地数据库,同时写入充值流水供Java拉取。是否继续?')) return;
try {
const r = await fetch('api/recharge_orders_api.php?action=sync_platform');
const d = await r.json();
if (d.error) { glassAlert('同步失败: '+d.error); return; }
if (d.success) {
glassAlert('同步完成!平台共 '+d.data.platform_total+' 笔,新同步 '+d.data.synced+' 笔,已存在 '+d.data.already_paid+' 笔');
_roLoad();
} else {
glassAlert('同步失败: '+(d.message||'未知错误'));
}
} catch(e) { glassAlert('同步请求失败: '+e.message); }
}
document.addEventListener('keydown', function(e) { if (e.key==='Escape') _roCloseDtl(); });
async function loadCashierManage(el) {
+88 -9
View File
@@ -381,8 +381,36 @@ function handleNotify() {
$stmt->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$row) {
debugLog('[pay notify] 订单不存在: ' . $outTradeNo);
// 订单未知也返回 success,避免平台死循环重试无效通知
debugLog('[pay notify] 本地订单不存在: ' . $outTradeNo . ',尝试从平台同步');
// 本地没有订单 → 尝试从平台MySQL补建
$platformResult = checkPlatformOrderStatus($outTradeNo);
if (is_array($platformResult) && $platformResult['status'] === 'paid') {
$now = time();
$platformPlayer = $platformResult['player'] ?? ($params['param'] ?? 'unknown');
$bonds = PAY_TEST_BONDS;
$ins = $db->prepare("INSERT OR IGNORE INTO pay_orders (out_trade_no, trade_no, player_name, money, bond_amount, status, name, submit_params, created_at, paid_at) VALUES (:no, :tn, :p, :m, :b, 'paid', :n, '', :t, :t)");
$ins->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
$ins->bindValue(':tn', $platformResult['trade_no'] ?? '', SQLITE3_TEXT);
$ins->bindValue(':p', $platformPlayer, SQLITE3_TEXT);
$ins->bindValue(':m', $platformResult['money'] ?? '0.01', SQLITE3_TEXT);
$ins->bindValue(':b', $bonds, SQLITE3_INTEGER);
$ins->bindValue(':n', PAY_TEST_NAME, SQLITE3_TEXT);
$ins->bindValue(':t', $now, SQLITE3_INTEGER);
$ins->execute();
// 写 web_transactions
$txIns = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, operator, reason, detail, status, created_at) VALUES (:p, 'recharge', :a, '支付平台(callback同步)', '在线充值(支付宝)', :d, 'pending', :t)");
$txIns->bindValue(':p', $platformPlayer, SQLITE3_TEXT);
$txIns->bindValue(':a', $bonds, SQLITE3_INTEGER);
$txIns->bindValue(':d', $outTradeNo, SQLITE3_TEXT);
$txIns->bindValue(':t', $now, SQLITE3_INTEGER);
$txIns->execute();
debugLog('[pay notify] callback同步成功', ['out_trade_no' => $outTradeNo, 'player' => $platformPlayer]);
echo 'success';
exit;
}
// 平台也没有 → 返回success避免死循环
echo 'success';
exit;
}
@@ -515,16 +543,63 @@ function queryOrder($token) {
$stmt->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
$stmt->bindValue(':p', $info['player'], SQLITE3_TEXT);
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
// ★ 本地订单不存在 → 直接查平台MySQL,如果平台已支付则同步到本地
if (!$row) {
error('订单不存在');
debugLog('[queryOrder] 本地订单不存在,尝试从平台同步', ['out_trade_no' => $outTradeNo, 'player' => $info['player']]);
$platformResult = checkPlatformOrderStatus($outTradeNo);
if (is_array($platformResult) && $platformResult['status'] === 'paid') {
// 平台已支付 → 创建本地订单并标记paid
$now = time();
$platformPlayer = $platformResult['player'] ?: $info['player'];
$bonds = PAY_TEST_BONDS;
$money = $platformResult['money'] ?? PAY_TEST_MONEY;
$ins = $db->prepare("INSERT OR IGNORE INTO pay_orders (out_trade_no, trade_no, player_name, money, bond_amount, status, name, submit_params, created_at, paid_at) VALUES (:no, :tn, :p, :m, :b, 'paid', :n, '', :t, :t)");
$ins->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
$ins->bindValue(':tn', $platformResult['trade_no'] ?? '', SQLITE3_TEXT);
$ins->bindValue(':p', $platformPlayer, SQLITE3_TEXT);
$ins->bindValue(':m', $money, SQLITE3_TEXT);
$ins->bindValue(':b', $bonds, SQLITE3_INTEGER);
$ins->bindValue(':n', PAY_TEST_NAME, SQLITE3_TEXT);
$ins->bindValue(':t', $now, SQLITE3_INTEGER);
$ins->execute();
// 写 web_transactions
$txCheck = $db->prepare("SELECT id FROM web_transactions WHERE detail = :d AND type = 'recharge' LIMIT 1");
$txCheck->bindValue(':d', $outTradeNo, SQLITE3_TEXT);
$txRow = $txCheck->execute()->fetchArray(SQLITE3_ASSOC);
if (!$txRow) {
$txIns = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, operator, reason, detail, status, created_at) VALUES (:p, 'recharge', :a, '支付平台(查询同步)', '在线充值(支付宝)', :d, 'pending', :t)");
$txIns->bindValue(':p', $platformPlayer, SQLITE3_TEXT);
$txIns->bindValue(':a', $bonds, SQLITE3_INTEGER);
$txIns->bindValue(':d', $outTradeNo, SQLITE3_TEXT);
$txIns->bindValue(':t', $now, SQLITE3_INTEGER);
$txIns->execute();
}
debugLog('[queryOrder] 平台同步成功', ['out_trade_no' => $outTradeNo, 'player' => $platformPlayer]);
success([
'out_trade_no' => $outTradeNo,
'status' => 'paid',
'money' => $money,
'bonds' => $bonds,
'paid_at' => $now,
'created_at' => $now,
], '已支付,游戏内债券发放中(若未到账请稍候或联系管理员)');
return;
}
// 平台也没有 → 真的不存在
debugLog('[queryOrder] 平台也无此订单', ['out_trade_no' => $outTradeNo]);
error('订单不存在,请确认订单号是否正确');
}
// ★ 2026-07-15 核心修复:如果本地还是 created,直接查平台MySQL
// 不依赖 poller 补单,确保支付成功后立即返回 paid 状态
$status = $row['status'];
if ($status !== 'paid') {
$platformStatus = checkPlatformOrderStatus($outTradeNo);
if ($platformStatus === 'paid') {
$platformResult = checkPlatformOrderStatus($outTradeNo);
$platformPaid = is_array($platformResult) && $platformResult['status'] === 'paid';
if ($platformPaid) {
// 平台已支付,立即更新本地状态
$now = time();
$upd = $db->prepare("UPDATE pay_orders SET status='paid', paid_at=:t WHERE out_trade_no=:no AND status != 'paid'");
@@ -585,15 +660,19 @@ function checkPlatformOrderStatus($outTradeNo) {
"mysql:host=localhost;dbname=caihong;charset=utf8mb4",
'kH3C3LLinNwYdTF5',
'sRhsdxrpHBhmSsp8',
[PDO::ATTR_TIMEOUT => 3, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
[PDO::ATTR_TIMEOUT => 10, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$stmt = $pdo->prepare("SELECT status FROM pay_order WHERE out_trade_no = ? AND status IN (1, 2) LIMIT 1");
$stmt = $pdo->prepare("SELECT status, trade_no, money, param, addtime FROM pay_order WHERE out_trade_no = ? LIMIT 1");
$stmt->execute([$outTradeNo]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? 'paid' : 'not_found';
if (!$row) return 'not_found';
if (in_array($row['status'], [1, 2])) {
return ['status' => 'paid', 'trade_no' => $row['trade_no'], 'money' => $row['money'], 'player' => $row['param'] ?? ''];
}
return 'not_paid';
} catch (\Throwable $e) {
debugLog('[checkPlatformOrderStatus] 查询失败', ['error' => $e->getMessage(), 'out_trade_no' => $outTradeNo]);
return 'not_found';
return 'error';
}
}
+107 -1
View File
@@ -27,6 +27,9 @@ switch ($action) {
case 'reconcile':
handleReconcile();
break;
case 'sync_platform':
handleSyncFromPlatform();
break;
default:
error('未知操作');
}
@@ -147,8 +150,111 @@ function handleReconcile() {
}
/**
* 确保 pay_orders 表存在(从 pay.php 复制)
* 从平台MySQL同步所有已支付订单到本地
*/
function handleSyncFromPlatform() {
$db = getDB();
ensurePayOrdersTable($db);
ensureWebTransactionsTable($db);
try {
$pdo = new PDO(
"mysql:host=localhost;dbname=caihong;charset=utf8mb4",
'kH3C3LLinNwYdTF5',
'sRhsdxrpHBhmSsp8',
[PDO::ATTR_TIMEOUT => 10, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// 查所有已支付订单
$stmt = $pdo->query("SELECT out_trade_no, trade_no, uid, money, status, param, addtime FROM pay_order WHERE status IN (1, 2) ORDER BY addtime DESC LIMIT 200");
$platformOrders = $stmt->fetchAll(PDO::FETCH_ASSOC);
$synced = 0;
$alreadyPaid = 0;
$errors = [];
$now = time();
foreach ($platformOrders as $po) {
$outTradeNo = $po['out_trade_no'];
$player = $po['param'] ?? 'unknown';
$tradeNo = $po['trade_no'] ?? '';
$money = (string)$po['money'];
$bonds = ($money >= '1.0') ? (int)($money * 10) : max(1, (int)($money * 100));
// 查本地是否已有
$check = $db->prepare("SELECT id, status FROM pay_orders WHERE out_trade_no = :no");
$check->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
$localRow = $check->execute()->fetchArray(SQLITE3_ASSOC);
if ($localRow && $localRow['status'] === 'paid') {
$alreadyPaid++;
continue;
}
if ($localRow) {
// 已有记录但未标记paid → 更新
$upd = $db->prepare("UPDATE pay_orders SET status='paid', trade_no=:tn, paid_at=:t WHERE out_trade_no=:no AND status != 'paid'");
$upd->bindValue(':tn', $tradeNo, SQLITE3_TEXT);
$upd->bindValue(':t', $now, SQLITE3_INTEGER);
$upd->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
$upd->execute();
} else {
// 本地无记录 → 创建
$ins = $db->prepare("INSERT OR IGNORE INTO pay_orders (out_trade_no, trade_no, player_name, money, bond_amount, status, name, submit_params, created_at, paid_at) VALUES (:no, :tn, :p, :m, :b, 'paid', :n, '', :t, :t)");
$ins->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
$ins->bindValue(':tn', $tradeNo, SQLITE3_TEXT);
$ins->bindValue(':p', $player, SQLITE3_TEXT);
$ins->bindValue(':m', $money, SQLITE3_TEXT);
$ins->bindValue(':b', $bonds, SQLITE3_INTEGER);
$ins->bindValue(':n', '债券充值', SQLITE3_TEXT);
$ins->bindValue(':t', $now, SQLITE3_INTEGER);
$ins->execute();
}
// 写 web_transactions(幂等)
$txCheck = $db->prepare("SELECT id FROM web_transactions WHERE detail = :d AND type = 'recharge' LIMIT 1");
$txCheck->bindValue(':d', $outTradeNo, SQLITE3_TEXT);
$txRow = $txCheck->execute()->fetchArray(SQLITE3_ASSOC);
if (!$txRow) {
$txIns = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, operator, reason, detail, status, created_at) VALUES (:p, 'recharge', :a, '管理员同步', '在线充值(支付宝)', :d, 'pending', :t)");
$txIns->bindValue(':p', $player, SQLITE3_TEXT);
$txIns->bindValue(':a', $bonds, SQLITE3_INTEGER);
$txIns->bindValue(':d', $outTradeNo, SQLITE3_TEXT);
$txIns->bindValue(':t', $now, SQLITE3_INTEGER);
$txIns->execute();
}
$synced++;
}
success([
'platform_total' => count($platformOrders),
'synced' => $synced,
'already_paid' => $alreadyPaid,
], "同步完成:平台{$synced}笔已同步,{$alreadyPaid}笔已存在");
} catch (\Throwable $e) {
error('同步失败: ' . $e->getMessage());
}
}
/**
* 确保 web_transactions 表存在
*/
function ensureWebTransactionsTable($db) {
$db->exec("CREATE TABLE IF NOT EXISTS web_transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_name TEXT NOT NULL,
type TEXT,
amount INTEGER DEFAULT 0,
operator TEXT,
reason TEXT,
detail TEXT,
status TEXT DEFAULT 'pending',
created_at INTEGER
)");
@$db->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_web_tx_detail_type ON web_transactions(detail, type)");
}
function ensurePayOrdersTable($db) {
$db->exec("CREATE TABLE IF NOT EXISTS pay_orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,