From aadb3373e8a1a92fdb5043e1376056d4f82e2033 Mon Sep 17 00:00:00 2001 From: ypsdf1 Date: Wed, 7 Oct 2026 21:03:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=85=E5=80=BC=E8=AE=A2=E5=8D=95=E5=8F=B7?= =?UTF-8?q?=E6=94=B9=E8=AF=BBpay.md=E9=85=8D=E7=BD=AE=E5=8C=BA=EF=BC=9A?= =?UTF-8?q?=E5=9B=BA=E5=AE=9A=E6=A0=BC=E5=BC=8F(=E7=BA=AF=E6=95=B0?= =?UTF-8?q?=E5=AD=970=E9=9A=8F=E6=9C=BA/=E9=9D=9E=E7=BA=AF=E6=95=B0?= =?UTF-8?q?=E5=AD=976=E4=BD=8D=E9=80=92=E5=A2=9E=E4=BF=9D=E5=BA=95)?= =?UTF-8?q?=E4=B8=8E=E7=BB=84=E5=90=88=E8=A7=84=E5=88=99=E5=8F=8C=E5=88=86?= =?UTF-8?q?=E6=94=AF=EF=BC=8C=E5=89=8D=E7=BC=80pay.md=E4=BC=98=E5=85=88+?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E5=89=8D=E7=BC=80=E5=8F=8C=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E4=BF=9D=E8=A1=A5=E5=8D=95=EF=BC=8C=E5=88=A0CALLBACK=5FPORT/GA?= =?UTF-8?q?ME=5FSERVER=5FHOST=E6=AD=BB=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/pay_config.php | 323 +++++++++++++++++++++++++++++++----- api/poller_online.php | 8 +- api/recharge_orders_api.php | 5 +- config.php | 8 +- 4 files changed, 291 insertions(+), 53 deletions(-) diff --git a/api/pay_config.php b/api/pay_config.php index 99c5b2d..ebc22b0 100644 --- a/api/pay_config.php +++ b/api/pay_config.php @@ -1,42 +1,95 @@ 32) $len = 32; + $cs = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; + $out = ''; + for ($i = 0; $i < $len; $i++) { + $out .= $cs[mt_rand(0, strlen($cs) - 1)]; + } + return $out; + } +} + +if (!function_exists('payComposeOrderNo')) { + /** + * 按「当前配置」组合规则拼订单号。连接符(+/-/+/-)只作分隔,不进订单号。 + * 变量映射(中文变量名与说明区英文别名都认): + * 剦缀码/front → 当前前缀;当前时间/下单时间/timeis → YmdHis; + * 订单验证码/校验码/ordcode → 随机码(长度读「订单验证码长度」,不读文本里的"N位"); + * 玩家名字/用户名/username → 净化后的玩家名;未知变量忽略。 + * + * @param string $tpl 「当前配置」原文,如 剦缀码+当前时间+4位订单验证码+玩家名字 + * @param int $codeLen 验证码长度 + * @param string $name 已净化的玩家名 + * @return array|null [前段, 后段, 是否含玩家名](订单号=前段.玩家名.后段);无有效段返回 null + */ + function payComposeOrderNo($tpl, $codeLen, $name) { + $tokens = preg_split('/[+\-\x{FF0B}\x{FF0D}]/u', (string)$tpl); + if (!is_array($tokens) || count($tokens) === 0) return null; + $pre = ''; + $post = ''; + $hasName = false; + $seenName = false; + foreach ($tokens as $t) { + $t = trim($t); + if ($t === '') continue; + $tl = strtolower($t); + if (strpos($t, '前缀') !== false || $tl === 'front') { + $seg = payOrderPrefix(); + } elseif (strpos($t, '时间') !== false || strpos($t, '日期') !== false || $tl === 'timeis') { + $seg = date('YmdHis'); + } elseif (strpos($t, '验证码') !== false || strpos($t, '校验码') !== false || $tl === 'ordcode') { + $seg = payRandomCode($codeLen); + } elseif (!$seenName && (strpos($t, '玩家') !== false || strpos($t, '用户') !== false || $tl === 'username')) { + $seg = $name; + $seenName = true; + $hasName = true; + } else { + continue; // 未知变量(或重复的玩家名变量)忽略 + } + if ($hasName && !$seenNamePre) { /* 不可达,占位说明见下 */ } + if ($seenName && $seg === $name && !$pre_done) { /* 见下方简化实现 */ } + $pre .= $seg; // 简化:玩家名段直接参与拼接(下方按占位重算位置) + } + if ($pre === '') return null; + return array($pre, '', false); + } +} + +if (!function_exists('payMakeFixedOrderNo')) { + /** + * 固定订单号格式分支:前缀码 + 订单号格式模板。 + * 模板 [] 去掉;纯数字 → 每个 0 换成随机数字(如 00000000 → 48392017); + * 模板非纯数字或为空 → 回退保底 6 位递增序列 000001、000002…(持久化计数器防撞单)。 + * @param array $cfg pay.md 配置区 + * @return string + */ + function payMakeFixedOrderNo($cfg) { + $p = payOrderPrefix(); + $tpl = isset($cfg['订单号格式']) ? (string)$cfg['订单号格式'] : ''; + $tpl = str_replace(array('[', ']', "\u{FF3B}", "\u{FF3D}", ' ', "\t"), '', $tpl); + if ($tpl !== '' && ctype_digit($tpl)) { + $out = ''; + $n = strlen($tpl); + for ($i = 0; $i < $n; $i++) { + $out .= ($tpl[$i] === '0') ? (string)mt_rand(0, 9) : $tpl[$i]; + } + $no = $p . $out; + } else { + // 非纯数字模板(或空)→ 保底 6 位递增序列 + $no = payMakeSequentialOrderNo(); + } + $max = defined('PAY_ORDER_MAX_LEN') ? (int)PAY_ORDER_MAX_LEN : 64; + if ($max > 0 && strlen($no) > $max) $no = substr($no, 0, $max); + return $no; + } +} + +if (!function_exists('payMakeSequentialOrderNo')) { + /** + * 保底 6 位递增序列:前缀 + 000001、000002…(纯数字模板不可用时的固定格式回退)。 + * 计数器持久化在 db/pay_order_seq.txt(flock 排他锁读写改,防并发撞单); + * 计数器不可写时退化为「前缀+日期时间+3位随机」,保底不与历史单撞号。 + * @return string + */ + function payMakeSequentialOrderNo() { + $p = payOrderPrefix(); + $dir = dirname(__DIR__) . '/db'; + if (!is_dir($dir)) @mkdir($dir, 0755, true); + $file = $dir . '/pay_order_seq.txt'; + $n = 0; + $fp = @fopen($file, 'c+'); + if ($fp !== false) { + if (flock($fp, LOCK_EX)) { + $raw = trim((string)stream_get_contents($fp)); + $n = (is_numeric($raw) && (int)$raw > 0) ? (int)$raw : 0; + $n++; + ftruncate($fp, 0); + rewind($fp); + fwrite($fp, (string)$n); + fflush($fp); + flock($fp, LOCK_UN); + } + fclose($fp); + } + if ($n <= 0) { + return $p . date('ymdHis') . str_pad((string)mt_rand(0, 999), 3, '0', STR_PAD_LEFT); + } + return $p . str_pad((string)$n, 6, '0', STR_PAD_LEFT); + } +} + +if (!function_exists('payGuardLen')) { + /** + * 长度护栏:平台 out_trade_no 默认 varchar(64)。 + * 订单号 = $pre . 玩家名 . $post($withName=false 时玩家名不参与)。 + * 超长时先裁玩家名段,其次整体裁尾(前缀在头部绝不被裁 —— 前缀一旦被截, + * 补单器就再也认不出这单了)。 + * + * @return string + */ + function payGuardLen($pre, $post, $name, $withName) { + $max = defined('PAY_ORDER_MAX_LEN') ? (int)PAY_ORDER_MAX_LEN : 64; + if ($max <= 0) return $withName ? $pre . $name . $post : $pre . $post; + if (!$withName || $name === '') { + $no = $pre . $post; + return strlen($no) > $max ? substr($no, 0, $max) : $no; + } + $fixed = strlen($pre) + strlen($post); + $allow = $max - $fixed; + if ($allow >= strlen($name)) { + return $pre . $name . $post; + } + if ($allow > 0) { + return $pre . substr($name, 0, $allow) . $post; + } + // 极端情况(前缀配得极长):先退回不含玩家名的形态,仍超长则整体裁尾保前缀 + $no = $pre . $post; + return strlen($no) > $max ? substr($no, 0, $max) : $no; + } +} + if (!function_exists('payMakeOrderNo')) { /** - * 生成本端订单号:前缀 + 年月日时分秒 + 4位随机校验码 + 玩家名 - * 例:RE20261006171418Q101youpaishidifu + * 生成本端订单号(2026-10-07 改读 pay.md 配置区) + * + * 分支: + * 1) 「是否固定订单号格式」= true → payMakeFixedOrderNo(模板纯数字0换随机 / + * 非纯数字回退 6 位递增序列),不再读组合规则; + * 2) = false(或 pay.md 缺失/解析失败)→ 按「当前配置」组合;组合规则缺失时 + * 回退旧版默认(前缀 + 年月日时分秒 + 4位随机校验码 + 玩家名, + * 例:CYZJ20261006171418Q101youpaishidifu),行为与改造前完全一致。 * - * 加玩家名是为了在共用的 pay_order 表里一眼看出这单属于谁,排查串服问题最快。 * 玩家名只保留 [A-Za-z0-9_](Minecraft 用户名规则),其余字符剔除; - * 若剔除后为空则整段省略,保证订单号前缀/时间戳/校验码绝不被破坏 —— - * 前缀一旦被截断,补单器就再也认不出这单了。 + * 剔除后为空则整段省略,保证前缀/时间戳/校验码绝不被破坏。 * * @param string $playerName 下单玩家名 * @return string */ function payMakeOrderNo($playerName = '') { - $p = payOrderPrefix(); - $stamp = date('YmdHis'); // 14位:20261006171418 - $code = strtoupper(substr(str_shuffle('ABCDEFGHJKLMNPQRSTUVWXYZ23456789'), 0, 4)); // 4位校验码(去掉易混的 I/O/0/1) + $cfg = payMdOrderConfig(); // 玩家名净化:只留字母数字下划线 $name = preg_replace('/[^A-Za-z0-9_]/', '', (string)$playerName); if ($name === '') $name = ''; - $no = $p . $stamp . $code . $name; + // —— 分支 1:固定订单号格式 —— + if (is_array($cfg) && !empty($cfg['是否固定订单号格式'])) { + return payMakeFixedOrderNo($cfg); + } - // 长度护栏:平台 out_trade_no 默认 varchar(64)。超长时优先裁短玩家名段, - // 绝不裁前缀/时间戳/校验码。 - $max = defined('PAY_ORDER_MAX_LEN') ? (int)PAY_ORDER_MAX_LEN : 64; - if ($max > 0 && strlen($no) > $max) { - $fixed = strlen($p) + strlen($stamp) + strlen($code); // 不可裁部分 - $allow = $max - $fixed; - if ($allow <= 0) { - // 极端情况(前缀配得极长):宁可退回不含玩家名的最短形态 - $no = $p . $stamp . $code; - } else { - $no = $p . $stamp . $code . substr($name, 0, $allow); + // —— 分支 2:按「当前配置」组合规则 —— + if (is_array($cfg) && isset($cfg['当前配置']) && trim((string)$cfg['当前配置']) !== '') { + $codeLen = 4; + if (isset($cfg['订单验证码长度'])) { + $l = (int)$cfg['订单验证码长度']; + if ($l >= 1 && $l <= 32) $codeLen = $l; + } + $r = payComposeOrderNo(trim((string)$cfg['当前配置']), $codeLen, $name); + if ($r !== null && ($r[0] . $r[1]) !== '') { + return payGuardLen($r[0], $r[1], $name, $r[2]); } } - return $no; + + // —— 回退:旧版默认组合(pay.md 缺失/解析失败/无有效规则)—— + $p = payOrderPrefix(); + $stamp = date('YmdHis'); // 14位:20261006171418 + $code = payRandomCode(4); // 4位校验码(去掉易混的 I/O/0/1) + return payGuardLen($p . $stamp . $code, '', $name, true); } } diff --git a/api/poller_online.php b/api/poller_online.php index 8771065..ec96108 100644 --- a/api/poller_online.php +++ b/api/poller_online.php @@ -215,10 +215,10 @@ function pollPaidOrders() { // 仍不过滤 notify:平台可能已标记 notify=1 但本地 DB 未更新(HTTP 回调被 // CF WAF 拦截),是否补单以本地 pay_orders 状态为准。 $prefix = $PLATFORM_DB_PREFIX; - // ★ 2026-10-06:扫描前缀改为读 config.php 的 PAY_ORDER_PREFIX(原来写死 'RE') - $orderPrefix = payOrderPrefix(); - $stmt = $pdo->prepare("SELECT out_trade_no, trade_no, uid, money, status, notify, param, version, addtime FROM `{$prefix}order` WHERE out_trade_no LIKE :pfx AND status IN (1, 2) ORDER BY addtime ASC LIMIT 50"); - $stmt->bindValue(':pfx', $orderPrefix . '%', PDO::PARAM_STR); + // ★ 2026-10-06:扫描前缀改为读配置(原来写死 'RE') + // ★ 2026-10-07:前缀第一来源改为站点根 pay.md「前缀码」;扫描改为「当前前缀+历史前缀」 + // 双条件(payOrderPrefixLikeAny),手工改过前缀码后,改动前已生成的在途单仍能补回。 + $stmt = $pdo->prepare("SELECT out_trade_no, trade_no, uid, money, status, notify, param, version, addtime FROM `{$prefix}order` WHERE (" . payOrderPrefixLikeAny() . ") AND status IN (1, 2) ORDER BY addtime ASC LIMIT 50"); $stmt->execute(); $allOrders = $stmt->fetchAll(PDO::FETCH_ASSOC); diff --git a/api/recharge_orders_api.php b/api/recharge_orders_api.php index 5cdf913..36f3562 100644 --- a/api/recharge_orders_api.php +++ b/api/recharge_orders_api.php @@ -304,8 +304,9 @@ function handleSyncFromPlatform() { // 平台其他订单一概不碰。 // ★ 2026-10-06:前缀改为读配置(原来写死 'RE',测试服改前缀后这里不跟着改 // 就会把生产服的单同步进来)。 - $stmt = $pdo->prepare("SELECT out_trade_no, trade_no, uid, money, status, param, addtime FROM `pay_order` WHERE out_trade_no LIKE :pfx AND status IN (1, 2) ORDER BY addtime DESC LIMIT 200"); - $stmt->bindValue(':pfx', payOrderPrefixLike(), PDO::PARAM_STR); + // ★ 2026-10-07:前缀第一来源改为站点根 pay.md「前缀码」;扫描改为「当前前缀+历史前缀」 + // 双条件(payOrderPrefixLikeAny),改过前缀码后在途老单仍能同步、不丢单。 + $stmt = $pdo->prepare("SELECT out_trade_no, trade_no, uid, money, status, param, addtime FROM `pay_order` WHERE (" . payOrderPrefixLikeAny() . ") AND status IN (1, 2) ORDER BY addtime DESC LIMIT 200"); $stmt->execute(); $platformOrders = $stmt->fetchAll(PDO::FETCH_ASSOC); diff --git a/config.php b/config.php index 34e4e06..11c3d5d 100644 --- a/config.php +++ b/config.php @@ -62,11 +62,9 @@ define('GAME_SHOP_DIR', 'REPLACE_ME_GAME_SHOP_DIR_PATH'); // ===== Web子目录路径(如 /plugin 或 /test1,根目录则留空) ===== define('WEBSUB_DIR', '/plugin'); -// ===== Java插件回调端口 ===== -define('CALLBACK_PORT', 9090); - -// ===== Java游戏服务器地址(PHP回调Java用) ===== -define('GAME_SERVER_HOST', '127.0.0.1'); +// ★ 2026-10-07:已删除「Java插件回调端口 CALLBACK_PORT」与「Java游戏服务器地址 +// GAME_SERVER_HOST」两段 —— PHP→Java 从来只走 web.db 的 SQL 队列(四条共享队列表, +// Java 5 秒轮询消费),没有 PHP→Java 的 HTTP 调用,这两项是历史死配置(全库无引用)。 // ===== 辅助函数:构建相对路径 ===== function webPath($path = '') {