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:
@@ -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/*
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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, /永久/);
|
||||
});
|
||||
Reference in New Issue
Block a user