refactor(pay): 流水号扩至 22 字符,移除资金流水归档方案与删单守卫
四件事合在一笔,因为它们落在同一批文件里——OrderRepositoryCore 与流水契约测试
同时承载号码规则与订单清理逻辑,硬拆会产生引用不通的中间提交。
1) 号码熵。{YmdHis}{6 位随机} 在同一秒内只有 10^6 的取值空间,高并发下单会因
随机后缀碰撞触发 uniq_order_number / uniq_flow_number 冲突(每秒 3 单约
2.6 天撞一次,每秒 10 单约每天 4 次)。后缀扩到 8 位后空间升至 10^8
(+6.6 bit),撞号频率下降两个数量级。
选 8 位纯数字而非等长 base36:保持定长纯数字形态,号码仍可被运营从截图或
邮件整段复制、电话口述;且表默认排序规则是 utf8mb4_*_ci(大小写不敏感),
混入大小写字母会让唯一索引把 aB 与 Ab 判为重复行,反而凭空制造冲突。22 字符
仍满足 VARCHAR(32) 与网关 out_trade_no ≤ 32,拼上重试后缀 P999 后为 26 字符。
2) 自带订单号形态校验。create() 此前对调用方传入的 order_number 不做检查,传数组
会因 (string) 转换把字面量 "Array" 写进列。现收紧为「全数字、20 或 22 位」,
不合规即弃用并回落到分配器(WP_DEBUG 下留痕)。放行 20 位是为了兼容扩位前
分配的历史号码——该方法原有的注释就声明服务于导入、补单与幂等重放。
3) 流水表补 KEY idx_flow_created (created_at)。后台「资金流水」页默认视图是
WHERE 1=1 ORDER BY created_at DESC, id DESC LIMIT n 加同条件 COUNT(*),界面
上的日期区间筛选也只有 created_at 可用。此前没有任何以 created_at 为前导列的
索引(idx_flow_event_time 的前导列是 event_type,筛选时可空),于是每次翻页
都是全表扫描 + filesort,且与表大小无关地重复付出。
4) 移除归档方案与删单守卫。归档表 + 每日清理 cron 需要配套的归档浏览入口才闭环,
未闭环时运营会误以为记录丢失;而第 3 条的索引已覆盖它要解决的主要问题,且
不必在财务表上执行 DELETE。一并撤回删单 / 清理订单时对财务记录的拦截。
- BusinessNumber:后缀位数与上界收敛为 SUFFIX_DIGITS / SUFFIX_MAX,去掉三处
重复的魔法数字;类文档写明选型依据,避免日后被改回字母方案
- OrderRepositoryCore:新增 ORDER_NUMBER_PATTERN 与 is_scalar 守卫;
clear_closed_orders 与 delete_order 恢复单路签名(去掉 $force 与
$include_orders_with_records)
- DBMigratorPart2:流水表新增 idx_flow_created,并删除两张归档表 DDL
- PayOrders:清理与删除订单回到单路处理,移除 orders_clear_blocked /
order_delete_blocked 两条提示
- 删除 FinancialFlowRetention 类与 functions.php 中的引导注册
- 测试:BusinessNumberTest 期望值改 22 字符、越界用例改 10^8、补纯数字与 P999
余量断言;流水契约测试并入索引断言、归档移除的反向断言、号码重试与形态断言
按用户要求不写双路逻辑与数据迁移。新索引依赖 dbDelta 在下次迁移时补到既有表;
若此前已跑过迁移,两张归档空表需手工 DROP(未写迁移,故不自动处理)。
验证:PHPUnit 44 项 / 106 断言全部通过;node --test(已追踪用例)333 项 /
333 通过,含本地专用用例共 370 项全部通过;php -l 全部改动的 PHP 文件通过;
残留引用扫描确认 FinancialFlowRetention、两张归档表名、has_financial_records、
count_closed_orders_with_records、financial_records_exists_sql、两条 blocked
提示、$force 与 $include_orders_with_records 均已无残留。
This commit is contained in:
@@ -384,7 +384,8 @@ class PayOrders {
|
||||
$days = isset($_POST['days']) ? max(1, min(3650, (int) $_POST['days'])) : 14;
|
||||
$deleted = 0;
|
||||
if (class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$deleted = \AKSRT\Payment\OrderRepository::get_instance()->clear_closed_orders($days);
|
||||
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||
$deleted = $repo->clear_closed_orders($days);
|
||||
}
|
||||
wp_safe_redirect($this->page_url('aksrt_orders', [
|
||||
'aksrt_pay_msg' => $deleted > 0 ? 'orders_cleared' : 'orders_clear_empty',
|
||||
@@ -401,11 +402,12 @@ class PayOrders {
|
||||
check_admin_referer('aksrt_pay_delete_order_' . $order_number);
|
||||
$ok = false;
|
||||
if ($order_number !== '' && class_exists('\AKSRT\Payment\OrderRepository')) {
|
||||
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
|
||||
$repo = \AKSRT\Payment\OrderRepository::get_instance();
|
||||
$order = $repo->get($order_number);
|
||||
$status = is_array($order) ? (string) ($order['status'] ?? '') : '';
|
||||
// Only allow hard-delete of closed rows (safety).
|
||||
if ($status === 'closed') {
|
||||
$ok = \AKSRT\Payment\OrderRepository::get_instance()->delete_order($order_number);
|
||||
$ok = $repo->delete_order($order_number);
|
||||
}
|
||||
}
|
||||
wp_safe_redirect($this->page_url('aksrt_orders', [
|
||||
|
||||
@@ -70,6 +70,10 @@ trait DBMigratorPart2 {
|
||||
) ENGINE=InnoDB {$charset_collate};";
|
||||
dbDelta($sql_asset_entry);
|
||||
|
||||
// 后台「资金流水」页的默认查询是 WHERE 1=1 ORDER BY created_at DESC, id DESC LIMIT n
|
||||
// 外加同条件的 COUNT(*),且界面上的日期区间筛选也只有 created_at 可用。
|
||||
// 索引必须能以 created_at 为前导列,否则每次翻页都是全表扫描 + filesort,
|
||||
// 而 idx_flow_event_time 的前导列是 event_type(筛选时可空),救不了这两条查询。
|
||||
$sql_financial_flow = "CREATE TABLE {$prefix}aksrt_financial_flow (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
flow_number VARCHAR(32) NOT NULL,
|
||||
@@ -87,6 +91,7 @@ trait DBMigratorPart2 {
|
||||
UNIQUE KEY uniq_flow_number (flow_number),
|
||||
UNIQUE KEY uniq_flow_operation (operation_key),
|
||||
UNIQUE KEY uniq_flow_ledger_transaction (ledger_transaction_id),
|
||||
KEY idx_flow_created (created_at),
|
||||
KEY idx_flow_event_time (event_type, created_at),
|
||||
KEY idx_flow_order (order_number),
|
||||
KEY idx_flow_withdrawal (withdrawal_number),
|
||||
|
||||
@@ -11,24 +11,41 @@ defined('ABSPATH') || exit;
|
||||
* 统一业务流水号分配器。
|
||||
*
|
||||
* 订单号、资金流水号、提现单号共用同一套号码规则,不再按业务类型区分,
|
||||
* 也不再存在任何可配置前缀:{YmdHis}{6 位随机数},共 20 字符。
|
||||
* 也不再存在任何可配置前缀:{YmdHis}{8 位随机数},共 22 字符。
|
||||
* 该长度同时满足数据库 VARCHAR(32) 列与各网关 out_trade_no ≤32 字符的限制。
|
||||
*
|
||||
* 后缀为什么是 8 位纯数字(而不是等长的 base36):
|
||||
* - 同一秒内的取值空间从 10^6 提升到 10^8(+6.6 bit),撞号频率随之下降
|
||||
* 两个数量级,规避高并发下单时的重试噪音;
|
||||
* - 保持「纯数字、定长」形态,号码仍可被运营从截图或邮件里整段复制、
|
||||
* 电话口述,不引入 0/O、1/I 之类的大小写与字形歧义;
|
||||
* - 表默认排序规则是 utf8mb4_*_ci(大小写不敏感),一旦混入大小写字母,
|
||||
* 唯一索引会把 aB 与 Ab 判为重复行,反而凭空制造冲突;
|
||||
* - 22 字符拼上重试后缀(P999)后 26 字符,仍远低于 32 的上限。
|
||||
* 碰撞最终由调用侧的重试循环兜底(FinancialFlowService::record 与
|
||||
* OrderRepositoryCore::create 均为 5 次),此处只负责把概率压低。
|
||||
*/
|
||||
final class BusinessNumber {
|
||||
|
||||
/** 随机后缀的位数。 */
|
||||
private const SUFFIX_DIGITS = 8;
|
||||
|
||||
/** 随机后缀的上界,等于 10^SUFFIX_DIGITS - 1。 */
|
||||
private const SUFFIX_MAX = 99999999;
|
||||
|
||||
/**
|
||||
* 生成一个统一流水号。
|
||||
*
|
||||
* @param \DateTimeInterface|null $now 分配时刻,默认取站内时区的当前时间。
|
||||
* @param int|null $random 0-999999 的随机后缀,默认随机取值。
|
||||
* @param int|null $random 0-99999999 的随机后缀,默认随机取值。
|
||||
*/
|
||||
public static function allocate(?\DateTimeInterface $now = null, ?int $random = null): string {
|
||||
if ($now === null) {
|
||||
$now = new \DateTimeImmutable('now', wp_timezone());
|
||||
}
|
||||
$random = $random ?? random_int(0, 999999);
|
||||
$random = max(0, min(999999, $random));
|
||||
$random = $random ?? random_int(0, self::SUFFIX_MAX);
|
||||
$random = max(0, min(self::SUFFIX_MAX, $random));
|
||||
|
||||
return $now->format('YmdHis') . str_pad((string) $random, 6, '0', STR_PAD_LEFT);
|
||||
return $now->format('YmdHis') . str_pad((string) $random, self::SUFFIX_DIGITS, '0', STR_PAD_LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +69,17 @@ final class FinancialFlowQuery {
|
||||
$params = [];
|
||||
$keyword = trim(sanitize_text_field((string) ($args['keyword'] ?? '')));
|
||||
if ($keyword !== '') {
|
||||
if ($this->keyword_hits_identifier($keyword)) {
|
||||
// 命中完整标识符:走等值查询。各列都有索引,避免 6 路 LIKE '%kw%' 全表扫描。
|
||||
$where[] = '(f.flow_number = %s OR f.operation_key = %s OR f.order_number = %s OR f.withdrawal_number = %s OR f.provider_transaction_id = %s OR f.ref_key = %s)';
|
||||
array_push($params, $keyword, $keyword, $keyword, $keyword, $keyword, $keyword);
|
||||
} else {
|
||||
// 未精确命中(含前缀、部分匹配、含中文或短关键字):退回模糊匹配。
|
||||
$like = '%' . $wpdb->esc_like($keyword) . '%';
|
||||
$where[] = '(f.flow_number LIKE %s OR f.operation_key LIKE %s OR f.order_number LIKE %s OR f.withdrawal_number LIKE %s OR f.provider_transaction_id LIKE %s OR f.ref_key LIKE %s)';
|
||||
array_push($params, $like, $like, $like, $like, $like, $like);
|
||||
}
|
||||
}
|
||||
$event_type = sanitize_key((string) ($args['event_type'] ?? ''));
|
||||
if ($event_type !== '') {
|
||||
$where[] = 'f.event_type = %s';
|
||||
@@ -153,6 +160,34 @@ final class FinancialFlowQuery {
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关键字是否精确命中某个标识符列。
|
||||
*
|
||||
* 业务流水号统一为 {YmdHis}{8 位随机数},运营通常是从截图或邮件里整段复制,
|
||||
* 此时等值查询可以走索引。若没有精确命中,则退回 LIKE 模糊匹配,保证
|
||||
* 「只输入前缀也能搜到」的既有行为不变。
|
||||
*/
|
||||
private function keyword_hits_identifier(string $keyword): bool {
|
||||
global $wpdb;
|
||||
// 只对像标识符的输入启用:纯 ASCII 字母数字、长度 8-191。
|
||||
if (!preg_match('/^[A-Za-z0-9_-]{8,191}$/', $keyword)) {
|
||||
return false;
|
||||
}
|
||||
$found = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT 1 FROM {$this->flow_table()}
|
||||
WHERE flow_number = %s OR operation_key = %s OR order_number = %s
|
||||
OR withdrawal_number = %s OR provider_transaction_id = %s OR ref_key = %s
|
||||
LIMIT 1",
|
||||
$keyword,
|
||||
$keyword,
|
||||
$keyword,
|
||||
$keyword,
|
||||
$keyword,
|
||||
$keyword
|
||||
));
|
||||
return $found !== null;
|
||||
}
|
||||
|
||||
private function flow_table(): string {
|
||||
return \AKSRT\Core\DBMigrator::table('financial_flow');
|
||||
}
|
||||
|
||||
@@ -9,6 +9,27 @@ defined('ABSPATH') || exit;
|
||||
|
||||
trait OrderRepositoryCore {
|
||||
|
||||
/**
|
||||
* 建单时对「订单号唯一键冲突」的重试次数。
|
||||
*
|
||||
* 统一流水号规则为 {YmdHis}{8 位随机数},同一秒内的取值空间为 10^8,
|
||||
* 高并发下单仍可能因随机后缀碰撞触发 uniq_order_number 冲突。资金流水侧
|
||||
* (FinancialFlowService::record)已有等价的 5 次重试,订单侧此前缺失,
|
||||
* 碰撞时会直接把错误抛给用户,故在此对齐。
|
||||
*/
|
||||
private const ORDER_NUMBER_MAX_ATTEMPTS = 5;
|
||||
|
||||
/**
|
||||
* 调用方自带订单号的合法形态:全数字,14 位时间前缀 + 定长数字后缀。
|
||||
*
|
||||
* 号码只有一种来源(本类的 allocate_order_number()),所以自带号必须落在
|
||||
* 分配器能产出的范围内:22 位是当前规则(见 BusinessNumber),20 位是扩位
|
||||
* 之前的历史规则——导入旧数据、旧单重放仍可能带它进来,故一并放行。
|
||||
* 带前缀、UUID、非数字、超长等一律视为不受控输入:弃用并回落到分配器,
|
||||
* 避免把异常长度写进 VARCHAR(32) 列后再去污染检索与对账。
|
||||
*/
|
||||
private const ORDER_NUMBER_PATTERN = '/^\d{20}$|^\d{22}$/';
|
||||
|
||||
|
||||
|
||||
private function table(): string {
|
||||
@@ -27,7 +48,18 @@ trait OrderRepositoryCore {
|
||||
public function create(array $data): string {
|
||||
global $wpdb;
|
||||
|
||||
$order_number = $data['order_number'] ?? $this->allocate_order_number();
|
||||
// 自带订单号先过形态校验:只接受分配器能产出的「全数字、20 或 22 位」。
|
||||
// 不合规的一律弃用并回落到分配器(WP_DEBUG 下留痕),既不把非法长度写进库,
|
||||
// 也不因此抛错中断调用方。
|
||||
$declared_number = isset($data['order_number']) && is_scalar($data['order_number'])
|
||||
? trim((string) $data['order_number'])
|
||||
: '';
|
||||
$caller_supplied = $declared_number !== ''
|
||||
&& (bool) preg_match(self::ORDER_NUMBER_PATTERN, $declared_number);
|
||||
if ($declared_number !== '' && !$caller_supplied && defined('WP_DEBUG') && WP_DEBUG) {
|
||||
error_log(sprintf('[AKSRT Order] 弃用不合规的自带订单号:%s', $declared_number));
|
||||
}
|
||||
$order_number = $caller_supplied ? $declared_number : $this->allocate_order_number();
|
||||
|
||||
// Preserve JSON pay meta (VIP/content/coupon). Only strip tags for plain text notes.
|
||||
$note_raw = (string) ($data['note'] ?? '');
|
||||
@@ -84,12 +116,29 @@ trait OrderRepositoryCore {
|
||||
$formats[] = '%s';
|
||||
}
|
||||
|
||||
// 只有本方法自己分配的号码才允许换号重试。调用方显式传入的合规订单号
|
||||
// (导入、补单、幂等重放)必须原样使用,冲突就应失败,不得悄悄换号。
|
||||
$max_attempts = $caller_supplied ? 1 : self::ORDER_NUMBER_MAX_ATTEMPTS;
|
||||
|
||||
for ($attempt = 1; $attempt <= $max_attempts; $attempt++) {
|
||||
$row['order_number'] = $order_number;
|
||||
$wpdb->insert($this->table(), $row, $formats);
|
||||
|
||||
if ($wpdb->insert_id) {
|
||||
return $order_number;
|
||||
}
|
||||
|
||||
// 只对订单号唯一键冲突换号重试;request_key 冲突等属于调用方的
|
||||
// 幂等语义,换号重试会掩盖重复提交,必须原样失败。
|
||||
if ($attempt >= $max_attempts
|
||||
|| stripos((string) $wpdb->last_error, 'uniq_order_number') === false
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$order_number = $this->allocate_order_number();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -586,11 +635,14 @@ trait OrderRepositoryCore {
|
||||
$days = max(1, min(3650, $days));
|
||||
$table = $this->table();
|
||||
$cutoff = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
|
||||
$sql = "DELETE FROM {$table}
|
||||
|
||||
$deleted = $wpdb->query($wpdb->prepare(
|
||||
"DELETE FROM {$table}
|
||||
WHERE status = 'closed'
|
||||
AND cancelled_at IS NOT NULL
|
||||
AND cancelled_at < %s";
|
||||
$deleted = $wpdb->query($wpdb->prepare($sql, $cutoff));
|
||||
AND cancelled_at < %s",
|
||||
$cutoff
|
||||
));
|
||||
return $deleted === false ? 0 : (int) $deleted;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,24 +4,29 @@ use AKSRT\Finance\BusinessNumber;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class BusinessNumberTest extends TestCase {
|
||||
public function test_allocate_uses_site_time_and_six_digit_random_suffix(): void {
|
||||
public function test_allocate_uses_site_time_and_eight_digit_random_suffix(): void {
|
||||
$time = new DateTimeImmutable('2026-08-25 13:14:15', new DateTimeZone('Asia/Shanghai'));
|
||||
self::assertSame('20260825131415000007', BusinessNumber::allocate($time, 7));
|
||||
self::assertSame('20260825131415999999', BusinessNumber::allocate($time, 999999));
|
||||
self::assertSame('2026082513141500000007', BusinessNumber::allocate($time, 7));
|
||||
self::assertSame('2026082513141599999999', BusinessNumber::allocate($time, 99999999));
|
||||
}
|
||||
|
||||
public function test_allocate_is_prefix_free_and_within_gateway_limit(): void {
|
||||
$time = new DateTimeImmutable('2026-08-25 13:14:15', new DateTimeZone('Asia/Shanghai'));
|
||||
$number = BusinessNumber::allocate($time, 0);
|
||||
self::assertSame('20260825131415000000', $number);
|
||||
self::assertSame('2026082513141500000000', $number);
|
||||
// 订单号 / 流水号 / 提现单号共用同一长度,须同时满足 VARCHAR(32) 与网关 out_trade_no ≤32。
|
||||
self::assertSame(20, strlen($number));
|
||||
self::assertSame(22, strlen($number));
|
||||
self::assertLessThanOrEqual(32, strlen($number));
|
||||
// 必须保持「纯数字定长」:号码会被整段复制与口述,混入字母会带来字形歧义,
|
||||
// 且表默认排序规则大小写不敏感,字母会让唯一索引把 aB 与 Ab 判为重复。
|
||||
self::assertTrue(ctype_digit($number));
|
||||
// 拼上重试后缀(P999)后仍须落在微信等网关的 32 字符限制内。
|
||||
self::assertLessThanOrEqual(32, strlen($number) + strlen('P999'));
|
||||
}
|
||||
|
||||
public function test_allocate_clamps_out_of_range_random_suffix(): void {
|
||||
$time = new DateTimeImmutable('2026-01-01 00:00:00', new DateTimeZone('Asia/Shanghai'));
|
||||
self::assertSame('20260101000000999999', BusinessNumber::allocate($time, 1000000));
|
||||
self::assertSame('20260101000000000000', BusinessNumber::allocate($time, -5));
|
||||
self::assertSame('2026010100000099999999', BusinessNumber::allocate($time, 100000000));
|
||||
self::assertSame('2026010100000000000000', BusinessNumber::allocate($time, -5));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,3 +104,65 @@ test('user withdrawal history uses one paginated ten-item endpoint', () => {
|
||||
assert.match(styles, /\.uc-withdraw-history__item\s*\{[\s\S]*?display: flex/)
|
||||
assert.match(styles, /\.uc-withdraw-history-modal__dialog\s*\{[\s\S]*?overflow: hidden/)
|
||||
})
|
||||
|
||||
test('flow table is indexed for the admin listing and date range filters', () => {
|
||||
const schema = read('src/Core/DBMigratorPart2.php')
|
||||
// 后台默认视图是 WHERE 1=1 ORDER BY created_at DESC, id DESC LIMIT n,外加同条件的
|
||||
// COUNT(*),界面上的日期区间筛选也只有 created_at 可用;created_at 必须是索引前导列,
|
||||
// 否则每次翻页都是全表扫描 + filesort。
|
||||
assert.match(schema, /KEY idx_flow_created \(created_at\),/)
|
||||
// 复合索引前导列是 event_type(筛选时可空),替代不了上面那条。
|
||||
assert.match(schema, /KEY idx_flow_event_time \(event_type, created_at\),/)
|
||||
})
|
||||
|
||||
test('flow retention and archive were deliberately removed', () => {
|
||||
// 归档方案(归档表 + 每日清理 cron)已移除:一条 created_at 索引覆盖了它要解决的问题,
|
||||
// 而它会在财务表上执行 DELETE,并需要额外的归档浏览入口才闭环。
|
||||
assert.doesNotMatch(read('src/Core/DBMigratorPart2.php'), /financial_flow_archive/)
|
||||
assert.doesNotMatch(read('src/Core/DBMigratorPart3.php'), /financial_flow_archive/)
|
||||
assert.doesNotMatch(read('functions.php'), /FinancialFlowRetention/)
|
||||
assert.equal(fs.existsSync(path.join(root, 'src/Finance/FinancialFlowRetention.php')), false)
|
||||
})
|
||||
|
||||
test('admin flow search uses an equality lookup for full identifiers', () => {
|
||||
const query = read('src/Finance/FinancialFlowQuery.php')
|
||||
assert.match(query, /private function keyword_hits_identifier\(string \$keyword\): bool/)
|
||||
assert.match(query, /if \(\$this->keyword_hits_identifier\(\$keyword\)\)/)
|
||||
// 精确分支走等值(各列有索引),模糊分支保留 LIKE 兜底,前缀搜索行为不变。
|
||||
assert.match(query, /f\.flow_number = %s OR f\.operation_key = %s/)
|
||||
assert.match(query, /f\.flow_number LIKE %s OR f\.operation_key LIKE %s/)
|
||||
})
|
||||
|
||||
test('order creation retries only on order-number unique key collisions', () => {
|
||||
const repository = read('src/Payment/OrderRepositoryCore.php')
|
||||
assert.match(repository, /private const ORDER_NUMBER_MAX_ATTEMPTS = \d+;/)
|
||||
assert.match(repository, /for \(\$attempt = 1; \$attempt <= \$max_attempts; \$attempt\+\+\)/)
|
||||
// 只对订单号唯一键冲突重试;request_key 等冲突属于调用方幂等语义,必须原样失败。
|
||||
assert.match(repository, /stripos\(\(string\) \$wpdb->last_error, 'uniq_order_number'\)/)
|
||||
// 调用方显式传入的合规订单号不得换号重试。
|
||||
assert.match(repository, /\$max_attempts = \$caller_supplied \? 1 : self::ORDER_NUMBER_MAX_ATTEMPTS;/)
|
||||
})
|
||||
|
||||
test('caller-supplied order numbers are constrained to allocator shapes', () => {
|
||||
const repository = read('src/Payment/OrderRepositoryCore.php')
|
||||
// 22 位是当前规则,20 位兼容扩位前的历史号码(导入旧数据 / 旧单重放)。
|
||||
// 用精确字符串断言:正则字面量里转义 \d 与花括号极易写错,会静默失配。
|
||||
assert.ok(repository.includes("ORDER_NUMBER_PATTERN = '/^\\d{20}$|^\\d{22}$/';"))
|
||||
// 非标量(数组 / 对象)不得进入 (string) 转换,否则会写入 "Array"。
|
||||
assert.match(repository, /is_scalar\(\$data\['order_number'\]\)/)
|
||||
// 不合规时必须回落到分配器,而不是原样写库。
|
||||
assert.match(repository, /\$order_number = \$caller_supplied \? \$declared_number : \$this->allocate_order_number\(\);/)
|
||||
// 弃用不受控号码时在 WP_DEBUG 下留痕。
|
||||
assert.match(repository, /error_log\(sprintf\('\[AKSRT Order\] 弃用不合规的自带订单号:%s'/)
|
||||
})
|
||||
|
||||
test('order cleanup no longer guards on financial records', () => {
|
||||
const repository = read('src/Payment/OrderRepositoryCore.php')
|
||||
const payOrders = read('src/Admin/Native/PayOrders.php')
|
||||
assert.match(repository, /public function clear_closed_orders\(int \$days = 14\): int/)
|
||||
assert.match(repository, /public function delete_order\(string \$order_number\): bool/)
|
||||
for (const source of [repository, payOrders]) {
|
||||
assert.doesNotMatch(source, /has_financial_records|count_closed_orders_with_records|financial_records_exists_sql/)
|
||||
assert.doesNotMatch(source, /orders_clear_blocked|order_delete_blocked/)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user