用户配置 5-6 个收款渠道时(400px modal-mini 内 6 项),方法行项目
被 flex 压缩导致图标变形:方形 logo 被压至 31px/22px,宽形 stripe
logo(2468:1024)被压成 35px 方形。经无头 Chrome 几何实证,参考主题
同场景存在完全相同的缺陷(两主题 main.css 该组件逐条一致),属 Zibll
组件固有问题而非复刻偏差;按用户要求修复且不改变正常场景视觉:
- main.css:.payment-method-radio img 增加 flex:none(图标恒为固
有比例宽,高 35px 不变);main.min.css 经 build-public-assets 重建
- 三个弹窗方法行加 hh(.flex.hh{flex-wrap:wrap}):空间不足时整行
换行而非压缩图标——充值/积分(服务端模板)与会员(pay-vip.js)
三处同步
- 契约测试锁定 img flex:none 与三处行标记(322 项全部通过)
- 验证:无头 Chrome 实测 6 渠道 370/360/330/300px 容器下全部图标
恒定固有比例宽度,行自动换行
214 lines
13 KiB
JavaScript
214 lines
13 KiB
JavaScript
/**
|
||
* 卡密兑换渠道契约(三个购买弹窗 · 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 aksrtPay = read('inc', 'helpers', 'aksrt-pay.php');
|
||
const mainCss = read('assets', 'css', 'main.css');
|
||
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|aksrt-vip-submit/);
|
||
// 按钮字样对齐参考主题:会员弹窗即 initiate_pay_input(4) 默认「立即支付」;
|
||
// 无可用支付方式渲染 Zibll 同款 badg 提示(而非禁用按钮/自实现文案)。
|
||
assert.doesNotMatch(payVipJs, /cta_label/);
|
||
assert.match(payVipJs, /canPurchase \? "立即支付"/);
|
||
assert.match(payVipJs, /badg px12 c-yellow-2/, "暂时无法支付,请与客服联系");
|
||
// 渠道展示名对齐参考主题 zibpay_get_payment_method_args(余额/微信/支付宝)。
|
||
assert.match(aksrtPay, /'balance'\s+=> __\('余额', 'aksrt'\)/);
|
||
assert.match(aksrtPay, /'wechatpay'\s+=> __\('微信', 'aksrt'\)/);
|
||
// 显隐:通用 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' => false\]/);
|
||
// 会员单密码默认关闭(对齐参考主题 default false):未显式开启时卡密盒显示卡号+密码。
|
||
assert.doesNotMatch(chargeCard, /'only_password' => true/);
|
||
assert.match(read('src', 'Settings', 'PaymentRuntime.php'), /'only_password' => !empty\(\$settings\['pay_vip_pass_charge_only_password'\]\),/);
|
||
assert.doesNotMatch(read('src', 'Settings', 'PaymentRuntime.php'), /!isset\(\$settings\['pay_vip_pass_charge_only_password'\]\)/);
|
||
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, /永久/);
|
||
});
|
||
|
||
test('多渠道防挤压:图标 flex:none + 方法行 hh 换行(三弹窗一致)', () => {
|
||
// 图标禁用 flex 压缩(参考主题同场景存在变形缺陷,实测 6 渠道 360px
|
||
// 容器图标被压至 31px、宽形 stripe 被压成 35px 方形)。
|
||
assert.match(mainCss, /\.payment-method-radio img \{[^}]*height:\s*35px;[^}]*flex:\s*none;/s);
|
||
// 方法行启用 hh 换行(.flex.hh{flex-wrap:wrap}),放不下时整行换行。
|
||
assert.match(wallet, /class="flex hh mb10"/);
|
||
assert.match(creditsRenderer, /class="flex hh mb10"/);
|
||
assert.match(payVipJs, /node\("div", "flex hh mb10"\)/);
|
||
});
|