/** * 「收款接口显示顺序」契约(后台 sortable 控件 → 收银台支付方式顺序)。 * * 背景:后台 收款接口 → 收款接口显示顺序 曾整列空白、保存后顺序被静默改写, * 且没有任何前台代码消费该配置。本契约锁定四件事: * 1) sortable 项使用 AksField 实际读取的 `label` 键(写 `title` 会渲染成空白); * 2) 持久化必须保留用户提交的键顺序(键顺序即配置值本身); * 3) 排序只保留一份实现,并且真的作用到收银台; * 4) 相关的重复实现 / 死模块不回潮。 */ const test = require('node:test'); const assert = require('node:assert'); const fs = require('node:fs'); const path = require('node:path'); const root = path.join(__dirname, '..', '..'); const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8'); const schemaPhp = read('src', 'Admin', 'Schema', 'SchemaPayment.php'); const schemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'gateway.js'); const aksField = read('assets', 'admin', 'src', 'components', 'form', 'AksField.vue'); const provider = read('src', 'Admin', 'SchemaProvider.php'); const pay = read('inc', 'helpers', 'aksrt-pay.php'); const gateway = read('src', 'Payment', 'Gateway.php'); const wallet = read('inc', 'helpers', 'user-center-renderers', 'credits-wallet.php'); const pointsRenderer = read('inc', 'helpers', 'user-center-renderers', 'credits-balance.php'); const vipPackages = read('src', 'Content', 'VipServicePackages.php'); const balanceCharge = read('src', 'Credits', 'BalanceServiceCharge.php'); test('sortable 项使用渲染组件读取的 label 键', () => { // 渲染侧读取 item.label —— items 里写 title 会让每一行只剩上下箭头。 assert.match(aksField, /\{\{\s*item\.label\s*\}\}/); assert.match(schemaJs, /items:\s*\[\s*\{ key: 'wechat', label: '微信' \}/); assert.match(schemaJs, /\{ key: 'card_pass', label: '卡密' \}/); assert.doesNotMatch(schemaJs, /title: '微信'/); assert.match(schemaPhp, /\['key' => 'wechat', 'label' => '微信'\]/); assert.match(schemaPhp, /\['key' => 'card_pass', 'label' => '卡密'\]/); assert.doesNotMatch(schemaPhp, /'key' => 'wechat', 'title' => '微信'/); }); test('sortable 持久化保留提交键顺序', () => { const start = provider.indexOf("if ($type === 'sortable') {"); const end = provider.indexOf("if ($type === 'link') {", start); assert.ok(start >= 0 && end > start, 'SchemaProvider 应存在 sortable 分支'); const block = provider.slice(start, end); const resultLoopStart = block.indexOf('foreach ($value as $item_key'); assert.ok(resultLoopStart > 0, '结果必须按提交顺序(遍历 $value)构建'); const definitionsLoop = block.slice(0, resultLoopStart); const resultLoop = block.slice(resultLoopStart); // items 定义只用于白名单校验,不得据此产出结果键顺序。 assert.doesNotMatch(definitionsLoop, /\$items\[/); assert.match(definitionsLoop, /\$definitions\[\$item_key\] = \$item_schema;/, 'items 定义应收敛为白名单'); // 键顺序即排序值:按提交顺序写入结果,且丢弃未定义的槽位键。 assert.match(resultLoop, /!isset\(\$definitions\[\$item_key\]\)/); assert.match(resultLoop, /\$items\[\$item_key\] = \$this->sanitize_object_fields/); }); test('排序只有一份实现,无调用方的重复实现已移除', () => { assert.match(pay, /if \(!function_exists\('aksrt_sort_payment_methods'\)\)/); assert.match(pay, /function aksrt_sort_payment_methods\(array \$methods\): array/); assert.match(pay, /\$configured = is_array\(\$settings\) \? \(\$settings\['pay_sdk_order'\] \?\? \[\]\) : \[\];/); // 槽位映射与后台 sortable 项一一对应,易支付子通道随所属渠道槽位。 assert.match(pay, /'wechat'\s+=> \['wechatpay', 'epay_wxpay'\]/); assert.match(pay, /'alipay'\s+=> \['alipay', 'epay_alipay'\]/); assert.match(pay, /'card_pass' => \['card_pass'\]/); // 旧的无调用方实现(第二套排序逻辑)已删除。 assert.doesNotMatch(gateway, /function get_available\(/); assert.doesNotMatch(gateway, /function apply_sdk_order\(/); assert.doesNotMatch(gateway, /function get_status_summary\(/); }); test('基础渠道列表与卡密渠道都经过同一排序实现', () => { // 内容收银台 / 会员购买 / 余额充值共用基础列表。 assert.match(pay, /return aksrt_sort_payment_methods\(\$methods\);/); assert.match(balanceCharge, /aksrt_sort_payment_methods\(\$methods\)/); assert.match(vipPackages, /aksrt_sort_payment_methods\(\$methods\)/); // 购买积分弹窗自建余额 + 卡密列表,同样排序。 assert.match(pointsRenderer, /aksrt_sort_payment_methods\(\$points_methods\)/); // 卡密输入盒是 method 条件块,位置无关:卡密渠道可被移到任意槽位。 assert.match(wallet, /in_array\('card_pass', array_column\(\$methods, 'id'\), true\)/); }); test('会员弹窗不再保留重复的支付方式列表回退分支', () => { assert.doesNotMatch(vipPackages, /class_exists\('\\\\AKSRT\\\\Payment\\\\Gateway'\)/); // 卡密渠道注入契约保持(对齐参考主题 zibpay_get_payment_methods)。 assert.match(vipPackages, /'id' => 'card_pass', 'name' => __\('卡密', 'aksrt'\)/); }); test('未被引用的后台 schema 默认值死模块不回潮', () => { assert.equal( fs.existsSync(path.join(root, 'assets', 'admin', 'src', 'constants', 'settings-defaults.js')), false, 'settings-defaults.js 无任何引用方,应保持删除' ); const schemaIndex = read('assets', 'admin', 'src', 'schema', 'index.js'); assert.doesNotMatch(schemaIndex, /getSchemaDefaults|getSchemaSectionKeys|getDomainSchemas|CANONICAL_SCHEMA_DOMAINS/); });