feat(vip): restructure membership settings into structured groups
Replace the pipe-delimited VIP configuration with structured groups and accordions, matching the reference theme (Zibll) 1:1. Settings: - 会员等级 keeps only the nav buttons and the two level switches, plus the "must enable level 1 first" guard. Level identity fields (display name, intro, benefits HTML, icon) and 会员商品 move into the 一级会员/二级会员 tabs of 会员购买选项, so each level is configured in one place. - Level tab titles bind to the configured display name via a new titleKey/titleFallback hook in AksFormRenderer. - 会员商品 is a repeater with exactly the reference fields: 执行价/原价/ 促销标签/标签颜色/会员有效时间/时长单位. The tag colour uses the full b+jb palette and now reaches the product card instead of a hardcoded jb-yellow. - 会员续费 gains the 自定义商品 type backed by per-level renew-product groups; the discount becomes a 0.1-9.9 slider with float sanitizing. - 会员升级 becomes the reference three-block accordion (month-to-month day-rate, jump to permanent, permanent to permanent), each with its own price, strikethrough price, promo tag and tag colour. - 积分兑换明细 is a repeater (points/level/duration/unit) where the duration accepts the literal Permanent. Runtime: - PaymentRuntime normalizes the structured groups into purchasable rows (money normalization, palette validation, zero-price and out-of-range rows dropped) and no longer emits any *_raw strings. - The catalog builder carries tag_class through and routes renewals to the renew-product group under 自定义商品. - The points-exchange order note carries time/unit so month durations use calendar math instead of a 30-day approximation. Dead code removed: parse_level_products, parse_packages, parse_upgrade_product_line and their pipe-splitting, plus nine zero-call PaymentRuntime proxy methods in HomeCms. No dual-format or migration path was added; SchemaProvider already falls back to defaults when a stored group value is not an array, so existing pipe strings degrade safely. Verified: php -l on every changed PHP file, 655 Node contract tests passing (14 new ones in vip-settings-structured-contract.test.cjs exercising the real schema build and runtime normalization via a PHP harness), and the admin SPA builds.
This commit is contained in:
1 parent
dcb80cff6e
commit
af8dccdc45
15 files changed
+1461
-489
No files matched your search
@@ -10,8 +10,9 @@
|
||||
* 会员介绍与开通会员更多内容默认值),SPA schema / PHP schema /
|
||||
* PaymentRuntime 回退三处一致——Settings::get_all() 不合并
|
||||
* schema 默认值,运行时回退即实际默认值;
|
||||
* 3) vip_renew_desc / vip_upgrade_desc 为死配置(AKSRT 无续费/升级
|
||||
* 卡渲染面,全仓零消费方),两份 schema 与 PaymentRuntime 均已删除;
|
||||
* 3) vip_renew_desc / vip_upgrade_desc 为参考主题的一句话简介字段,
|
||||
* 两份 schema 与 PaymentRuntime 均已定义,并由用户中心 /user/vip/
|
||||
* 档位卡渲染消费(vip-tab.php 按续费/升级动作取对应简介);
|
||||
* 4) 权益与卡密说明字段保存时保留 HTML(sanitize_type html),
|
||||
* 不再被 sanitize_textarea_field 剥掉标签。
|
||||
*/
|
||||
@@ -85,6 +86,18 @@ test('两个文案字段位于分区末尾的界面文案分组且不再属于
|
||||
// 「会员等级」组不再包含这两个字段。
|
||||
assert.doesNotMatch(slice(vipSchemaJs, "key: 'basic'", "key: 'benefit'"), /pay_user_vip_desc|pay_user_vip_more/);
|
||||
assert.doesNotMatch(slice(schemaPhp, "'key' => 'basic'", "'key' => 'benefit'"), /pay_user_vip_desc|pay_user_vip_more/);
|
||||
// 「会员等级」组只留导航按钮与两个等级开关;等级身份字段全部搬进
|
||||
// 「会员购买选项」的等级 Tab(名称/简介/权益/图标)。
|
||||
const basicJs = slice(vipSchemaJs, "key: 'basic'", "key: 'benefit'");
|
||||
const basicPhp = slice(schemaPhp, "'key' => 'basic'", "'key' => 'benefit'");
|
||||
for (const key of ['pay_user_vip_1_name', 'pay_user_vip_2_name', 'pay_user_vip_1_desc', 'pay_user_vip_2_desc', 'vip_1_img_icon', 'vip_2_img_icon']) {
|
||||
assert.ok(!basicJs.includes(key), `vip.js 会员等级组不应再含 ${key}`);
|
||||
assert.ok(!basicPhp.includes(key), `SchemaPayment.php 会员等级组不应再含 ${key}`);
|
||||
}
|
||||
assert.ok(basicJs.includes('pay_user_vip_1_s') && basicJs.includes('pay_user_vip_2_s'), 'vip.js 会员等级组应保留两个等级开关');
|
||||
assert.ok(basicPhp.includes("'key' => 'pay_user_vip_1_s'") && basicPhp.includes("'key' => 'pay_user_vip_2_s'"), 'SchemaPayment.php 会员等级组应保留两个等级开关');
|
||||
// 二级会员先于一级会员开启时的危险提示(参考主题同款)。
|
||||
assert.ok(basicJs.includes('vip_level_order_notice') && basicPhp.includes('vip_level_order_notice'));
|
||||
// 依赖:任一等级启用时显示(zibll pay_user_vip_1_s 依赖的双等级适配)。
|
||||
assert.ok(
|
||||
vipSchemaJs.includes("dependsOn: { any: [{ key: 'pay_user_vip_1_s', value: true }, { key: 'pay_user_vip_2_s', value: true }] }"),
|
||||
@@ -96,11 +109,19 @@ test('两个文案字段位于分区末尾的界面文案分组且不再属于
|
||||
);
|
||||
});
|
||||
|
||||
test('续费/升级说明死配置已从两份 schema 与运行时删除', () => {
|
||||
for (const [name, src] of [['vip.js', vipSchemaJs], ['SchemaPayment.php', schemaPhp], ['PaymentRuntime.php', paymentRuntime]]) {
|
||||
assert.ok(!src.includes('vip_renew_desc'), `${name} 不应再含 vip_renew_desc`);
|
||||
assert.ok(!src.includes('vip_upgrade_desc'), `${name} 不应再含 vip_upgrade_desc`);
|
||||
test('续费/升级介绍字段存在且被档位卡消费', () => {
|
||||
for (const [name, src] of [['vip.js', vipSchemaJs], ['SchemaPayment.php', schemaPhp]]) {
|
||||
assert.ok(src.includes('vip_renew_desc'), `${name} 应含 vip_renew_desc`);
|
||||
assert.ok(src.includes('vip_upgrade_desc'), `${name} 应含 vip_upgrade_desc`);
|
||||
}
|
||||
// 运行时回退默认值(get_all 不合并 schema 默认值,回退即实际默认值)。
|
||||
assert.ok(paymentRuntime.includes("$settings['vip_renew_desc'] ?? '立即续费会员,畅享VIP权益'"));
|
||||
assert.ok(paymentRuntime.includes("$settings['vip_upgrade_desc'] ?? '升级VIP会员,享更多会员权益'"));
|
||||
// 消费方:/user/vip/ 档位卡按动作取续费/升级简介,不得回退成死配置。
|
||||
assert.ok(vipTabPhp.includes("$vip_settings['renew']['desc']"));
|
||||
assert.ok(vipTabPhp.includes("$vip_settings['upgrade']['desc']"));
|
||||
assert.match(vipTabPhp, /\$card_action = 'renew'/);
|
||||
assert.match(vipTabPhp, /\$card_action = 'upgrade'/);
|
||||
});
|
||||
|
||||
test('文案默认值三处一致并对齐 zibll', () => {
|
||||
@@ -111,10 +132,12 @@ test('文案默认值三处一致并对齐 zibll', () => {
|
||||
assert.ok(vipSchemaJs.includes("pay_user_vip_2_desc: '尊享专属特权'"));
|
||||
assert.ok(vipSchemaJs.includes(`pay_user_vip_desc: '${DESC_DEFAULT}'`));
|
||||
assert.ok(vipSchemaJs.includes(`pay_user_vip_more: '${MORE_DEFAULT}'`));
|
||||
assert.ok(vipSchemaJs.includes(`default: '${EQUITY_DEFAULT}'`));
|
||||
// 权益默认值收敛为常量,两份 schema 与运行时共用同一份字面量。
|
||||
assert.ok(vipSchemaJs.includes(`const VIP_EQUITY_DEFAULT = '${EQUITY_DEFAULT}'`));
|
||||
assert.ok(vipSchemaJs.includes('default: VIP_EQUITY_DEFAULT'));
|
||||
// PHP schema 字段 default。
|
||||
assert.ok(schemaPhp.includes("'default' => '黄金会员'"));
|
||||
assert.ok(schemaPhp.includes("'default' => '钻石会员'"));
|
||||
assert.ok(schemaPhp.includes("'default' => $level === 1 ? '黄金会员' : '钻石会员'"));
|
||||
assert.ok(schemaPhp.includes(EQUITY_DEFAULT));
|
||||
assert.ok(schemaPhp.includes(`'default' => '${DESC_DEFAULT}'`));
|
||||
assert.ok(schemaPhp.includes(`'default' => '${MORE_DEFAULT}'`));
|
||||
// PaymentRuntime 运行时回退(get_all 不合并 schema 默认值,回退即实际默认值)。
|
||||
@@ -132,10 +155,9 @@ test('文案默认值三处一致并对齐 zibll', () => {
|
||||
});
|
||||
|
||||
test('权益与卡密说明字段保存时保留 HTML', () => {
|
||||
assert.ok(vipSchemaJs.includes("key: 'pay_user_vip_1_equity', label: '一级会员权益 HTML', type: 'textarea', sanitize_type: 'html'"));
|
||||
assert.ok(vipSchemaJs.includes("key: 'pay_user_vip_2_equity', label: '二级会员权益 HTML', type: 'textarea', sanitize_type: 'html'"));
|
||||
assert.ok(schemaPhp.includes("'key' => 'pay_user_vip_1_equity', 'label' => '一级会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html'"));
|
||||
assert.ok(schemaPhp.includes("'key' => 'pay_user_vip_2_equity', 'label' => '二级会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html'"));
|
||||
// 权益字段已并入「会员购买选项」的等级 Tab,标签统一为「会员权益 HTML」。
|
||||
assert.ok(vipSchemaJs.includes("key: `pay_user_vip_${level}_equity`, label: '会员权益 HTML', type: 'textarea', sanitize_type: 'html'"));
|
||||
assert.ok(schemaPhp.includes("['key' => 'pay_user_vip_' . $level . '_equity', 'label' => '会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html'"));
|
||||
assert.ok(rechargeJs.includes("key: 'pay_vip_pass_charge_desc', label: '卡密兑换会员说明', type: 'textarea', sanitize_type: 'html'"));
|
||||
assert.ok(schemaPhp.includes("'key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换会员说明', 'type' => 'textarea', 'sanitize_type' => 'html'"));
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user