diff --git a/.gitignore b/.gitignore
index 41c9e9b2..b171dc60 100644
--- a/.gitignore
+++ b/.gitignore
@@ -59,6 +59,7 @@
!/tests/widget-system/card-pass-universal-redeem.test.cjs
!/tests/widget-system/card-pass-modal-redeem.test.cjs
!/tests/widget-system/balance-charge-modal.test.cjs
+!/tests/widget-system/pay-sdk-order-contract.test.cjs
!/tests/admin/
/tests/admin/*
!/tests/admin/charge-cards-page-contract.test.cjs
diff --git a/assets/admin/src/components/form/AksField.vue b/assets/admin/src/components/form/AksField.vue
index 44328092..03d805cc 100644
--- a/assets/admin/src/components/form/AksField.vue
+++ b/assets/admin/src/components/form/AksField.vue
@@ -498,8 +498,8 @@
↓
-
@@ -1365,7 +1365,6 @@ function confirmRepeaterModal() {
justify-content: space-between;
padding: 10px 12px;
background: var(--aksrt-color-canvas);
- cursor: move;
}
.aksrt-field__sortable-body {
diff --git a/assets/admin/src/constants/settings-defaults.js b/assets/admin/src/constants/settings-defaults.js
deleted file mode 100644
index 9c4b5b3c..00000000
--- a/assets/admin/src/constants/settings-defaults.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { ALL_SCHEMAS, CANONICAL_SCHEMA_DOMAINS, normalizeSchemaDomain } from '@/schema/index.js'
-
-function cloneDefaults(defaults = {}) {
- return JSON.parse(JSON.stringify(defaults))
-}
-
-export function getSectionDefaults(domain, section) {
- const domainKey = normalizeSchemaDomain(domain)
- const sectionSchema = ALL_SCHEMAS[domainKey]?.[section]
- return cloneDefaults(sectionSchema?.defaults || {})
-}
-
-export function getDomainDefaults(domain) {
- const domainKey = normalizeSchemaDomain(domain)
- const sections = Object.values(ALL_SCHEMAS[domainKey] || {})
-
- return sections.reduce((defaults, schema) => {
- return {
- ...defaults,
- ...cloneDefaults(schema?.defaults || {}),
- }
- }, {})
-}
-
-export const SETTINGS_DEFAULTS_BY_DOMAIN = CANONICAL_SCHEMA_DOMAINS.reduce((defaults, domain) => {
- defaults[domain] = getDomainDefaults(domain)
- return defaults
-}, {})
diff --git a/assets/admin/src/schema/index.js b/assets/admin/src/schema/index.js
index 0ae8cc5d..d1121072 100644
--- a/assets/admin/src/schema/index.js
+++ b/assets/admin/src/schema/index.js
@@ -43,17 +43,6 @@ export function normalizeSchemaDomain(domain = '') {
return value.replace(/-/g, '_')
}
-export const CANONICAL_SCHEMA_DOMAINS = [
- 'global',
- 'page_display',
- 'post_list',
- 'permissions',
- 'users',
- 'payment',
- 'extensions',
- 'tools',
-]
-
/** All schemas organized by domain */
export const ALL_SCHEMAS = Object.freeze({
global: globalSchemas,
@@ -77,31 +66,3 @@ export function getSchema(domain, section) {
if (!domainSchemas) return null
return domainSchemas[section] || null
}
-
-/**
- * Get all schemas for a domain.
- * @param {string} domain
- * @returns {Object}
- */
-export function getDomainSchemas(domain) {
- return ALL_SCHEMAS[normalizeSchemaDomain(domain)] || {}
-}
-
-export function getSchemaSectionKeys(domain) {
- return Object.keys(getDomainSchemas(domain))
-}
-
-export function getSchemaDefaults(domain, section = '') {
- const domainKey = normalizeSchemaDomain(domain)
-
- if (section) {
- return { ...(getSchema(domainKey, section)?.defaults || {}) }
- }
-
- return Object.values(getDomainSchemas(domainKey)).reduce((defaults, schema) => {
- return {
- ...defaults,
- ...(schema?.defaults || {}),
- }
- }, {})
-}
diff --git a/assets/admin/src/schema/sections/payment/gateway.js b/assets/admin/src/schema/sections/payment/gateway.js
index 86fca069..c0dcb190 100644
--- a/assets/admin/src/schema/sections/payment/gateway.js
+++ b/assets/admin/src/schema/sections/payment/gateway.js
@@ -187,12 +187,12 @@ export const gatewaySchema = {
type: 'sortable',
default: {},
items: [
- { key: 'wechat', title: '微信', fields: [] },
- { key: 'alipay', title: '支付宝', fields: [] },
- { key: 'paypal', title: 'PayPal(贝宝)', fields: [] },
- { key: 'stripe', title: 'Stripe', fields: [] },
- { key: 'balance', title: '余额', fields: [] },
- { key: 'card_pass', title: '卡密', fields: [] },
+ { key: 'wechat', label: '微信' },
+ { key: 'alipay', label: '支付宝' },
+ { key: 'paypal', label: 'PayPal(贝宝)' },
+ { key: 'stripe', label: 'Stripe' },
+ { key: 'balance', label: '余额' },
+ { key: 'card_pass', label: '卡密' },
],
help: '收银台支付方式显示顺序;仅显示已开启的接口',
},
diff --git a/inc/helpers/aksrt-pay.php b/inc/helpers/aksrt-pay.php
index 223cfdf7..b8195d9e 100644
--- a/inc/helpers/aksrt-pay.php
+++ b/inc/helpers/aksrt-pay.php
@@ -136,7 +136,76 @@ if (!function_exists('aksrt_list_cash_payment_methods')) {
// Do not inject a fake balance/alipay option when nothing is configured —
// callers should surface “暂无可用支付方式” instead of a dead method.
- return $methods;
+ return aksrt_sort_payment_methods($methods);
+ }
+}
+
+/**
+ * 「收款接口显示顺序」槽位 → 网关 id 映射。
+ *
+ * 后台 sortable 项的 key 与之一一对应;微信/支付宝槽位同时收纳易支付子通道。
+ *
+ * @return array>
+ */
+if (!function_exists('aksrt_payment_order_slots')) {
+ function aksrt_payment_order_slots(): array {
+ return [
+ 'wechat' => ['wechatpay', 'epay_wxpay'],
+ 'alipay' => ['alipay', 'epay_alipay'],
+ 'paypal' => ['paypal'],
+ 'stripe' => ['stripe'],
+ 'balance' => ['balance'],
+ 'card_pass' => ['card_pass'],
+ ];
+ }
+}
+
+/**
+ * 按后台「收款接口显示顺序」(pay_sdk_order)重排支付方式列表。
+ *
+ * 语义对齐参考主题 zibpay_get_payment_methods():按配置槽位顺序把命中的方式
+ * 依次提到前面,未纳入槽位的方式保持原有相对顺序并留在末尾;未配置顺序时
+ * 原样返回(即保持注册顺序)。这是排序的唯一实现,所有收银台构建支付方式
+ * 列表的位置都必须经由此函数。
+ *
+ * @param array $methods
+ * @return array
+ */
+if (!function_exists('aksrt_sort_payment_methods')) {
+ function aksrt_sort_payment_methods(array $methods): array {
+ if (!$methods) {
+ return $methods;
+ }
+ $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
+ $configured = is_array($settings) ? ($settings['pay_sdk_order'] ?? []) : [];
+ if (!is_array($configured) || $configured === []) {
+ return $methods;
+ }
+
+ $slots = aksrt_payment_order_slots();
+ $remaining = $methods;
+ $sorted = [];
+ foreach (array_keys($configured) as $slot) {
+ $slot = sanitize_key((string) $slot);
+ if ($slot === '') {
+ continue;
+ }
+ foreach ($slots[$slot] ?? [] as $gateway_id) {
+ foreach ($remaining as $index => $method) {
+ if ((string) ($method['id'] ?? '') !== $gateway_id) {
+ continue;
+ }
+ $sorted[] = $method;
+ unset($remaining[$index]);
+ break;
+ }
+ }
+ }
+ foreach ($remaining as $method) {
+ $sorted[] = $method;
+ }
+
+ return $sorted;
}
}
diff --git a/inc/helpers/user-center-renderers/credits-balance.php b/inc/helpers/user-center-renderers/credits-balance.php
index 2feeb163..57c13cd4 100644
--- a/inc/helpers/user-center-renderers/credits-balance.php
+++ b/inc/helpers/user-center-renderers/credits-balance.php
@@ -73,6 +73,10 @@ $currency = (string) ($currency_name ?? __('积分', 'aksrt'));
if (!empty($points_card_pass['enabled'])) {
$points_methods[] = ['id' => 'card_pass', 'name' => __('卡密', 'aksrt')];
}
+ // 「收款接口显示顺序」:购买积分弹窗只含余额与卡密两个槽位。
+ if (function_exists('aksrt_sort_payment_methods')) {
+ $points_methods = aksrt_sort_payment_methods($points_methods);
+ }
$points_custom_limit_html = sprintf(__('最低购买%d积分,最高购买%d积分', 'aksrt'), $pb_min_points, $pb_daily_max_points);
$points_gateway = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_payment_gateway_settings() : [];
$points_mark = (string) ($points_gateway['currency']['mark'] ?? '¥');
diff --git a/inc/helpers/user-center-renderers/credits-wallet.php b/inc/helpers/user-center-renderers/credits-wallet.php
index a71c5ee0..a026f616 100644
--- a/inc/helpers/user-center-renderers/credits-wallet.php
+++ b/inc/helpers/user-center-renderers/credits-wallet.php
@@ -116,7 +116,9 @@ $charge_img_uri = get_template_directory_uri() . '/assets/img/';
$charge_default_product = $packages ? (string) $packages[0]['key'] : 'custom';
// 仅卡密可用时隐藏金额选择(参考主题:卡密决定充值金额)。
$charge_card_only = count($methods) === 1 && (string) ($methods[0]['id'] ?? '') === 'card_pass';
-$charge_card_in_methods = count($methods) > 0 && $methods[count($methods) - 1]['id'] === 'card_pass';
+// 卡密输入盒是 payment_method 条件块,与它在渠道行中的位置无关:卡密渠道可由
+// 「收款接口显示顺序」移到任意位置,故此处只判断渠道是否存在。
+$charge_card_in_methods = in_array('card_pass', array_column($methods, 'id'), true);
$charge_mark_symbol = !in_array($mark, ['', '元'], true);
$charge_mark_html = $charge_mark_symbol
? '' . esc_html($mark) . ''
diff --git a/src/Admin/Schema/SchemaPayment.php b/src/Admin/Schema/SchemaPayment.php
index 25856197..a611d612 100644
--- a/src/Admin/Schema/SchemaPayment.php
+++ b/src/Admin/Schema/SchemaPayment.php
@@ -414,12 +414,12 @@ class SchemaPayment {
]],
['key' => 'sdk-order', 'title' => '收款接口显示顺序', 'fields' => [
['key' => 'pay_sdk_order', 'label' => '显示顺序', 'type' => 'sortable', 'default' => [], 'items' => [
- ['key' => 'wechat', 'title' => '微信', 'fields' => []],
- ['key' => 'alipay', 'title' => '支付宝', 'fields' => []],
- ['key' => 'paypal', 'title' => 'PayPal(贝宝)', 'fields' => []],
- ['key' => 'stripe', 'title' => 'Stripe', 'fields' => []],
- ['key' => 'balance', 'title' => '余额', 'fields' => []],
- ['key' => 'card_pass', 'title' => '卡密', 'fields' => []],
+ ['key' => 'wechat', 'label' => '微信'],
+ ['key' => 'alipay', 'label' => '支付宝'],
+ ['key' => 'paypal', 'label' => 'PayPal(贝宝)'],
+ ['key' => 'stripe', 'label' => 'Stripe'],
+ ['key' => 'balance', 'label' => '余额'],
+ ['key' => 'card_pass', 'label' => '卡密'],
], 'help' => '收银台支付方式显示顺序;仅显示已开启的接口'],
]],
['key' => 'alipay', 'title' => '支付宝官方', 'collapsible' => true, 'expandWhen' => 'pay_alipay_sdk_options', 'dependsOn' => ['key' => 'pay_alipay_sdk_options', 'value' => 'official_alipay'], 'fields' => [
diff --git a/src/Admin/SchemaProvider.php b/src/Admin/SchemaProvider.php
index 7bd633d7..67c3b32b 100644
--- a/src/Admin/SchemaProvider.php
+++ b/src/Admin/SchemaProvider.php
@@ -942,12 +942,25 @@ class SchemaProvider {
if (!is_array($value)) {
return [];
}
- $items = [];
+ $definitions = [];
foreach ((array) ($field['items'] ?? []) as $item_schema) {
- $item_key = sanitize_key((string) ($item_schema['key'] ?? ''));
- if ($item_key !== '' && array_key_exists($item_key, $value) && is_array($value[$item_key])) {
- $items[$item_key] = $this->sanitize_object_fields($value[$item_key], (array) ($item_schema['fields'] ?? []));
+ if (!is_array($item_schema)) {
+ continue;
}
+ $item_key = sanitize_key((string) ($item_schema['key'] ?? ''));
+ if ($item_key !== '') {
+ $definitions[$item_key] = $item_schema;
+ }
+ }
+ // 键顺序即配置值本身:必须按提交顺序重建。若改为遍历 items
+ // 定义,用户排序会被静默改写回 schema 静态顺序(等于排序失效)。
+ $items = [];
+ foreach ($value as $item_key => $item_value) {
+ $item_key = sanitize_key((string) $item_key);
+ if ($item_key === '' || !isset($definitions[$item_key]) || !is_array($item_value)) {
+ continue;
+ }
+ $items[$item_key] = $this->sanitize_object_fields($item_value, (array) ($definitions[$item_key]['fields'] ?? []));
}
return $items;
}
diff --git a/src/Content/VipServicePackages.php b/src/Content/VipServicePackages.php
index 6ba4260e..c8d3a531 100644
--- a/src/Content/VipServicePackages.php
+++ b/src/Content/VipServicePackages.php
@@ -517,28 +517,9 @@ trait VipServicePackages {
* @return array
*/
private function available_payment_methods(): array {
- $methods = [];
- if (function_exists('aksrt_list_cash_payment_methods')) {
- $methods = aksrt_list_cash_payment_methods(true);
- } else {
- if (!class_exists('\AKSRT\Payment\Gateway')) {
- return [];
- }
- $gw = \AKSRT\Payment\Gateway::get_instance();
- foreach (['balance', 'alipay', 'wechatpay', 'paypal', 'stripe', 'epay_alipay', 'epay_wxpay'] as $id) {
- $g = $gw->get_gateway($id);
- if (!$g) {
- continue;
- }
- if (method_exists($g, 'is_configured') && !$g->is_configured()) {
- continue;
- }
- $methods[] = [
- 'id' => $id,
- 'name' => method_exists($g, 'get_name') ? (string) $g->get_name() : $id,
- ];
- }
- }
+ $methods = function_exists('aksrt_list_cash_payment_methods')
+ ? aksrt_list_cash_payment_methods(true)
+ : [];
// 卡密兑换会员渠道(后台 会员购买 → 启用卡密兑换会员功能):
// 提交走 /pay/card-pass/redeem type=vip,不创建支付订单。
@@ -546,7 +527,10 @@ trait VipServicePackages {
if (!empty($card_pass['enabled'])) {
$methods[] = ['id' => 'card_pass', 'name' => __('卡密', 'aksrt')];
}
- return $methods;
+ // 卡密渠道追加后需按「收款接口显示顺序」重新定位(含 card_pass 槽位)。
+ return function_exists('aksrt_sort_payment_methods')
+ ? aksrt_sort_payment_methods($methods)
+ : $methods;
}
/**
diff --git a/src/Credits/BalanceServiceCharge.php b/src/Credits/BalanceServiceCharge.php
index 55493af6..e0b379f4 100644
--- a/src/Credits/BalanceServiceCharge.php
+++ b/src/Credits/BalanceServiceCharge.php
@@ -161,6 +161,10 @@ trait BalanceServiceCharge {
if ($card_pass_on) {
$methods[] = ['id' => 'card_pass', 'name' => __('卡密', 'aksrt')];
}
+ // 卡密渠道追加后需按「收款接口显示顺序」重新定位(含 card_pass 槽位)。
+ if (function_exists('aksrt_sort_payment_methods')) {
+ $methods = aksrt_sort_payment_methods($methods);
+ }
return [
'enabled' => !empty($cfg['enabled']),
'desc' => (string) ($cfg['desc'] ?? ''),
diff --git a/src/Payment/Gateway.php b/src/Payment/Gateway.php
index 6382f6bb..b88907c1 100644
--- a/src/Payment/Gateway.php
+++ b/src/Payment/Gateway.php
@@ -44,76 +44,6 @@ class Gateway {
return $this->gateways;
}
- /**
- * 获取可用网关列表(已配置的),按「收款接口显示顺序」配置排序。
- */
- public function get_available(): array {
- $available = [];
- foreach ($this->gateways as $id => $gateway) {
- if ($gateway->is_configured()) {
- $available[$id] = $gateway;
- }
- }
- return $this->apply_sdk_order($available);
- }
-
- /**
- * 按 pay_sdk_order(收款接口显示顺序)重排可用网关。
- * 易支付子通道跟随对应渠道槽位(epay_wxpay 跟随 wechat、epay_alipay 跟随 alipay);
- * 未配置顺序时保持注册顺序,未纳入槽位的扩展网关保持在末尾。
- *
- * @param array $available
- * @return array
- */
- private function apply_sdk_order(array $available): array {
- $settings = \AKSRT\Settings\Settings::get_all();
- $configured = (array) ($settings['pay_sdk_order'] ?? []);
- $order = array_values(array_filter(array_map('sanitize_key', array_keys($configured))));
- if (!$order) {
- return $available;
- }
-
- $slot_map = [
- 'wechat' => ['wechatpay', 'epay_wxpay'],
- 'alipay' => ['alipay', 'epay_alipay'],
- 'paypal' => ['paypal'],
- 'stripe' => ['stripe'],
- 'balance' => ['balance'],
- 'card_pass' => ['card_pass'],
- ];
-
- $sorted = [];
- foreach ($order as $slot) {
- foreach (($slot_map[$slot] ?? []) as $id) {
- if (isset($available[$id]) && !isset($sorted[$id])) {
- $sorted[$id] = $available[$id];
- }
- }
- }
- foreach ($available as $id => $gateway) {
- if (!isset($sorted[$id])) {
- $sorted[$id] = $gateway;
- }
- }
- return $sorted;
- }
-
- /**
- * 获取网关状态摘要(用于后台展示)
- */
- public function get_status_summary(): array {
- $summary = [];
- foreach ($this->gateways as $id => $gateway) {
- $summary[] = [
- 'id' => $id,
- 'name' => $gateway->get_name(),
- 'configured' => $gateway->is_configured(),
- 'available' => $gateway->is_configured(),
- ];
- }
- return $summary;
- }
-
/**
* 绑定支付回调路由。必须在 functions.php 中初始化。
*
diff --git a/tests/widget-system/pay-sdk-order-contract.test.cjs b/tests/widget-system/pay-sdk-order-contract.test.cjs
new file mode 100644
index 00000000..d156bb55
--- /dev/null
+++ b/tests/widget-system/pay-sdk-order-contract.test.cjs
@@ -0,0 +1,103 @@
+/**
+ * 「收款接口显示顺序」契约(后台 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/);
+});