From 7f513b9fa10841898c9167f4f2bf5ee2ebbd43bf Mon Sep 17 00:00:00 2001 From: ypsdf1 Date: Fri, 2 Oct 2026 21:43:23 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=85=E5=80=BC=E8=A1=A5=E5=8D=95=E5=8F=AA?= =?UTF-8?q?=E8=AE=A4=E6=9C=AC=E6=96=B9RE=E8=AE=A2=E5=8D=95=EF=BC=9A?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E5=A4=9A=E5=95=86=E6=88=B7=E5=BA=93=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=95=B4=E8=A1=A8=E6=8B=89=E5=8F=96=EF=BC=8C=E6=9D=9C?= =?UTF-8?q?=E7=BB=9D=E5=9E=83=E5=9C=BE=E5=85=85=E5=80=BC=E5=8D=95=E5=88=B7?= =?UTF-8?q?=E5=B1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/pay.php | 6 + api/pay_poller.php | 215 ++++++++++++++++++++++++++++++++++++ api/poller_online.php | 26 +++-- api/recharge_orders_api.php | 15 ++- 4 files changed, 252 insertions(+), 10 deletions(-) create mode 100644 api/pay_poller.php diff --git a/api/pay.php b/api/pay.php index 9e6e1c7..afe5e05 100644 --- a/api/pay.php +++ b/api/pay.php @@ -631,6 +631,12 @@ function queryOrder($token) { // ★ 本地订单不存在 → 双路查询(MySQL + 官方API),如果平台已支付则同步到本地 if (!$row) { + // ★ 2026-10-02 归属校验:本方订单号只可能是 createOrder 生成的 RE 前缀。 + // 不是 RE 开头 = 平台其他商户/项目的订单 → 一概不查平台、不建本地单、不写流水。 + if (strncmp((string)$outTradeNo, 'RE', 2) !== 0) { + debugLog('[queryOrder] 非本方订单号,拒绝查询平台', ['out_trade_no' => $outTradeNo, 'player' => $info['player']]); + error('订单不存在,请确认订单号是否正确'); + } debugLog('[queryOrder] 本地订单不存在,尝试双路查询同步', ['out_trade_no' => $outTradeNo, 'player' => $info['player']]); // 路径1:平台MySQL直查 diff --git a/api/pay_poller.php b/api/pay_poller.php new file mode 100644 index 0000000..6b078ee --- /dev/null +++ b/api/pay_poller.php @@ -0,0 +1,215 @@ + 0) { ob_end_clean(); } + +// ===== 配置 ===== +$PLATFORM_DB_HOST = 'localhost'; +$PLATFORM_DB_NAME = 'caihong'; +$PLATFORM_DB_USER = 'kH3C3LLinNwYdTF5'; +$PLATFORM_DB_PASS = 'sRhsdxrpHBhmSsp8'; +$PLATFORM_DB_PREFIX = 'pay_'; + +// SQLite 数据库路径(与 core.php 一致) +$SQLITE_DB_PATH = __DIR__ . '/../db/web.db'; +$MD5_KEY = 'Fo01ul2MrO8zRu0ZoUpP2U1kp0uu7rXk'; + +function debugLog($msg, $ctx = []) { + $logFile = __DIR__ . '/../db/debug.log'; + $ts = date('Y-m-d H:i:s'); + $entry = "[$ts] $msg"; + if ($ctx) $entry .= ' | Context: ' . json_encode($ctx, JSON_UNESCAPED_UNICODE); + $entry .= "\n"; + @file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX); +} + +function getSQLite() { + static $db = null; + if ($db === null) { + $db = new SQLite3($GLOBALS['SQLITE_DB_PATH']); + $db->enableExceptions(true); + $db->exec('PRAGMA journal_mode=WAL'); + $db->exec('PRAGMA busy_timeout=5000'); + // 确保表存在 + $db->exec("CREATE TABLE IF NOT EXISTS pay_orders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + out_trade_no TEXT UNIQUE, + trade_no TEXT, + player_name TEXT NOT NULL, + tier_id INTEGER DEFAULT 0, + money TEXT, + bond_amount INTEGER DEFAULT 0, + status TEXT DEFAULT 'created', + name TEXT, + platform_sign TEXT, + submit_params TEXT, + created_at INTEGER, + paid_at INTEGER + )"); + $db->exec("CREATE TABLE IF NOT EXISTS web_transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + player_name TEXT, + type TEXT, + amount INTEGER, + operator TEXT, + reason TEXT, + detail TEXT, + status TEXT DEFAULT 'pending', + created_at INTEGER + )"); + } + return $db; +} + +function getPlatformDB() { + static $pdo = null; + if ($pdo === null) { + $dsn = "mysql:host={$GLOBALS['PLATFORM_DB_HOST']};dbname={$GLOBALS['PLATFORM_DB_NAME']};charset=utf8mb4"; + $pdo = new PDO($dsn, $GLOBALS['PLATFORM_DB_USER'], $GLOBALS['PLATFORM_DB_PASS'], [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_TIMEOUT => 5, + ]); + } + return $pdo; +} + +function pollPaidOrders() { + $sqlite = getSQLite(); + + try { + $pdo = getPlatformDB(); + } catch (\Throwable $e) { + debugLog('[pay_poller] 平台数据库连接失败: ' . $e->getMessage()); + echo json_encode(['error' => 'platform_db_connect_failed', 'detail' => $e->getMessage()]); + return; + } + + // ★ 2026-10-02 只查后端自己的订单:平台 MySQL 是多商户共用库,不加过滤整表拉 + // 会把其他商户/项目的单(纯数字单号)也当成本地充值单,凭空造出玩家=unknown + // 的垃圾订单。本方单号固定 RE 前缀(pay.php createOrder 生成),平台侧据此过滤, + // PHP 侧再兜底校验;平台其他订单一概不读不写(含不改它的 notify)。 + // 仍不过滤 notify:平台可能标记 notify=1 但本地 DB 未更新(回调时 DB 写入异常)。 + $prefix = $GLOBALS['PLATFORM_DB_PREFIX']; + $stmt = $pdo->query("SELECT out_trade_no, trade_no, uid, money, status, notify, param, version FROM {$prefix}order WHERE out_trade_no LIKE 'RE%' AND status IN (1, 2) ORDER BY addtime ASC LIMIT 50"); + $allOrders = $stmt->fetchAll(PDO::FETCH_ASSOC); + + if (empty($allOrders)) { + echo json_encode(['result' => 'no_paid_orders_on_platform']); + return; + } + + // 只保留本方订单(RE 前缀兜底校验),再过滤掉本地已处理的 + $orders = []; + $foreign = 0; + foreach ($allOrders as $o) { + $outNo = (string)$o['out_trade_no']; + if (strncmp($outNo, 'RE', 2) !== 0) { + $foreign++; // 非本方订单:不建单、不写流水、不改它的 notify + continue; + } + $check = $sqlite->prepare("SELECT status FROM pay_orders WHERE out_trade_no = :no"); + $check->bindValue(':no', $outNo, SQLITE3_TEXT); + $row = $check->execute()->fetchArray(SQLITE3_ASSOC); + if (!$row || $row['status'] !== 'paid') { + $orders[] = $o; + } + } + + if (empty($orders)) { + echo json_encode(['result' => 'no_pending_orders', 'platform_paid_count' => count($allOrders), 'foreign_skipped' => $foreign]); + return; + } + + // 获取 uid→player_name 映射 + $uidMap = []; + foreach ($orders as $o) { + $uidMap[$o['uid']] = $o['param'] ?? 'unknown'; + } + + $processed = 0; + $skipped = 0; + + foreach ($orders as $order) { + $outTradeNo = $order['out_trade_no']; + $tradeNo = $order['trade_no']; + $playerName = $order['param'] ?? 'unknown'; + $money = (string)$order['money']; + $bonds = ((float)$money >= 1.0) ? (int)((float)$money * 10) : max(1, (int)((float)$money * 100)); + + // 检查本地是否已处理(双重保险) + $check = $sqlite->prepare("SELECT status FROM pay_orders WHERE out_trade_no = :no"); + $check->bindValue(':no', $outTradeNo, SQLITE3_TEXT); + $row = $check->execute()->fetchArray(SQLITE3_ASSOC); + + if ($row && $row['status'] === 'paid') { + $skipped++; + markNotified($pdo, $tradeNo); + continue; + } + + debugLog('[pay_poller] 处理订单', ['out_trade_no' => $outTradeNo, 'player' => $playerName, 'money' => $money, 'bonds' => $bonds, 'platform_notify' => $order['notify']]); + + // 插入或更新本地订单 + if (!$row) { + $ins = $sqlite->prepare("INSERT OR IGNORE INTO pay_orders (out_trade_no, trade_no, player_name, money, bond_amount, status, created_at, paid_at) VALUES (:no, :tn, :p, :m, :b, 'paid', :t, :t)"); + $ins->bindValue(':no', $outTradeNo, SQLITE3_TEXT); + $ins->bindValue(':tn', $tradeNo, SQLITE3_TEXT); + $ins->bindValue(':p', $playerName, SQLITE3_TEXT); + $ins->bindValue(':m', $money, SQLITE3_TEXT); + $ins->bindValue(':b', $bonds, SQLITE3_INTEGER); + $ins->bindValue(':t', time(), SQLITE3_INTEGER); + $ins->execute(); + } else { + $upd = $sqlite->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', time(), SQLITE3_INTEGER); + $upd->bindValue(':no', $outTradeNo, SQLITE3_TEXT); + $upd->execute(); + } + + // 写 web_transactions(幂等) + $txCheck = $sqlite->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 = $sqlite->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', $playerName, SQLITE3_TEXT); + $txIns->bindValue(':a', $bonds, SQLITE3_INTEGER); + $txIns->bindValue(':d', $outTradeNo, SQLITE3_TEXT); + $txIns->bindValue(':t', time(), SQLITE3_INTEGER); + $txIns->execute(); + + debugLog('[pay_poller] 充值交易已写入', ['player' => $playerName, 'bonds' => $bonds, 'out_trade_no' => $outTradeNo, 'money' => $money]); + } + + markNotified($pdo, $tradeNo); + $processed++; + } + + echo json_encode(['result' => 'ok', 'processed' => $processed, 'skipped' => $skipped, 'foreign_skipped' => $foreign, 'total' => count($orders)]); +} + +function markNotified($pdo, $tradeNo) { + try { + $prefix = $GLOBALS['PLATFORM_DB_PREFIX']; + $stmt = $pdo->prepare("UPDATE {$prefix}order SET notify = 1 WHERE trade_no = ? AND notify = 0"); + $stmt->execute([$tradeNo]); + } catch (\Throwable $e) { + debugLog('[pay_poller] 标记notify失败: ' . $e->getMessage(), ['trade_no' => $tradeNo]); + } +} + +// ===== 路由 ===== +header('Content-Type: application/json; charset=utf-8'); +pollPaidOrders(); +?> \ No newline at end of file diff --git a/api/poller_online.php b/api/poller_online.php index fe6b335..3787b14 100644 --- a/api/poller_online.php +++ b/api/poller_online.php @@ -198,11 +198,16 @@ function pollPaidOrders() { return; } - // 查所有已支付订单 (status=1 或 status=2)。不过滤 notify: - // 平台可能已标记 notify=1 但本地的 DB 未更新(HTTP 回调被 CF WAF 拦截), - // 必须靠本地 SQLite 的 status='created' 来判断是否需要补单。 + // ★ 2026-10-02 只查后端自己的订单:平台 MySQL 是多商户共用库,`pay_order` 里 + // 绝大多数是其他商户/项目的单(纯数字单号)。以前不加过滤整表拉,平台每来一单 + // 就被我们"捡走"、凭空写进本地 pay_orders + web_transactions,产生玩家=unknown + // 的垃圾充值单(Java 每次拉交易都刷屏处理它们),还顺手把别人的订单 notify + // 改成 1 —— 纯属多管闲事。本方单号由 pay.php createOrder 生成、固定 RE 前缀, + // 平台侧据此过滤,PHP 侧再兜底校验一次;平台其他订单一概不读不写。 + // 仍不过滤 notify:平台可能已标记 notify=1 但本地 DB 未更新(HTTP 回调被 + // CF WAF 拦截),是否补单以本地 pay_orders 状态为准。 $prefix = $PLATFORM_DB_PREFIX; - $stmt = $pdo->query("SELECT out_trade_no, trade_no, uid, money, status, notify, param, version, addtime FROM `{$prefix}order` WHERE status IN (1, 2) ORDER BY addtime ASC LIMIT 50"); + $stmt = $pdo->query("SELECT out_trade_no, trade_no, uid, money, status, notify, param, version, addtime FROM `{$prefix}order` WHERE out_trade_no LIKE 'RE%' AND status IN (1, 2) ORDER BY addtime ASC LIMIT 50"); $allOrders = $stmt->fetchAll(PDO::FETCH_ASSOC); if (empty($allOrders)) { @@ -210,11 +215,17 @@ function pollPaidOrders() { return; } - // 过滤掉本地已处理(paid)的 + // 只保留本方订单(RE 前缀兜底校验),再过滤掉本地已处理(paid)的 $orders = []; + $foreign = 0; foreach ($allOrders as $o) { + $outNo = (string)$o['out_trade_no']; + if (strncmp($outNo, 'RE', 2) !== 0) { + $foreign++; // 非本方订单:不建单、不写流水、不改它的 notify + continue; + } $check = $sqlite->prepare("SELECT status FROM pay_orders WHERE out_trade_no = :no"); - $check->bindValue(':no', $o['out_trade_no'], SQLITE3_TEXT); + $check->bindValue(':no', $outNo, SQLITE3_TEXT); $row = $check->execute()->fetchArray(SQLITE3_ASSOC); if (!$row || $row['status'] !== 'paid') { $orders[] = $o; @@ -222,7 +233,7 @@ function pollPaidOrders() { } if (empty($orders)) { - echo json_encode(['result' => 'no_pending_orders', 'platform_paid_count' => count($allOrders)]); + echo json_encode(['result' => 'no_pending_orders', 'platform_paid_count' => count($allOrders), 'foreign_skipped' => $foreign]); return; } @@ -310,6 +321,7 @@ function pollPaidOrders() { 'result' => 'ok', 'processed' => $processed, 'skipped' => $skipped, + 'foreign_skipped' => $foreign, 'total' => count($orders), 'elapsed_ms' => $elapsed, ]); diff --git a/api/recharge_orders_api.php b/api/recharge_orders_api.php index 55b249f..5e7a1bd 100644 --- a/api/recharge_orders_api.php +++ b/api/recharge_orders_api.php @@ -294,17 +294,25 @@ function handleSyncFromPlatform() { try { $pdo = getAdminPlatformDB(); - // 查所有已支付订单 - $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"); + // ★ 2026-10-02 只查后端自己的订单:平台 MySQL 是多商户共用库,整表拉 200 条 + // 会把其他商户/项目的单(纯数字单号)也同步成本地充值单,凭空造出 + // 玩家=unknown 的垃圾订单。本方单号固定 RE 前缀(pay.php createOrder 生成), + // 平台侧据此过滤,PHP 侧再兜底校验;平台其他订单一概不碰。 + $stmt = $pdo->query("SELECT out_trade_no, trade_no, uid, money, status, param, addtime FROM `pay_order` WHERE out_trade_no LIKE 'RE%' AND status IN (1, 2) ORDER BY addtime DESC LIMIT 200"); $platformOrders = $stmt->fetchAll(PDO::FETCH_ASSOC); $synced = 0; $alreadyPaid = 0; + $foreign = 0; $errors = []; $now = time(); foreach ($platformOrders as $po) { $outTradeNo = $po['out_trade_no']; + if (strncmp((string)$outTradeNo, 'RE', 2) !== 0) { + $foreign++; // 非本方订单:不建单、不写流水 + continue; + } $player = $po['param'] ?? 'unknown'; $tradeNo = $po['trade_no'] ?? ''; $money = (string)$po['money']; @@ -367,7 +375,8 @@ function handleSyncFromPlatform() { 'platform_total' => count($platformOrders), 'synced' => $synced, 'already_paid' => $alreadyPaid, - ], "同步完成:平台{$synced}笔已同步,{$alreadyPaid}笔已存在"); + 'foreign_skipped' => $foreign, + ], "同步完成:平台{$synced}笔已同步,{$alreadyPaid}笔已存在,{$foreign}笔非本方订单已忽略"); } catch (\Throwable $e) { error('同步失败: ' . $e->getMessage());