diff --git a/admin.php b/admin.php
index a53b978..49921d9 100644
--- a/admin.php
+++ b/admin.php
@@ -3403,6 +3403,7 @@ async function loadRechargeOrders(el) {
+
`;
@@ -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) {
diff --git a/api/pay.php b/api/pay.php
index 0c51b66..6850c33 100644
--- a/api/pay.php
+++ b/api/pay.php
@@ -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';
}
}
diff --git a/api/recharge_orders_api.php b/api/recharge_orders_api.php
index c10a639..dbeb393 100644
--- a/api/recharge_orders_api.php
+++ b/api/recharge_orders_api.php
@@ -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,