fix(vip): 会员弹窗支付方式渠道对齐 Zibll initiate_pay_input 组件

支付方式 UI 不再自实现(uc-vip-shop__method 文本 radio),改为与其他
两个弹窗(充值/积分)完全同源的 Zibll zibpay_get_initiate_pay_input
组件结构:

- 支付方式行:.payment-method-radio.hollow-radio + logo 图 +
  data-for/data-value 选中模型(多渠道时展示「请选择支付方式」)
- 我的余额盒:data-controller == balance 条件显隐,展示货币符号与余额
  (视图模型透传 wallet_balance / currency_mark)
- 卡密盒:Zibll password_box 同构(mb10 padding-h10 padding-w6 +
  desc muted-box + form-control 卡号/卡密输入)
- hidden payment_method + initiate-pay 按钮类(mt6 but jb-red
  btn-block radius)
- 服务端 payment_method_logo_url() 统一映射 pay-*-logo.svg
- 显隐统一走 data-controller(==/!=)通用匹配,卡密选中隐藏 .charge-box
- 清理 uc-vip-shop__method* 死样式与旧 change 监听;契约测试同步锁定
  (.gitignore 白名单补 card-pass-modal-redeem.test.cjs)
This commit is contained in:
2026-09-18 12:44:38 +08:00
parent e72d0710e6
commit 721fa29b8d
5 changed files with 298 additions and 62 deletions
+1
View File
@@ -57,6 +57,7 @@
!/tests/widget-system/single-cover-height-contract.test.cjs
!/tests/widget-system/frontend-lifecycle-contract.test.cjs
!/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/admin/
/tests/admin/*
-21
View File
@@ -765,27 +765,6 @@ body.uc-modal-open {
font-weight: 600;
color: #b45309;
}
.uc-vip-shop__methods {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 12px;
margin-top: 8px;
padding-top: 12px;
border-top: 1px solid var(--color-line-soft, rgba(0,0,0,0.06));
}
.uc-vip-shop__methods-label {
font-size: 12px;
font-weight: 600;
color: var(--color-ink-3, #6b7280);
}
.uc-vip-shop__method {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
cursor: pointer;
}
.uc-vip-shop__tip {
margin: 10px 0 0;
font-size: 12px;
+78 -41
View File
@@ -102,43 +102,64 @@
append(parent, wrap);
}
function renderMethods(parent, methods, cardPass) {
function renderMethods(parent, methods, cardPass, wallet) {
methods = Array.isArray(methods) ? methods : [];
if (!methods.length) {
append(parent, node("div", "muted-2-color text-center padding-10", "暂无可用支付方式"));
return;
}
if (methods.length === 1) {
var one = node("input");
one.type = "hidden";
one.name = "aksrt_vip_pay_method";
one.value = text(methods[0].id || "balance");
one.setAttribute("data-aksrt-vip-method-fixed", "");
append(parent, one);
} else {
var wrap = node("div", "uc-vip-shop__methods mb10");
wrap.setAttribute("data-aksrt-vip-methods", "");
append(wrap, node("span", "uc-vip-shop__methods-label", "支付方式"));
wallet = wallet || {};
var defaultMethod = text(methods[0].id || "balance");
// 支付方式选择行(Zibll zibpay_get_initiate_pay_input 同构:
// .payment-method-radio.hollow-radio + logo 图 + data-for 选中模型;
// 与余额充值/购买积分弹窗完全一致的组件)。
if (methods.length > 1) {
append(parent, node("div", "muted-2-color em09 mb6", "请选择支付方式"));
var row = node("div", "flex mb10");
methods.forEach(function (method, index) {
var label = node("label", "uc-vip-shop__method");
var input = node("input");
input.type = "radio";
input.name = "aksrt_vip_pay_method";
input.value = text(method.id || "balance");
input.checked = index === 0;
append(label, input);
append(label, node("span", "", method.name || method.id || ""));
append(wrap, label);
var id = text(method.id || "balance");
var item = node("div", "flex jc hh payment-method-radio hollow-radio flex-auto pointer" + (index === 0 ? " active" : ""));
item.setAttribute("data-for", "payment_method");
item.setAttribute("data-value", id);
if (method.logo) {
var img = node("img");
img.src = text(method.logo);
img.alt = id + "-logo";
img.decoding = "async";
append(item, img);
}
append(item, node("div", "", method.name || id || ""));
append(row, item);
});
append(parent, wrap);
append(parent, row);
}
// 卡密渠道输入盒(契约与参考主题一致:data-controller="payment_method")。
// 我的余额盒(Zibll user_balance_box 同构:选中余额渠道时显示)。
if (methods.some(function (m) { return text(m.id) === "balance"; })) {
var balanceBox = node("div", "mb10 muted-box padding-h10");
balanceBox.setAttribute("data-controller", "payment_method");
balanceBox.setAttribute("data-condition", "==");
balanceBox.setAttribute("data-value", "balance");
if (defaultMethod !== "balance") balanceBox.style.display = "none";
var balanceRow = node("div", "flex jsb ac");
append(balanceRow, node("span", "muted-2-color", "我的余额"));
var balanceValue = node("div", "c-blue-2");
append(balanceValue, node("span", "px12 mr3", wallet.mark || "¥"));
append(balanceValue, node("span", "em14", text(wallet.balance || "0.00")));
append(balanceRow, balanceValue);
append(balanceBox, balanceRow);
append(parent, balanceBox);
}
// 卡密盒(Zibll password_box 同构:mb10 padding-h10 padding-w6 +
// data-controller="payment_method" 契约 + form-control 输入)。
if (cardPass) {
var box = node("div", "uc-vip-shop__card-pass mb10");
var box = node("div", "mb10 padding-h10 padding-w6");
box.setAttribute("data-controller", "payment_method");
box.setAttribute("data-condition", "==");
box.setAttribute("data-value", "card_pass");
box.style.display = "none";
if (defaultMethod !== "card_pass") box.style.display = "none";
if (cardPass.desc) append(box, node("div", "muted-box muted-2-color padding-10 mb10 em09", cardPass.desc));
append(box, node("div", "muted-2-color em09 mb6", cardPass.only_password ? "请输入卡密" : "请输入卡号和密码"));
if (!cardPass.only_password) {
@@ -157,15 +178,26 @@
append(box, pwInput);
append(parent, box);
}
var hidden = node("input");
hidden.type = "hidden";
hidden.name = "payment_method";
hidden.value = defaultMethod;
append(parent, hidden);
}
// 卡密渠道显隐:选中 card_pass 时隐藏套餐(.charge-box)、显示卡密输入盒。
// 渠道显隐:通用 data-controller 契约(==/!= 匹配当前 payment_method);
// 选中 card_pass 时隐藏套餐(.charge-box),与参考主题 pay.js 行为一致。
function applyVipMethodVisibility(root) {
var pane = root.closest ? root.closest(".aksrt-vip-pane") : null;
pane = pane || root;
var value = selectedMethod(pane);
var cardBox = pane.querySelector('[data-controller="payment_method"][data-value="card_pass"]');
if (cardBox) cardBox.style.display = value === "card_pass" ? "" : "none";
pane.querySelectorAll('[data-controller="payment_method"]').forEach(function (el) {
var condition = el.getAttribute("data-condition") || "==";
var target = el.getAttribute("data-value") || "";
var match = condition === "!=" ? value !== target : value === target;
el.style.display = match ? "" : "none";
});
pane.querySelectorAll(".charge-box").forEach(function (el) {
el.style.display = value === "card_pass" ? "none" : "";
});
@@ -286,6 +318,7 @@
var groups = Array.isArray(catalog.level_packages) ? catalog.level_packages : [];
var methods = Array.isArray(view.payment_methods) ? view.payment_methods : [];
var cardPass = view.card_pass && view.card_pass.enabled ? view.card_pass : null;
var wallet = { balance: text(catalog.wallet_balance || "0.00"), mark: text(view.currency_mark || "¥") };
var allowed = view.allowed_actions || {};
var reasons = view.action_reasons || {};
var modal = node("div", "box-body payvip-modal");
@@ -365,12 +398,12 @@
var products = node("div", "pay-vip-products");
renderProducts(products, group.items);
renderMethods(products, methods);
renderMethods(products, methods, cardPass, wallet);
var actionName = actionKey(action);
var actionAllowed = allowed[actionName] === true;
var reason = actionReason(reasons[actionName]);
var canPurchase = actionAllowed && methods.length > 0;
var submit = node("button", "but jb-red btn-block padding-lg radius aksrt-vip-submit", canPurchase ? (group.items[0].cta_label || actionLabel(action)) : (reason || "暂无可用支付方式"));
var submit = node("button", "mt6 but jb-red initiate-pay btn-block radius aksrt-vip-submit", canPurchase ? (group.items[0].cta_label || actionLabel(action)) : (reason || "暂无可用支付方式"));
submit.type = "button";
submit.disabled = !canPurchase;
submit.setAttribute("data-vip-submit", "");
@@ -401,12 +434,10 @@
}
function selectedMethod(root) {
var fixed = root.querySelector("[data-aksrt-vip-method-fixed]");
if (fixed && fixed.value) return fixed.value;
var checked = root.querySelector('input[name="aksrt_vip_pay_method"]:checked');
if (checked) return checked.value;
var first = root.querySelector('input[name="aksrt_vip_pay_method"]');
return first ? first.value : "balance";
var hidden = root.querySelector('input[name="payment_method"]');
if (hidden && hidden.value) return hidden.value;
var first = root.querySelector('[data-for="payment_method"]');
return first ? first.getAttribute("data-value") : "balance";
}
function selectedPackage(root) {
@@ -538,12 +569,18 @@
function bindModalUi(root) {
if (!root || root.dataset.aksrtVipBound === "1") return;
root.dataset.aksrtVipBound = "1";
root.addEventListener("change", function (event) {
var input = event.target.closest ? event.target.closest('input[name="aksrt_vip_pay_method"]') : null;
if (!input) return;
applyVipMethodVisibility(input);
});
root.addEventListener("click", function (event) {
var methodItem = event.target.closest ? event.target.closest('[data-for="payment_method"]') : null;
if (methodItem) {
var pane = methodItem.closest(".aksrt-vip-pane") || root;
pane.querySelectorAll('[data-for="payment_method"]').forEach(function (el) {
el.classList.toggle("active", el === methodItem);
});
var hidden = pane.querySelector('input[name="payment_method"]');
if (hidden) hidden.value = methodItem.getAttribute("data-value") || "";
applyVipMethodVisibility(pane);
return;
}
var tab = event.target.closest("[data-vip-tab]");
if (tab) {
event.preventDefault();
+29
View File
@@ -64,6 +64,15 @@ trait VipServiceModal {
}
$points_exchange = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : [];
$methods = is_array($catalog['payment_methods'] ?? null) ? $catalog['payment_methods'] : [];
// 支付方式 logo(与充值/积分弹窗的 payment-method-radio 组件同源)。
$methods = array_map(function ($m) {
$id = sanitize_key((string) ($m['id'] ?? ''));
return [
'id' => $id,
'name' => (string) ($m['name'] ?? $id),
'logo' => $this->payment_method_logo_url($id),
];
}, $methods);
$card_pass = is_array($catalog['card_pass'] ?? null)
? array_merge(['enabled' => false, 'desc' => '', 'only_password' => false], $catalog['card_pass'])
: ['enabled' => false, 'desc' => '', 'only_password' => false];
@@ -111,6 +120,7 @@ trait VipServiceModal {
'currency_name' => (string) ($catalog['currency_name'] ?? ''),
'level_packages' => $groups,
'points_exchange' => $points_exchange,
'wallet_balance' => (string) ($catalog['wallet_balance'] ?? '0.00'),
'vip_url' => (string) ($catalog['vip_url'] ?? ''),
],
'member' => [
@@ -128,6 +138,7 @@ trait VipServiceModal {
'levels' => $level_view,
'payment_methods' => $methods,
'card_pass' => $card_pass,
'currency_mark' => (string) (\AKSRT\Settings\Settings::get_payment_gateway_settings()['currency']['mark'] ?? '¥'),
'allowed_actions' => [
'purchase' => $package_ids !== [] && $methods !== [],
'renew' => $has_renew && $methods !== [],
@@ -146,6 +157,24 @@ trait VipServiceModal {
];
}
/**
* 支付方式 logo 映射(assets/img/pay-*-logo.svg,与充值/积分弹窗一致)。
*/
private function payment_method_logo_url(string $id): string {
$logos = [
'balance' => 'pay-balance-logo.svg',
'alipay' => 'pay-alipay-logo.svg',
'epay_alipay' => 'pay-alipay-logo.svg',
'wechatpay' => 'pay-wechat-logo.svg',
'epay_wxpay' => 'pay-wechat-logo.svg',
'paypal' => 'pay-paypal-logo.svg',
'stripe' => 'pay-stripe-logo.svg',
'card_pass' => 'pay-card-pass-logo.svg',
];
$file = $logos[$id] ?? '';
return $file === '' ? '' : esc_url_raw(get_template_directory_uri() . '/assets/img/' . $file);
}
/**
* Mini hover card for header (legacy aksrtpay_get_vip_card).
*/
@@ -0,0 +1,190 @@
/**
* 卡密兑换渠道契约(三个购买弹窗 · 1:1 对齐参考主题)。
*
* 背景:余额卡片顶部的独立「卡密兑换」通用弹窗已删除,兑换入口收敛到
* 三个购买弹窗的支付方式中(对齐参考主题 zibpay_get_payment_methods +
* zibpay_is_allow_card_pass_pay 过滤器):
* 1) 余额充值弹窗(credits-wallet.php,服务端渲染)
* 2) 购买积分弹窗(credits-balance.php,服务端渲染,Zibll points-pay modal 结构)
* 3) 会员购买/续费弹窗(VipServiceModal JSON 视图模型 + pay-vip.js 渲染)
*
* 后台语义:余额充值 → 卡密兑换 → 三个独立开关
* 启用卡密兑换余额功能 / 启用卡密兑换积分功能 / 启用卡密兑换会员功能
* 任一开关关闭后,对应前台弹窗不出现卡密兑换渠道收款接口。
*
* REST 契约:POST /pay/card-pass/redeem,type 必填(balance/vip/credits),
* 不再有「空类型按卡密自动识别」;核销订单金额恒为 0。
*/
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 wallet = read('inc', 'helpers', 'user-center-renderers', 'credits-wallet.php');
const creditsRenderer = read('inc', 'helpers', 'user-center-renderers', 'credits-balance.php');
const creditsJs = read('assets', 'js', 'credits.js');
const payVipJs = read('assets', 'js', 'pay-vip.js');
const chargeCard = read('src', 'Payment', 'ChargeCard.php');
const userOrders = read('src', 'Payment', 'UserOrders.php');
const vipPackages = read('src', 'Content', 'VipServicePackages.php');
const vipPurchase = read('src', 'Content', 'VipServicePurchase.php');
const vipModal = read('src', 'Content', 'VipServiceModal.php');
const schemaPhp = read('src', 'Admin', 'Schema', 'SchemaPayment.php');
const schemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'recharge.js');
const vipSchemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'vip.js');
test('独立入口与通用弹窗已删除', () => {
assert.equal(
fs.existsSync(path.join(root, 'inc', 'helpers', 'user-center-renderers', 'credits-vip.php')),
false,
'credits-vip.php 死文件应已删除'
);
// 通用卡密弹窗与 initCardPass 不回潮。
assert.doesNotMatch(wallet, /data-aksrt-card-pass-modal|data-aksrt-card-redeem/);
assert.doesNotMatch(creditsJs, /function initCardPass\(|initCardPass\(\)/);
assert.doesNotMatch(creditsRenderer, /data-aksrt-card-pass-modal|data-aksrt-card-redeem/);
// 卡密 logo 就位(三个弹窗共用)。
assert.equal(fs.existsSync(path.join(root, 'assets', 'img', 'pay-card-pass-logo.svg')), true);
});
test('余额充值弹窗:卡密渠道为支付方式之一,卡号/卡密输入按单密码模式渲染', () => {
assert.match(wallet, /'card_pass'\s+=>\s+'pay-card-pass-logo\.svg'/);
// 支付方式列表注入 card_pass(由 get_charge_view / get_public_options['balance'] 驱动)。
assert.match(wallet, /card_pass_redeem\['desc'\]/);
assert.match(wallet, /card_pass_redeem\['only_password'\]/);
// 卡密输入盒契约与参考主题一致。
assert.match(wallet, /data-controller="payment_method" data-condition="==" data-value="card_pass"/);
assert.match(wallet, /name="card_pass\[card\]"/);
assert.match(wallet, /name="card_pass\[password\]"/);
// 仅卡密可用时隐藏金额选择(参考主题语义)。
assert.match(wallet, /\$charge_card_only/);
assert.match(wallet, /\$charge_card_in_methods/);
});
test('购买积分弹窗:Zibll points-pay modal 结构 + 卡密渠道', () => {
// 商品/自定义选择 + 支付方式 + hidden + 立即支付。
assert.match(creditsRenderer, /data-aksrt-points-pay-modal/);
assert.match(creditsRenderer, /data-aksrt-points-pay-form/);
assert.match(creditsRenderer, /data-aksrt-points-pay-open/);
assert.doesNotMatch(creditsRenderer, /data-aksrt-points-exchange-(open|modal|form|amount|preview|close)/);
assert.match(creditsRenderer, /请选择需购买的积分|请输入需购买的积分数额/);
assert.match(creditsRenderer, /name="points_product"/);
assert.match(creditsRenderer, /name="points_custom"/);
assert.match(creditsRenderer, /name="payment_method"/);
assert.match(creditsRenderer, /立即支付/);
// 卡密渠道受「启用卡密兑换积分功能」控制。
assert.match(creditsRenderer, /points_card_pass\['enabled'\]/);
assert.match(creditsRenderer, /data-controller="payment_method" data-condition="==" data-value="card_pass"/);
assert.match(creditsRenderer, /name="card_pass\[password\]"/);
});
test('购买积分前端:商品/支付方式选中模型 + 余额/卡密提交分流', () => {
assert.match(creditsJs, /function initPointsPay\(\)/);
assert.match(creditsJs, /data-aksrt-points-pay-open/);
assert.match(creditsJs, /\[data-for="points_product"\]/);
// 卡密选中:隐藏 .charge-box、显示卡密输入盒(契约与参考主题一致)。
assert.match(creditsJs, /data-controller="payment_method"\]\[data-value="card_pass"/);
assert.match(creditsJs, /value === 'card_pass' \? 'none' : ''/);
// 余额支付 → /pay/balance/points-exchange(product + custom 契约)。
assert.match(creditsJs, /\/pay\/balance\/points-exchange/);
assert.match(creditsJs, /JSON\.stringify\(\{ product: pkg, custom: validCustom \? custom : '0' \}\)/);
// 卡密支付 → /pay/card-pass/redeem type=credits。
assert.match(creditsJs, /redeemCardPass\('credits'/);
// bootAssets 挂载新函数,无死调用。
assert.match(creditsJs, /function bootAssets\(\)[\s\S]*?initPointsPay\(\);/);
});
test('会员弹窗:卡密渠道注入与直兑分流(不创建支付订单)', () => {
// 后端:available_payment_methods 追加 card_pass + 视图模型透传。
assert.match(vipPackages, /'id' => 'card_pass', 'name' => __\('卡密', 'aksrt'\)/);
assert.match(vipPackages, /function vip_card_pass_context\(\)/);
assert.match(vipPackages, /vip_card_pass_context\(\)/);
assert.match(vipModal, /'card_pass' => \$card_pass/);
assert.match(vipModal, /'card_pass' => \$card_pass,/);
// 支付方式组件与其他两个弹窗同源:logo URL 服务端映射 + 余额/货币符号透传。
assert.match(vipModal, /function payment_method_logo_url\(string \$id\): string/);
assert.match(vipModal, /'card_pass'\s+=>\s+'pay-card-pass-logo\.svg'/);
assert.match(vipModal, /'wallet_balance' => \(string\) \(\$catalog\['wallet_balance'\]/);
assert.match(vipModal, /'currency_mark' =>/);
// 后端防御:purchase() 拒绝 card_pass(卡密直兑走 redeem)。
assert.match(vipPurchase, /aksrt_card_pass_redeem_required/);
// 前端:Zibll initiate_pay_input 同构组件——payment-method-radio logo 行、
// 我的余额盒(data-controller == balance)、卡密盒(password_box 类结构)、
// hidden payment_method、initiate-pay 按钮。
assert.match(payVipJs, /payment-method-radio hollow-radio flex-auto pointer/);
assert.match(payVipJs, /item\.setAttribute\("data-for", "payment_method"\)/);
assert.match(payVipJs, /mb10 muted-box padding-h10/);
assert.match(payVipJs, /"data-condition", "=="/);
assert.match(payVipJs, /"data-value", "balance"/);
assert.match(payVipJs, /mb10 padding-h10 padding-w6/);
assert.match(payVipJs, /"data-value", "card_pass"/);
assert.match(payVipJs, /hidden\.name = "payment_method"/);
assert.match(payVipJs, /mt6 but jb-red initiate-pay btn-block radius/);
assert.doesNotMatch(payVipJs, /uc-vip-shop__method|aksrt_vip_pay_method/);
// 显隐:通用 data-controller(==/!=)匹配 + 卡密选中隐藏套餐。
assert.match(payVipJs, /function applyVipMethodVisibility\(/);
assert.match(payVipJs, /querySelectorAll\('\[data-controller="payment_method"\]'\)/);
assert.match(payVipJs, /querySelectorAll\("\.charge-box"\)/);
// 提交分流:卡密直兑 /pay/card-pass/redeem type=vip。
assert.match(payVipJs, /function redeemVipCardPass\(/);
assert.match(payVipJs, /\/pay\/card-pass\/redeem/);
assert.match(payVipJs, /type: "vip"/);
assert.match(payVipJs, /view\.card_pass && view\.card_pass\.enabled/);
});
test('REST 契约:type 必填(balance/vip/credits),无空类型自动识别', () => {
assert.match(chargeCard, /'type' => \['type' => 'string', 'required' => true, 'enum' => \[self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS\]\]/);
assert.match(chargeCard, /卡密兑换类型无效/);
assert.match(chargeCard, /REDEEMABLE_TYPES = \[self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS\]/);
// 兼容层:redeem_for_user 仍保留 expected_type 形参(前端恒传明确类型)。
assert.match(chargeCard, /function redeem_for_user\(int \$user_id, string \$password, string \$card = '', string \$expected_type = ''/);
});
test('后端类型闸门:三个独立开关各约束自己的类型', () => {
assert.match(chargeCard, /'balance' => \['enabled' => false, 'desc' => '', 'only_password' => false\]/);
assert.match(chargeCard, /'credits' => \['enabled' => false, 'desc' => '', 'only_password' => false\]/);
assert.match(chargeCard, /'vip'\s+=> \['enabled' => false, 'desc' => '', 'only_password' => true\]/);
assert.match(chargeCard, /卡密兑换余额功能未开启/);
assert.match(chargeCard, /卡密兑换积分功能未开启/);
assert.match(chargeCard, /卡密兑换会员功能未开启/);
assert.match(chargeCard, /积分系统未开启/);
// 收款接口 card_pass 主开关:三开关任一开启即生效。
const paymentRuntime = read('src', 'Settings', 'PaymentRuntime.php');
assert.match(paymentRuntime, /\$card_pass_enabled = !empty\(\$settings\['pay_balance_pass_charge_s'\]\)[\s\S]*?\|\| !empty\(\$settings\['pay_vip_pass_charge_s'\]\);/);
});
test('后台 schema:卡密兑换拆分为三个独立开关', () => {
for (const [name, src] of [['SchemaPayment.php', schemaPhp], ['recharge.js', schemaJs]]) {
assert.match(src, /启用卡密兑换余额功能/, name);
assert.match(src, /启用卡密兑换积分功能/, name);
assert.match(src, /启用卡密兑换会员功能/, name);
assert.match(src, /points_pass_exchange_s/, name);
assert.match(src, /points_pass_exchange_desc/, name);
assert.match(src, /points_pass_exchange_only_password/, name);
assert.match(src, /pay_vip_pass_charge_desc/, name);
assert.match(src, /pay_vip_pass_charge_only_password/, name);
// 旧总开关语义不回潮。
assert.doesNotMatch(src, /启用卡密兑换功能'|启用卡密兑换功能"/, name);
}
// recharge.js defaults 补齐三开关字段。
assert.match(schemaJs, /points_pass_exchange_s: false/);
assert.match(schemaJs, /pay_vip_pass_charge_s: false/);
// vip schema 的「会员卡密」组迁移至 recharge 卡密兑换组,原组删除。
assert.doesNotMatch(vipSchemaJs, /title: '会员卡密'/);
assert.doesNotMatch(vipSchemaJs, /pay_vip_pass_charge_only_password/);
const schemaPaymentVip = schemaPhp;
assert.doesNotMatch(schemaPaymentVip, /title' => '会员卡密'/);
});
test('核销订单契约:金额恒为 0,订单名称按 note 生成', () => {
assert.match(chargeCard, /订单金额恒为 0/);
assert.match(userOrders, /if \(\$payment_method === 'card_pass'\)\s*\{\s*\$amount = \$is_points \? '0' : '0\.00';/);
assert.match(userOrders, /if \(!empty\(\$meta\['card_pass'\]\)\)\s*\{[\s\S]*?resolve_card_pass_title\(\$meta\)/);
assert.match(userOrders, /卡密兑换-余额充值/);
assert.match(userOrders, /卡密兑换-积分充值/);
assert.match(userOrders, /购入\[%1\$s\]%2\$s/);
assert.match(userOrders, /永久/);
});