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 = ''; // 玩家名之后的段落 $withName = false; // 组合里含玩家名变量(payGuardLen 据此裁剪) $afterName = false; // 已越过玩家名位置 $valid = 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 (!$afterName && !$withName && (strpos($t, '玩家') !== false || strpos($t, '用户') !== false || $tl === 'username')) { // 玩家名段:本身由 payGuardLen 统一拼装,这里只切换分段状态(重复的玩家名变量忽略) $withName = true; $afterName = true; $valid = true; continue; } else { continue; // 未知变量忽略 } if ($afterName) $post .= $seg; else $pre .= $seg; $valid = true; } if (!$valid) return null; return array($pre, $post, $withName); } } if (!function_exists('payMakeFixedOrderNo')) { /** * 固定订单号格式分支:前缀码 + 订单号格式模板。 * 模板 [] 去掉; * - 纯数字(如 00000000)→ 按【模板位数】递增序列:00000001、00000002… * (计数器持久化在 db/pay_order_seq.txt,flock 防撞单;当前无订单则从 1 开始); * - 非纯数字或为空 → 保底 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)) { // ★ 纯数字模板:按模板位数递增序列(00000000 → CYZJ00000001, CYZJ00000002…) $no = payMakeSequentialOrderNo($tpl); } else { // 非纯数字模板(或空)→ 保底 6 位递增序列 $no = payMakeSequentialOrderNo('000000'); } $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')) { /** * 递增序列订单号:前缀 + 模板位数的序列(000001、000002…,默认 6 位)。 * 计数器持久化在 db/pay_order_seq.txt(flock 排他锁读写改,防并发撞单); * 计数器不可写时退化为「前缀+日期时间+3位随机」,保底不与历史单撞号。 * * @param string $tpl 位数模板(纯数字,如 '00000000'),决定序列补零位数 * @return string */ function payMakeSequentialOrderNo($tpl = '000000') { $p = payOrderPrefix(); $width = strlen(ctype_digit($tpl) ? $tpl : '000000'); $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, $width, '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')) { /** * 生成本端订单号(2026-10-07 改读 pay.md 配置区) * * 分支: * 1) 「是否固定订单号格式」= true → payMakeFixedOrderNo(模板纯数字0换随机 / * 非纯数字回退 6 位递增序列),不再读组合规则; * 2) = false(或 pay.md 缺失/解析失败)→ 按「当前配置」组合;组合规则缺失时 * 回退旧版默认(前缀 + 年月日时分秒 + 4位随机校验码 + 玩家名, * 例:CYZJ20261006171418Q101youpaishidifu),行为与改造前完全一致。 * * 玩家名只保留 [A-Za-z0-9_](Minecraft 用户名规则),其余字符剔除; * 剔除后为空则整段省略,保证前缀/时间戳/校验码绝不被破坏。 * * @param string $playerName 下单玩家名 * @return string */ function payMakeOrderNo($playerName = '') { $cfg = payMdOrderConfig(); // 玩家名净化:只留字母数字下划线 $name = preg_replace('/[^A-Za-z0-9_]/', '', (string)$playerName); if ($name === '') $name = ''; // —— 分支 1:固定订单号格式 —— if (is_array($cfg) && !empty($cfg['是否固定订单号格式'])) { return payMakeFixedOrderNo($cfg); } // —— 分支 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]); } } // —— 回退:旧版默认组合(pay.md 缺失/解析失败/无有效规则)—— $p = payOrderPrefix(); $stamp = date('YmdHis'); // 14位:20261006171418 $code = payRandomCode(4); // 4位校验码(去掉易混的 I/O/0/1) return payGuardLen($p . $stamp . $code, '', $name, true); } }