后台「收款接口 → 收款接口显示顺序」有三处缺陷叠加: 1) sortable 渲染组件读取 item.label,而 schema 写的是 title,导致 6 行 只剩上下箭头; 2) SchemaProvider 的 sortable 分支遍历 items 定义重建数组,而该字段的 键顺序本身就是配置值,用户排序被静默改回 schema 静态顺序; 3) 真正构建收银台支付方式列表的 aksrt_list_cash_payment_methods() 使用 硬编码顺序,Gateway::get_available()/apply_sdk_order() 那套排序逻辑 无任何调用方,配置对前台毫无影响。 改为:schema 项统一使用 label(JS 与 PHP 两处对齐);sortable 持久化按 提交键顺序重建,items 定义仅作白名单;新增唯一排序实现 aksrt_sort_payment_methods()(语义对齐参考主题 zibpay_get_payment_methods + pay_sdk_order 键顺序),并接入内容收银台、余额充值、购买积分与会员 购买四个入口。卡密输入盒改为按渠道存在与否判断,不再假定卡密固定在末位。 清理死代码:无调用方的 Gateway::get_available/apply_sdk_order/ get_status_summary、VipServicePackages 中不可达的支付方式列表回退分支、 零引用的 settings-defaults 模块及 schema/index.js 未使用的导出、支付 sortable 的 fields 死元数据、无拖拽实现却声明的 cursor:move。 未配置 pay_sdk_order 时顺序保持不变;未写数据迁移。 验证:php -l 8 个改动文件全部通过;新增 pay-sdk-order-contract 契约测试 (6 项);node --test tests/widget-system/*.test.cjs 301 项全部通过; 后台 SPA 已重新构建。
104 lines
5.7 KiB
JavaScript
104 lines
5.7 KiB
JavaScript
/**
|
|
* 「收款接口显示顺序」契约(后台 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/);
|
|
});
|