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
@@ -1053,6 +1053,8 @@ const repeaterDraft = ref({})
|
||||
function createRepeaterItem() {
|
||||
const newItem = { _id: `rpt_${Date.now()}_${++repeaterIdCounter}` }
|
||||
for (const field of (props.schema.fields || [])) {
|
||||
// UI-only chrome rows (notice / alert) are presentation only.
|
||||
if (field.ui_only) continue
|
||||
if (field.default !== undefined) {
|
||||
newItem[field.key] = field.default
|
||||
} else if (field.type === 'switch') {
|
||||
|
||||
@@ -64,9 +64,9 @@
|
||||
<n-tabs type="line" animated>
|
||||
<n-tab-pane
|
||||
v-for="tab in field.tabs"
|
||||
:key="tab.title || tab.key"
|
||||
:name="tab.title || tab.key"
|
||||
:tab="tab.title || tab.key"
|
||||
:key="tab.key || tab.title"
|
||||
:name="tabName(tab)"
|
||||
:tab="tabName(tab)"
|
||||
>
|
||||
<template v-for="sub in (tab.fields || [])" :key="sub.key">
|
||||
<AksField
|
||||
@@ -231,6 +231,18 @@ function isFieldVisible(field) {
|
||||
return matchDependsOn(field.dependsOn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab label. `titleKey` binds the label to a live form value (e.g. the VIP level
|
||||
* display name) so renaming a level renames its tab; `titleFallback` is used
|
||||
* while that value is still empty.
|
||||
*/
|
||||
function tabName(tab) {
|
||||
if (!tab) return ''
|
||||
const dynamic = tab.titleKey ? props.formData?.[tab.titleKey] : ''
|
||||
const label = typeof dynamic === 'string' ? dynamic.trim() : ''
|
||||
return label || tab.titleFallback || tab.title || tab.key || ''
|
||||
}
|
||||
|
||||
/** Whole group can be hidden (e.g. optional advanced blocks). */
|
||||
function isGroupVisible(group) {
|
||||
return matchDependsOn(group.dependsOn)
|
||||
|
||||
@@ -1,24 +1,109 @@
|
||||
/**
|
||||
* VIP会员 — 参考主题结构:会员等级、会员权益(普通/一级/二级 tab)、
|
||||
* 会员购买选项(一级/二级/续费/升级 tab)、卡密兑换与积分兑换。
|
||||
* VIP会员 — 结构 1:1 对照参考主题 Zibll:会员等级(开关)、会员权益
|
||||
* (普通/一级/二级 tab)、会员购买选项(一级/二级/续费/升级 tab)、
|
||||
* 积分兑换会员与界面文案。
|
||||
*
|
||||
* 会员商品 / 续费商品 / 升级价格 / 积分兑换明细均为结构化 group 或
|
||||
* accordion,不再使用 `|` 分隔的 textarea。等级 Tab 标题通过 titleKey
|
||||
* 绑定「会员显示名称」,改名即改 Tab 名。
|
||||
*/
|
||||
|
||||
const VIP_EQUITY_DEFAULT = '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>';
|
||||
|
||||
const vipTagPalette = [
|
||||
{ label: '#f74b3d', value: 'b-red' },
|
||||
{ label: '#f3920a', value: 'b-yellow' },
|
||||
{ label: '#08c4c1', value: 'b-cyan' },
|
||||
{ label: '#0a8cf3', value: 'b-blue' },
|
||||
{ label: '#1fd05a', value: 'b-green' },
|
||||
{ label: '#c133f5', value: 'b-purple' },
|
||||
{ label: '#121517', value: 'b-black' },
|
||||
{ label: 'linear-gradient(135deg, #ffbeb4 10%, #f61a1a 100%)', value: 'jb-red' },
|
||||
{ label: 'linear-gradient(135deg, #ff5e7f 30%, #ff967e 100%)', value: 'jb-pink' },
|
||||
{ label: 'linear-gradient(135deg, #ffd6b2 10%, #ff651c 100%)', value: 'jb-yellow' },
|
||||
{ label: 'linear-gradient(140deg, #039ab3 10%, #58dbcf 90%)', value: 'jb-cyan' },
|
||||
{ label: 'linear-gradient(135deg, #b6e6ff 10%, #198aff 100%)', value: 'jb-blue' },
|
||||
{ label: 'linear-gradient(135deg, #ccffcd 10%, #52bb51 100%)', value: 'jb-green' },
|
||||
{ label: 'linear-gradient(135deg, #fec2ff 10%, #d000de 100%)', value: 'jb-purple' },
|
||||
{ label: 'linear-gradient(25deg, #eab869 10%, #fbecd4 60%, #ffe0ae 100%)', value: 'jb-vip1' },
|
||||
{ label: 'linear-gradient(317deg, #4d4c4c 30%, #878787 70%, #5f5c5c 100%)', value: 'jb-vip2' },
|
||||
];
|
||||
|
||||
const vipUnitOptions = [
|
||||
{ label: '天', value: 'day' },
|
||||
{ label: '月', value: 'month' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 会员商品字段集(参考主题 CFS_Module::vip_product)。
|
||||
*/
|
||||
function vipProductFields() {
|
||||
return [
|
||||
{ key: 'price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '699.00', min: 0, suffix: '元', help: '会员的实际购买价格' },
|
||||
{ key: 'show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '999.00', min: 0, suffix: '元', help: '显示在执行价格前面,并划掉' },
|
||||
{ key: 'tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '', rows: 1, help: '支持HTML,请注意控制长度' },
|
||||
{ key: 'tag_class', label: '标签颜色', type: 'palette', default: 'jb-yellow', dependsOn: { key: 'tag', operator: 'not_empty' }, options: vipTagPalette },
|
||||
{ key: 'time', label: '会员有效时间', type: 'spinner', default: 3, min: 0, max: 3600, step: 1, help: '开通会员的时长。填0则为永久会员' },
|
||||
{ key: 'unit', label: '时长单位', type: 'radio', default: 'month', inline: true, dependsOn: { key: 'time', operator: 'truthy' }, options: vipUnitOptions },
|
||||
{ key: '_time_permanent_hint', label: '', type: 'alert', alertType: 'success', ui_only: true, dependsOn: { key: 'time', operator: 'eq', value: 0 }, content: '会员有效时间已设置为:永久会员' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员等级 Tab 字段集(参考主题 CFS_Module::vip_tab,显示名称并入 Tab)。
|
||||
*/
|
||||
function vipLevelTabFields(level) {
|
||||
const dep = { key: `pay_user_vip_${level}_s`, value: true };
|
||||
return [
|
||||
{ key: `pay_user_vip_${level}_name`, label: '会员显示名称', type: 'input', default: level === 1 ? '黄金会员' : '钻石会员', dependsOn: dep, help: '会员名称(例如“黄金会员”、“超级会员”)' },
|
||||
{ key: `pay_user_vip_${level}_desc`, label: '一句话简介', type: 'input', default: '尊享专属特权', dependsOn: dep },
|
||||
{ key: `pay_user_vip_${level}_equity`, label: '会员权益 HTML', type: 'textarea', sanitize_type: 'html', default: VIP_EQUITY_DEFAULT, rows: 4, dependsOn: dep, help: '建议使用 <li> 列表项' },
|
||||
{ key: `vip_${level}_product_s`, label: '购买会员', type: 'switch', default: true, dependsOn: dep, help: '关闭后则不能付费购买会员,但可通过积分兑换会员,请确保已开启积分以及积分兑换会员功能' },
|
||||
{
|
||||
key: `vip_${level}_product`,
|
||||
label: '会员商品',
|
||||
type: 'group',
|
||||
max: 8,
|
||||
button_title: '添加会员商品',
|
||||
default: [
|
||||
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
dependsOn: { all: [dep, { key: `vip_${level}_product_s`, value: true }] },
|
||||
help: '会员购买价格配置:执行价、原价、促销标签、标签颜色与会员有效时间',
|
||||
fields: vipProductFields(),
|
||||
},
|
||||
{ key: `vip_${level}_img_icon`, label: '会员图标', type: 'upload', default: '', dependsOn: dep, help: `留空使用内置 vip-${level}.svg 图标;建议尺寸300x300` },
|
||||
];
|
||||
}
|
||||
|
||||
export const vipSchema = {
|
||||
key: 'payment.vip',
|
||||
title: 'VIP会员',
|
||||
icon: 'fa fas fa-gem',
|
||||
description: '双级会员:等级名称、权益(下载次数与限速)、套餐、续费/升级与积分兑换。',
|
||||
description: '双级会员:等级开关、权益(下载次数与限速)、会员商品、续费/升级与积分兑换。',
|
||||
defaults: {
|
||||
nav_pay_vip: true,
|
||||
nav_user_pay_vip: true,
|
||||
pay_user_vip_1_s: true,
|
||||
pay_user_vip_1_name: '黄金会员',
|
||||
pay_user_vip_1_desc: '尊享专属特权',
|
||||
pay_user_vip_1_equity: '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>',
|
||||
pay_user_vip_1_equity: VIP_EQUITY_DEFAULT,
|
||||
vip_1_product_s: true,
|
||||
vip_1_product: [
|
||||
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
vip_1_img_icon: '',
|
||||
pay_user_vip_2_s: true,
|
||||
pay_user_vip_2_name: '钻石会员',
|
||||
pay_user_vip_2_desc: '尊享专属特权',
|
||||
pay_user_vip_2_equity: '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>',
|
||||
pay_user_vip_2_equity: VIP_EQUITY_DEFAULT,
|
||||
vip_2_product_s: true,
|
||||
vip_2_product: [
|
||||
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
vip_2_img_icon: '',
|
||||
pay_user_vip_desc: '开通VIP会员,享受会员专属折扣以及多项特权',
|
||||
pay_user_vip_more: '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>',
|
||||
@@ -29,17 +114,45 @@ export const vipSchema = {
|
||||
pay_download_limit_vip_2: 0,
|
||||
pay_download_speed_vip_2: 0,
|
||||
vip_1_product_s: true,
|
||||
payment_vip_1_products: '30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡',
|
||||
vip_2_product_s: true,
|
||||
payment_vip_2_products: '58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡',
|
||||
vip_renew: true,
|
||||
vip_renew_desc: '立即续费会员,畅享VIP权益',
|
||||
vip_renew_price_type: 'discount',
|
||||
vip_renew_discount: 8,
|
||||
vip_renew_reduce: '20.00',
|
||||
vip_1_renew_product: [
|
||||
{ price: 69, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 169, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
vip_2_renew_product: [
|
||||
{ price: 269, show_price: 599, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 369, show_price: 899, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
vip_2_product_s: true,
|
||||
vip_renew: true,
|
||||
vip_renew_discount: 8,
|
||||
vip_renew_reduce: '20.00',
|
||||
vip_upgrade: true,
|
||||
vip_upgrade_product: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
|
||||
vip_upgrade_desc: '升级VIP会员,享更多会员权益',
|
||||
vip_upgrade_product: {
|
||||
month_to_month_s: true,
|
||||
unit_price: '10.00',
|
||||
unit_tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐',
|
||||
unit_tag_class: 'jb-yellow',
|
||||
jump_s: false,
|
||||
jump_price: '199.00',
|
||||
jump_show_price: '299.00',
|
||||
jump_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||
jump_tag_class: 'jb-red',
|
||||
permanent_price: '199.00',
|
||||
permanent_show_price: '299.00',
|
||||
permanent_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||
permanent_tag_class: 'jb-red',
|
||||
},
|
||||
pay_vip_points_exchange_s: false,
|
||||
payment_vip_points_packages: '1|500|30|积分月卡\n1|5000|0|积分永久',
|
||||
pay_vip_points_exchange_product: [
|
||||
{ points: 500, level: 1, time: 30, unit: 'day' },
|
||||
{ points: 5000, level: 1, time: 'Permanent' },
|
||||
],
|
||||
pay_vip_points_exchange_desc: '',
|
||||
},
|
||||
groups: [
|
||||
@@ -65,15 +178,16 @@ export const vipSchema = {
|
||||
{ key: 'nav_pay_vip', label: '导航栏购买按钮', type: 'switch', default: true, help: '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。' },
|
||||
{ key: 'nav_user_pay_vip', label: '用户框购买按钮', type: 'switch', default: true, help: '在导航栏用户框内显示开通会员按钮。' },
|
||||
{ key: 'pay_user_vip_1_s', label: '启用一级会员', type: 'switch', default: true },
|
||||
{ key: 'pay_user_vip_1_name', label: '一级会员显示名称', type: 'input', default: '黄金会员', dependsOn: { key: 'pay_user_vip_1_s', value: true } },
|
||||
{ key: 'pay_user_vip_1_desc', label: '一级会员一句话简介', type: 'input', default: '尊享专属特权', dependsOn: { key: 'pay_user_vip_1_s', value: true } },
|
||||
{ key: 'pay_user_vip_1_equity', label: '一级会员权益 HTML', type: 'textarea', sanitize_type: 'html', default: '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>', rows: 4, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '建议使用 <li> 列表项' },
|
||||
{ key: 'vip_1_img_icon', label: '一级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '留空使用内置 vip-1.svg 图标' },
|
||||
{
|
||||
key: 'vip_level_order_notice',
|
||||
label: '',
|
||||
type: 'notice',
|
||||
ui_only: true,
|
||||
alertType: 'danger',
|
||||
dependsOn: { all: [{ key: 'pay_user_vip_2_s', value: true }, { key: 'pay_user_vip_1_s', operator: 'falsy' }] },
|
||||
content: '<div style="text-align:center"><b>必须先开启一级会员后才能开启二级会员!</b></div>',
|
||||
},
|
||||
{ key: 'pay_user_vip_2_s', label: '启用二级会员', type: 'switch', default: true },
|
||||
{ key: 'pay_user_vip_2_name', label: '二级会员显示名称', type: 'input', default: '钻石会员', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
||||
{ key: 'pay_user_vip_2_desc', label: '二级会员一句话简介', type: 'input', default: '尊享专属特权', dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
||||
{ key: 'pay_user_vip_2_equity', label: '二级会员权益 HTML', type: 'textarea', sanitize_type: 'html', default: '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>', rows: 4, dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
||||
{ key: 'vip_2_img_icon', label: '二级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '留空使用内置 vip-2.svg 图标' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -97,6 +211,8 @@ export const vipSchema = {
|
||||
{
|
||||
key: 'vip1',
|
||||
title: '一级会员',
|
||||
titleKey: 'pay_user_vip_1_name',
|
||||
titleFallback: '一级会员',
|
||||
fields: [
|
||||
{ key: 'pay_download_limit_vip_1', label: '付费资源:免费资源每日可下载(个)', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '一级会员每日最多可下载几个免费资源(为0则不限制)' },
|
||||
{ key: 'pay_download_speed_vip_1', label: '付费资源:下载限速(KB/s)', type: 'number', default: 0, min: 0, dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效' },
|
||||
@@ -105,6 +221,8 @@ export const vipSchema = {
|
||||
{
|
||||
key: 'vip2',
|
||||
title: '二级会员',
|
||||
titleKey: 'pay_user_vip_2_name',
|
||||
titleFallback: '二级会员',
|
||||
fields: [
|
||||
{ key: 'pay_download_limit_vip_2', label: '付费资源:免费资源每日可下载(个)', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '二级会员每日最多可下载几个免费资源(为0则不限制)' },
|
||||
{ key: 'pay_download_speed_vip_2', label: '付费资源:下载限速(KB/s)', type: 'number', default: 0, min: 0, dependsOn: { key: 'pay_user_vip_2_s', value: true }, help: '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效' },
|
||||
@@ -127,52 +245,108 @@ export const vipSchema = {
|
||||
{
|
||||
key: 'vip1',
|
||||
title: '一级会员',
|
||||
titleKey: 'pay_user_vip_1_name',
|
||||
titleFallback: '一级会员',
|
||||
fields: [
|
||||
{ key: 'vip_1_product_s', label: '允许购买一级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_1_s', value: true } },
|
||||
{
|
||||
key: 'payment_vip_1_products',
|
||||
label: '一级会员套餐',
|
||||
type: 'textarea',
|
||||
default: '30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡',
|
||||
dependsOn: { key: 'vip_1_product_s', value: true },
|
||||
help: '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久',
|
||||
},
|
||||
...vipLevelTabFields(1),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'vip2',
|
||||
title: '二级会员',
|
||||
titleKey: 'pay_user_vip_2_name',
|
||||
titleFallback: '二级会员',
|
||||
fields: [
|
||||
{ key: 'vip_2_product_s', label: '允许购买二级会员', type: 'switch', default: true, dependsOn: { key: 'pay_user_vip_2_s', value: true } },
|
||||
{
|
||||
key: 'payment_vip_2_products',
|
||||
label: '二级会员套餐',
|
||||
type: 'textarea',
|
||||
default: '58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡',
|
||||
dependsOn: { key: 'vip_2_product_s', value: true },
|
||||
help: '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久',
|
||||
},
|
||||
...vipLevelTabFields(2),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'renew',
|
||||
title: '会员续费',
|
||||
fields: [
|
||||
{ key: 'vip_renew', label: '允许续费', type: 'switch', default: true, help: '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费' },
|
||||
{
|
||||
key: 'vip_renew',
|
||||
label: '允许续费',
|
||||
type: 'switch',
|
||||
default: true,
|
||||
help: '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费',
|
||||
},
|
||||
{
|
||||
key: 'vip_renew_desc',
|
||||
label: '续费介绍',
|
||||
type: 'input',
|
||||
default: '立即续费会员,畅享VIP权益',
|
||||
dependsOn: { key: 'vip_renew', value: true },
|
||||
help: '续费会员的一句话简介',
|
||||
},
|
||||
{
|
||||
key: 'vip_renew_price_type',
|
||||
label: '续费价格类型',
|
||||
type: 'radio',
|
||||
type: 'select',
|
||||
default: 'discount',
|
||||
dependsOn: { key: 'vip_renew', value: true },
|
||||
options: [
|
||||
{ label: '相同价格', value: 'same' },
|
||||
{ label: '折扣价格', value: 'discount' },
|
||||
{ label: '优惠立减', value: 'reduce' },
|
||||
{ label: '自定义商品', value: 'customize' },
|
||||
],
|
||||
help: '自定义商品时,按下方「一级会员续费」「二级会员续费」的商品选项计价',
|
||||
},
|
||||
{
|
||||
key: 'vip_renew_discount',
|
||||
label: '续费折扣',
|
||||
type: 'slider',
|
||||
sanitize_type: 'float',
|
||||
default: 8,
|
||||
min: 0.1,
|
||||
max: 9.9,
|
||||
step: 0.1,
|
||||
unit: '折',
|
||||
dependsOn: { key: 'vip_renew_price_type', value: 'discount' },
|
||||
help: '在会员商品基础上打几折,如 8 = 八折',
|
||||
},
|
||||
{
|
||||
key: 'vip_renew_reduce',
|
||||
label: '续费减免金额',
|
||||
type: 'spinner',
|
||||
sanitize_type: 'money_decimal',
|
||||
default: '20.00',
|
||||
min: 0,
|
||||
max: 500,
|
||||
step: 5,
|
||||
unit: '元',
|
||||
dependsOn: { key: 'vip_renew_price_type', value: 'reduce' },
|
||||
help: '在会员商品基础上优惠的金额,优惠后总金额不能为0元',
|
||||
},
|
||||
{
|
||||
key: 'vip_1_renew_product',
|
||||
label: '一级会员续费',
|
||||
type: 'group',
|
||||
max: 8,
|
||||
button_title: '添加续费商品',
|
||||
default: [
|
||||
{ price: 69, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 169, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
dependsOn: { key: 'vip_renew_price_type', value: 'customize' },
|
||||
help: '一级会员续费的商品选项',
|
||||
fields: vipProductFields(),
|
||||
},
|
||||
{
|
||||
key: 'vip_2_renew_product',
|
||||
label: '二级会员续费',
|
||||
type: 'group',
|
||||
max: 8,
|
||||
button_title: '添加续费商品',
|
||||
default: [
|
||||
{ price: 269, show_price: 599, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
|
||||
{ price: 369, show_price: 899, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
|
||||
],
|
||||
dependsOn: { key: 'vip_renew_price_type', value: 'customize' },
|
||||
help: '二级会员续费的商品选项',
|
||||
fields: vipProductFields(),
|
||||
},
|
||||
{ key: 'vip_renew_discount', label: '续费折扣(折,如 8=八折)', type: 'number', default: 8, min: 1, max: 100, dependsOn: { key: 'vip_renew_price_type', value: 'discount' } },
|
||||
{ key: 'vip_renew_reduce', label: '续费减免金额', type: 'input', sanitize_type: 'money_decimal', default: '20.00', min: 0, max: 9999, dependsOn: { key: 'vip_renew_price_type', value: 'reduce' } },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -180,13 +354,45 @@ export const vipSchema = {
|
||||
title: '会员升级',
|
||||
fields: [
|
||||
{ key: 'vip_upgrade', label: '允许升级', type: 'switch', default: true },
|
||||
{ key: 'vip_upgrade_desc', label: '升级介绍', type: 'input', default: '升级VIP会员,享更多会员权益', dependsOn: { key: 'vip_upgrade', value: true }, help: '升级会员的一句话简介' },
|
||||
{
|
||||
key: 'vip_upgrade_product',
|
||||
label: '升级产品',
|
||||
type: 'textarea',
|
||||
default: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级',
|
||||
label: '升级价格',
|
||||
type: 'accordion',
|
||||
default: {
|
||||
month_to_month_s: true,
|
||||
unit_price: '10.00',
|
||||
unit_tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐',
|
||||
unit_tag_class: 'jb-yellow',
|
||||
jump_s: false,
|
||||
jump_price: '199.00',
|
||||
jump_show_price: '299.00',
|
||||
jump_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||
jump_tag_class: 'jb-red',
|
||||
permanent_price: '199.00',
|
||||
permanent_show_price: '299.00',
|
||||
permanent_tag: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠',
|
||||
permanent_tag_class: 'jb-red',
|
||||
},
|
||||
dependsOn: { key: 'vip_upgrade', value: true },
|
||||
help: '每行一条。永久VIP1升级:permanent|价格|标题;按剩余天:unit|每日单价|标题;限时跳转永久:jump|价格|标题',
|
||||
help: '会员升级的商品选项',
|
||||
fields: [
|
||||
{ key: '_upgrade_unit_notice', label: '', type: 'notice', ui_only: true, alertType: 'warning', content: '月费会员升级为月费会员,用户会员有效期不会改变;价格按照会员剩余天数计算升级价格,请在下方设置每天的单价' },
|
||||
{ key: 'month_to_month_s', label: '月费会员升级月费会员', type: 'switch', default: true },
|
||||
{ key: 'unit_price', label: '天单价', type: 'number', sanitize_type: 'money_decimal', default: '10.00', min: 0, unit: '元', dependsOn: { key: 'month_to_month_s', value: true } },
|
||||
{ key: 'unit_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', rows: 1, dependsOn: { key: 'month_to_month_s', value: true }, help: '支持HTML,请注意控制长度' },
|
||||
{ key: 'unit_tag_class', label: '标签颜色', type: 'palette', default: 'jb-yellow', dependsOn: { all: [{ key: 'month_to_month_s', value: true }, { key: 'unit_tag', operator: 'not_empty' }] }, options: vipTagPalette },
|
||||
{ key: '_upgrade_jump_notice', label: '', type: 'notice', ui_only: true, alertType: 'warning', content: '如果网站可以购买月费会员和永久会员,则可以开启此项;允许用户由月费会员直接升级到更高一级的永久会员,用户之前还未到期的部分将自动一起转为永久会员,可能会涉及到差价问题,请自行告知用户;如果网站没有永久会员的购买选项请务必关闭此项' },
|
||||
{ key: 'jump_s', label: '月费会员升级永久会员', type: 'switch', default: false },
|
||||
{ key: 'jump_price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '199.00', min: 0, unit: '元', dependsOn: { key: 'jump_s', value: true }, help: '月费会员升级永久会员的价格' },
|
||||
{ key: 'jump_show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '299.00', min: 0, unit: '元', dependsOn: { key: 'jump_s', value: true }, help: '显示在执行价格前面,并划掉' },
|
||||
{ key: 'jump_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', rows: 1, dependsOn: { key: 'jump_s', value: true }, help: '支持HTML,请注意控制长度' },
|
||||
{ key: 'jump_tag_class', label: '标签颜色', type: 'palette', default: 'jb-red', dependsOn: { all: [{ key: 'jump_s', value: true }, { key: 'jump_tag', operator: 'not_empty' }] }, options: vipTagPalette },
|
||||
{ key: 'permanent_price', label: '执行价', type: 'number', sanitize_type: 'money_decimal', default: '199.00', min: 0, unit: '元', help: '永久会员升级永久会员的价格' },
|
||||
{ key: 'permanent_show_price', label: '原价', type: 'number', sanitize_type: 'money_decimal', default: '299.00', min: 0, unit: '元', help: '显示在执行价格前面,并划掉' },
|
||||
{ key: 'permanent_tag', label: '促销标签', type: 'textarea', sanitize_type: 'html', default: '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', rows: 1, help: '支持HTML,请注意控制长度' },
|
||||
{ key: 'permanent_tag_class', label: '标签颜色', type: 'palette', default: 'jb-red', dependsOn: { key: 'permanent_tag', operator: 'not_empty' }, options: vipTagPalette },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -199,14 +405,43 @@ export const vipSchema = {
|
||||
title: '积分兑换会员',
|
||||
icon: 'fa fas fa-database',
|
||||
fields: [
|
||||
{ key: 'pay_vip_points_exchange_s', label: '启用积分兑换', type: 'switch', default: false },
|
||||
{ key: 'pay_vip_points_exchange_s', label: '启用积分兑换', type: 'switch', default: false, help: '开启后用户可使用积分兑换会员;由于积分可免费获取,所以积分兑换会员功能不支持升级及续费' },
|
||||
{
|
||||
key: 'payment_vip_points_packages',
|
||||
label: '兑换套餐',
|
||||
type: 'textarea',
|
||||
default: '1|500|30|积分月卡\n1|5000|0|积分永久',
|
||||
key: 'pay_vip_points_exchange_product',
|
||||
label: '积分兑换明细',
|
||||
type: 'group',
|
||||
max: 16,
|
||||
button_title: '添加积分兑换商品',
|
||||
default: [
|
||||
{ points: 500, level: 1, time: 30, unit: 'day' },
|
||||
{ points: 5000, level: 1, time: 'Permanent' },
|
||||
],
|
||||
dependsOn: { key: 'pay_vip_points_exchange_s', value: true },
|
||||
help: '每行:等级|积分|天数|标题(天数 0=永久;仅未开通会员可兑)',
|
||||
help: '每一行为一个可兑换的会员套餐',
|
||||
fields: [
|
||||
{ key: 'points', label: '使用积分', type: 'number', default: 100, min: 0, unit: '积分' },
|
||||
{
|
||||
key: 'level',
|
||||
label: '兑换会员',
|
||||
type: 'radio',
|
||||
default: 1,
|
||||
inline: true,
|
||||
options: [
|
||||
{ label: '一级会员', value: 1 },
|
||||
{ label: '二级会员', value: 2 },
|
||||
],
|
||||
},
|
||||
{ key: 'time', label: '兑换时长', type: 'input', default: '', help: '填 Permanent 为永久' },
|
||||
{
|
||||
key: 'unit',
|
||||
label: '时长单位',
|
||||
type: 'radio',
|
||||
default: 'day',
|
||||
inline: true,
|
||||
dependsOn: { key: 'time', operator: 'neq', value: 'Permanent' },
|
||||
options: vipUnitOptions,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ key: 'pay_vip_points_exchange_desc', label: '兑换说明', type: 'textarea', sanitize_type: 'html', default: '', rows: 2, dependsOn: { key: 'pay_vip_points_exchange_s', value: true }, help: '显示在积分兑换弹窗底部,可以为提醒事项、兑换协议等,支持HTML代码' },
|
||||
],
|
||||
|
||||
@@ -124,7 +124,8 @@
|
||||
// Zibll 套餐卡:zib-widget vip-product relative text-center product-box
|
||||
// (右上 tag + 价格行 + 时长 badge,无标题行)。
|
||||
var product = node("div", "zib-widget vip-product relative text-center product-box");
|
||||
if (item.discount_tag) append(product, node("div", "abs-right vip-tag badg jb-yellow", item.discount_tag));
|
||||
// 标签颜色取后台「标签颜色」配置,未配置时回退参考主题默认的 jb-yellow。
|
||||
if (item.discount_tag) append(product, node("div", "abs-right vip-tag badg " + (item.tag_class || "jb-yellow"), item.discount_tag));
|
||||
append(product, priceRow(mark, item.price_label || "", item.show_price_label || ""));
|
||||
if (item.days_label) append(product, node("div", "muted-2-color c-blue em09 p2-10 badg radius", item.days_label));
|
||||
append(label, product);
|
||||
|
||||
@@ -29,6 +29,8 @@ $vip_state_name = (string) ($vip_state['state'] ?? 'unopened');
|
||||
$vip_state_source = (string) ($vip_state['source'] ?? 'none');
|
||||
$levels = is_array($vip_settings['levels'] ?? null) ? $vip_settings['levels'] : [];
|
||||
$desc_top = (string) ($vip_settings['desc'] ?? '');
|
||||
$renew_desc = (string) ($vip_settings['renew']['desc'] ?? '');
|
||||
$upgrade_desc = (string) ($vip_settings['upgrade']['desc'] ?? '');
|
||||
|
||||
// Membership summary: title → level → intro → expiry.
|
||||
$vip_state = 'unopened';
|
||||
@@ -116,6 +118,9 @@ foreach ([1, 2] as $lv) {
|
||||
: '';
|
||||
$cta = sprintf(__('开通%s', 'aksrt'), $lv_name);
|
||||
$vip_attr = $lv;
|
||||
// 参考主题 zibpay_get_vip_card:续费/升级档位卡展示各自的一句话简介。
|
||||
$card_action = 'pay';
|
||||
$card_desc = $lv_desc;
|
||||
$lv_icon_url = esc_url((string) ($levels[$lv]['img_icon'] ?? ''));
|
||||
$lv_icon = $lv_icon_url !== ''
|
||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $lv_icon_url, 'aksrt-vip-level-icon') . ' alt="" decoding="async">'
|
||||
@@ -124,8 +129,10 @@ foreach ([1, 2] as $lv) {
|
||||
$renew_lv = (int) substr($btn_type, 6);
|
||||
if ($lv === $renew_lv) {
|
||||
$cta = sprintf(__('续费%s', 'aksrt'), $lv_name);
|
||||
$card_action = 'renew';
|
||||
} elseif ($lv > $renew_lv) {
|
||||
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
||||
$card_action = 'upgrade';
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
@@ -134,19 +141,26 @@ foreach ([1, 2] as $lv) {
|
||||
continue;
|
||||
}
|
||||
$cta = sprintf(__('升级%s', 'aksrt'), $lv_name);
|
||||
$card_action = 'upgrade';
|
||||
$vip_attr = 2;
|
||||
} elseif ($level > 0 && $lv < $level) {
|
||||
continue;
|
||||
} elseif ($level > 0 && $lv === $level) {
|
||||
$cta = sprintf(__('续费%s', 'aksrt'), $lv_name);
|
||||
$card_action = 'renew';
|
||||
}
|
||||
if ($card_action === 'renew' && $renew_desc !== '') {
|
||||
$card_desc = $renew_desc;
|
||||
} elseif ($card_action === 'upgrade' && $upgrade_desc !== '') {
|
||||
$card_desc = $upgrade_desc;
|
||||
}
|
||||
|
||||
$cards .= '<div class="aksrt-panel text-center pay-vip-info flex1 level-' . $lv . ' uc-vip-level-card">';
|
||||
$cards .= '<div class="relative uc-vip-level-card__inner">';
|
||||
$cards .= '<div class="v-iconbox mb10">';
|
||||
$cards .= '<div class="v-name font-bold"><span class="v-icon">' . $lv_icon . '</span>' . esc_html($lv_name) . '</div>';
|
||||
if ($lv_desc !== '') {
|
||||
$cards .= '<div class="v-desc em09 muted-2-color">' . esc_html($lv_desc) . '</div>';
|
||||
if ($card_desc !== '') {
|
||||
$cards .= '<div class="v-desc em09 muted-2-color">' . esc_html($card_desc) . '</div>';
|
||||
}
|
||||
$cards .= '</div>';
|
||||
$cards .= $equity;
|
||||
|
||||
@@ -178,7 +178,7 @@ class SchemaPayment {
|
||||
'vip' => [
|
||||
'key' => 'vip',
|
||||
'title' => 'VIP会员',
|
||||
'version' => '1.1.0',
|
||||
'version' => '1.2.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'notice', 'title' => '说明', 'fields' => [
|
||||
@@ -188,15 +188,8 @@ class SchemaPayment {
|
||||
['key' => 'nav_pay_vip', 'label' => '导航栏购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在顶部导航栏显示开通会员按钮;请注意顶部导航的整体宽度。'],
|
||||
['key' => 'nav_user_pay_vip', 'label' => '用户框购买按钮', 'type' => 'switch', 'default' => true, 'help' => '在导航栏用户框内显示开通会员按钮。'],
|
||||
['key' => 'pay_user_vip_1_s', 'label' => '启用一级会员', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_user_vip_1_name', 'label' => '一级会员显示名称', 'type' => 'input', 'default' => '黄金会员', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_1_desc', 'label' => '一级会员一句话简介', 'type' => 'input', 'default' => '尊享专属特权', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_1_equity', 'label' => '一级会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>', 'rows' => 4, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'vip_1_img_icon', 'label' => '一级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '留空使用内置 vip-1.svg 图标'],
|
||||
['key' => 'vip_level_order_notice', 'label' => '', 'type' => 'notice', 'ui_only' => true, 'alertType' => 'danger', 'dependsOn' => ['all' => [['key' => 'pay_user_vip_2_s', 'value' => true], ['key' => 'pay_user_vip_1_s', 'operator' => 'falsy']]], 'content' => '<div style="text-align:center"><b>必须先开启一级会员后才能开启二级会员!</b></div>'],
|
||||
['key' => 'pay_user_vip_2_s', 'label' => '启用二级会员', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'pay_user_vip_2_name', 'label' => '二级会员显示名称', 'type' => 'input', 'default' => '钻石会员', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_2_desc', 'label' => '二级会员一句话简介', 'type' => 'input', 'default' => '尊享专属特权', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'pay_user_vip_2_equity', 'label' => '二级会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>', 'rows' => 4, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'vip_2_img_icon', 'label' => '二级会员图标 URL', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '留空使用内置 vip-2.svg 图标'],
|
||||
]],
|
||||
['key' => 'benefit', 'title' => '会员权益', 'fields' => [
|
||||
['key' => 'vip_benefit', 'label' => '会员权益', 'type' => 'tabbed', 'tabs' => [
|
||||
@@ -204,11 +197,11 @@ class SchemaPayment {
|
||||
['key' => 'pay_download_limit', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'help' => '普通用户每日最多可下载几个免费资源(为0则不限制)'],
|
||||
['key' => 'pay_download_speed', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
||||
]],
|
||||
['key' => 'vip1', 'title' => '一级会员', 'fields' => [
|
||||
['key' => 'vip1', 'title' => '一级会员', 'titleKey' => 'pay_user_vip_1_name', 'titleFallback' => '一级会员', 'fields' => [
|
||||
['key' => 'pay_download_limit_vip_1', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '一级会员每日最多可下载几个免费资源(为0则不限制)'],
|
||||
['key' => 'pay_download_speed_vip_1', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true], 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
||||
]],
|
||||
['key' => 'vip2', 'title' => '二级会员', 'fields' => [
|
||||
['key' => 'vip2', 'title' => '二级会员', 'titleKey' => 'pay_user_vip_2_name', 'titleFallback' => '二级会员', 'fields' => [
|
||||
['key' => 'pay_download_limit_vip_2', 'label' => '付费资源:免费资源每日可下载(个)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '二级会员每日最多可下载几个免费资源(为0则不限制)'],
|
||||
['key' => 'pay_download_speed_vip_2', 'label' => '付费资源:下载限速(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true], 'help' => '付费下载限制最大下载速度(为0则不限制);只对本地服务器文件有效'],
|
||||
]],
|
||||
@@ -216,34 +209,96 @@ class SchemaPayment {
|
||||
]],
|
||||
['key' => 'opt', 'title' => '会员购买选项', 'fields' => [
|
||||
['key' => 'vip_opt', 'label' => '会员购买选项', 'type' => 'tabbed', 'tabs' => [
|
||||
['key' => 'vip1', 'title' => '一级会员', 'fields' => [
|
||||
['key' => 'vip_1_product_s', 'label' => '允许购买一级会员', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_user_vip_1_s', 'value' => true]],
|
||||
['key' => 'payment_vip_1_products', 'label' => '一级会员套餐', 'type' => 'textarea', 'default' => "30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡", 'dependsOn' => ['key' => 'vip_1_product_s', 'value' => true], 'help' => '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久'],
|
||||
]],
|
||||
['key' => 'vip2', 'title' => '二级会员', 'fields' => [
|
||||
['key' => 'vip_2_product_s', 'label' => '允许购买二级会员', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]],
|
||||
['key' => 'payment_vip_2_products', 'label' => '二级会员套餐', 'type' => 'textarea', 'default' => "58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡", 'dependsOn' => ['key' => 'vip_2_product_s', 'value' => true], 'help' => '每行:执行价|原价|时长|单位(day|month)|标签|标题;时长 0=永久'],
|
||||
]],
|
||||
['key' => 'vip1', 'title' => '一级会员', 'titleKey' => 'pay_user_vip_1_name', 'titleFallback' => '一级会员', 'fields' => self::vip_level_tab_fields(1)],
|
||||
['key' => 'vip2', 'title' => '二级会员', 'titleKey' => 'pay_user_vip_2_name', 'titleFallback' => '二级会员', 'fields' => self::vip_level_tab_fields(2)],
|
||||
['key' => 'renew', 'title' => '会员续费', 'fields' => [
|
||||
['key' => 'vip_renew', 'label' => '允许续费', 'type' => 'switch', 'default' => true, 'help' => '关闭后则不能续费,只能会员到期之后再购买。永久会员不需要续费'],
|
||||
['key' => 'vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'radio', 'default' => 'discount', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'options' => [
|
||||
['key' => 'vip_renew_desc', 'label' => '续费介绍', 'type' => 'input', 'default' => '立即续费会员,畅享VIP权益', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'help' => '续费会员的一句话简介'],
|
||||
['key' => 'vip_renew_price_type', 'label' => '续费价格类型', 'type' => 'select', 'default' => 'discount', 'dependsOn' => ['key' => 'vip_renew', 'value' => true], 'options' => [
|
||||
['label' => '相同价格', 'value' => 'same'],
|
||||
['label' => '折扣价格', 'value' => 'discount'],
|
||||
['label' => '优惠立减', 'value' => 'reduce'],
|
||||
]],
|
||||
['key' => 'vip_renew_discount', 'label' => '续费折扣(折,如 8=八折)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount']],
|
||||
['key' => 'vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '20.00', 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'reduce']],
|
||||
['label' => '自定义商品', 'value' => 'customize'],
|
||||
], 'help' => '自定义商品时,按下方「一级会员续费」「二级会员续费」的商品选项计价'],
|
||||
['key' => 'vip_renew_discount', 'label' => '续费折扣', 'type' => 'slider', 'sanitize_type' => 'float', 'default' => 8, 'min' => 0.1, 'max' => 9.9, 'step' => 0.1, 'unit' => '折', 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount'], 'help' => '在会员商品基础上打几折,如 8 = 八折'],
|
||||
['key' => 'vip_renew_reduce', 'label' => '续费减免金额', 'type' => 'spinner', 'sanitize_type' => 'money_decimal', 'default' => '20.00', 'min' => 0, 'max' => 500, 'step' => 5, 'unit' => '元', 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'reduce'], 'help' => '在会员商品基础上优惠的金额,优惠后总金额不能为0元'],
|
||||
[
|
||||
'key' => 'vip_1_renew_product', 'label' => '一级会员续费', 'type' => 'group', 'max' => 8,
|
||||
'button_title' => '添加续费商品', 'default' => [
|
||||
['price' => '69.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '169.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
],
|
||||
'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'customize'],
|
||||
'help' => '一级会员续费的商品选项',
|
||||
'fields' => self::vip_product_fields(),
|
||||
],
|
||||
[
|
||||
'key' => 'vip_2_renew_product', 'label' => '二级会员续费', 'type' => 'group', 'max' => 8,
|
||||
'button_title' => '添加续费商品', 'default' => [
|
||||
['price' => '269.00', 'show_price' => '599.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '369.00', 'show_price' => '899.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
],
|
||||
'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'customize'],
|
||||
'help' => '二级会员续费的商品选项',
|
||||
'fields' => self::vip_product_fields(),
|
||||
],
|
||||
]],
|
||||
['key' => 'upgrade', 'title' => '会员升级', 'fields' => [
|
||||
['key' => 'vip_upgrade', 'label' => '允许升级', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'vip_upgrade_product', 'label' => '升级产品', 'type' => 'textarea', 'default' => "unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级", 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '每行一条。永久VIP1:permanent|价格|标题;剩余天:unit|每日单价|标题;跳转永久:jump|价格|标题'],
|
||||
['key' => 'vip_upgrade_desc', 'label' => '升级介绍', 'type' => 'input', 'default' => '升级VIP会员,享更多会员权益', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '升级会员的一句话简介'],
|
||||
[
|
||||
'key' => 'vip_upgrade_product', 'label' => '升级价格', 'type' => 'accordion', 'default' => [
|
||||
'month_to_month_s' => true, 'unit_price' => '10.00', 'unit_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'unit_tag_class' => 'jb-yellow',
|
||||
'jump_s' => false, 'jump_price' => '199.00', 'jump_show_price' => '299.00', 'jump_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'jump_tag_class' => 'jb-red',
|
||||
'permanent_price' => '199.00', 'permanent_show_price' => '299.00', 'permanent_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'permanent_tag_class' => 'jb-red',
|
||||
],
|
||||
'dependsOn' => ['key' => 'vip_upgrade', 'value' => true],
|
||||
'help' => '会员升级的商品选项',
|
||||
'fields' => [
|
||||
['key' => '_upgrade_unit_notice', 'label' => '', 'type' => 'notice', 'ui_only' => true, 'alertType' => 'warning', 'content' => '月费会员升级为月费会员,用户会员有效期不会改变;价格按照会员剩余天数计算升级价格,请在下方设置每天的单价'],
|
||||
['key' => 'month_to_month_s', 'label' => '月费会员升级月费会员', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'unit_price', 'label' => '天单价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '10.00', 'min' => 0, 'unit' => '元', 'dependsOn' => ['key' => 'month_to_month_s', 'value' => true]],
|
||||
['key' => 'unit_tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'rows' => 1, 'dependsOn' => ['key' => 'month_to_month_s', 'value' => true], 'help' => '支持HTML,请注意控制长度'],
|
||||
['key' => 'unit_tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-yellow', 'dependsOn' => ['all' => [['key' => 'month_to_month_s', 'value' => true], ['key' => 'unit_tag', 'operator' => 'not_empty']]], 'options' => self::vip_tag_palette()],
|
||||
['key' => '_upgrade_jump_notice', 'label' => '', 'type' => 'notice', 'ui_only' => true, 'alertType' => 'warning', 'content' => '如果网站可以购买月费会员和永久会员,则可以开启此项;允许用户由月费会员直接升级到更高一级的永久会员,用户之前还未到期的部分将自动一起转为永久会员,可能会涉及到差价问题,请自行告知用户;如果网站没有永久会员的购买选项请务必关闭此项'],
|
||||
['key' => 'jump_s', 'label' => '月费会员升级永久会员', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'jump_price', 'label' => '执行价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '199.00', 'min' => 0, 'unit' => '元', 'dependsOn' => ['key' => 'jump_s', 'value' => true], 'help' => '月费会员升级永久会员的价格'],
|
||||
['key' => 'jump_show_price', 'label' => '原价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '299.00', 'min' => 0, 'unit' => '元', 'dependsOn' => ['key' => 'jump_s', 'value' => true], 'help' => '显示在执行价格前面,并划掉'],
|
||||
['key' => 'jump_tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'rows' => 1, 'dependsOn' => ['key' => 'jump_s', 'value' => true], 'help' => '支持HTML,请注意控制长度'],
|
||||
['key' => 'jump_tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-red', 'dependsOn' => ['all' => [['key' => 'jump_s', 'value' => true], ['key' => 'jump_tag', 'operator' => 'not_empty']]], 'options' => self::vip_tag_palette()],
|
||||
['key' => 'permanent_price', 'label' => '执行价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '199.00', 'min' => 0, 'unit' => '元', 'help' => '永久会员升级永久会员的价格'],
|
||||
['key' => 'permanent_show_price', 'label' => '原价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '299.00', 'min' => 0, 'unit' => '元', 'help' => '显示在执行价格前面,并划掉'],
|
||||
['key' => 'permanent_tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠', 'rows' => 1, 'help' => '支持HTML,请注意控制长度'],
|
||||
['key' => 'permanent_tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-red', 'dependsOn' => ['key' => 'permanent_tag', 'operator' => 'not_empty'], 'options' => self::vip_tag_palette()],
|
||||
],
|
||||
],
|
||||
]],
|
||||
]],
|
||||
]],
|
||||
['key' => 'points-exchange', 'title' => '积分兑换会员', 'fields' => [
|
||||
['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'payment_vip_points_packages', 'label' => '兑换套餐', 'type' => 'textarea', 'default' => "1|500|30|积分月卡\n1|5000|0|积分永久", 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true], 'help' => '每行:等级|积分|天数|标题(天数 0=永久;普通用户可开通,已有会员可续费同等级或升级更高等级)'],
|
||||
['key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true]],
|
||||
['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false, 'help' => '开启后用户可使用积分兑换会员;由于积分可免费获取,所以积分兑换会员功能不支持升级及续费'],
|
||||
[
|
||||
'key' => 'pay_vip_points_exchange_product', 'label' => '积分兑换明细', 'type' => 'group', 'max' => 16,
|
||||
'button_title' => '添加积分兑换商品', 'default' => [
|
||||
['points' => 500, 'level' => 1, 'time' => 30, 'unit' => 'day'],
|
||||
['points' => 5000, 'level' => 1, 'time' => 'Permanent'],
|
||||
],
|
||||
'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true],
|
||||
'help' => '每一行为一个可兑换的会员套餐',
|
||||
'fields' => [
|
||||
['key' => 'points', 'label' => '使用积分', 'type' => 'number', 'default' => 100, 'min' => 0, 'unit' => '积分'],
|
||||
['key' => 'level', 'label' => '兑换会员', 'type' => 'radio', 'default' => 1, 'inline' => true, 'options' => [
|
||||
['label' => '一级会员', 'value' => 1],
|
||||
['label' => '二级会员', 'value' => 2],
|
||||
]],
|
||||
['key' => 'time', 'label' => '兑换时长', 'type' => 'input', 'default' => '', 'help' => '填 Permanent 为永久'],
|
||||
['key' => 'unit', 'label' => '时长单位', 'type' => 'radio', 'default' => 'day', 'inline' => true, 'dependsOn' => ['key' => 'time', 'operator' => 'neq', 'value' => 'Permanent'], 'options' => [
|
||||
['label' => '天', 'value' => 'day'],
|
||||
['label' => '月', 'value' => 'month'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
['key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true], 'help' => '用户使用积分兑换时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码'],
|
||||
]],
|
||||
['key' => 'modal-copy', 'title' => '界面文案', 'fields' => [
|
||||
['key' => 'pay_user_vip_desc', 'label' => '会员介绍', 'type' => 'input', 'default' => '开通VIP会员,享受会员专属折扣以及多项特权', 'dependsOn' => ['any' => [['key' => 'pay_user_vip_1_s', 'value' => true], ['key' => 'pay_user_vip_2_s', 'value' => true]]]],
|
||||
@@ -576,4 +631,91 @@ class SchemaPayment {
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员商品/续费商品的统一字段集(参考主题 CFS_Module::vip_product 1:1)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_product_fields(): array {
|
||||
return [
|
||||
['key' => 'price', 'label' => '执行价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '699.00', 'min' => 0, 'suffix' => '元', 'help' => '会员的实际购买价格'],
|
||||
['key' => 'show_price', 'label' => '原价', 'type' => 'number', 'sanitize_type' => 'money_decimal', 'default' => '999.00', 'min' => 0, 'suffix' => '元', 'help' => '显示在执行价格前面,并划掉'],
|
||||
['key' => 'tag', 'label' => '促销标签', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => '', 'rows' => 1, 'help' => '支持HTML,请注意控制长度'],
|
||||
['key' => 'tag_class', 'label' => '标签颜色', 'type' => 'palette', 'default' => 'jb-yellow', 'dependsOn' => ['key' => 'tag', 'operator' => 'not_empty'], 'options' => self::vip_tag_palette()],
|
||||
['key' => 'time', 'label' => '会员有效时间', 'type' => 'spinner', 'default' => 3, 'min' => 0, 'max' => 3600, 'step' => 1, 'help' => '开通会员的时长。填0则为永久会员'],
|
||||
['key' => 'unit', 'label' => '时长单位', 'type' => 'radio', 'default' => 'month', 'inline' => true, 'dependsOn' => ['key' => 'time', 'operator' => 'truthy'], 'options' => [
|
||||
['label' => '天', 'value' => 'day'],
|
||||
['label' => '月', 'value' => 'month'],
|
||||
]],
|
||||
['key' => '_time_permanent_hint', 'label' => '', 'type' => 'alert', 'alertType' => 'success', 'ui_only' => true, 'dependsOn' => ['key' => 'time', 'operator' => 'eq', 'value' => 0], 'content' => '会员有效时间已设置为:永久会员'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员等级 Tab 的字段集(参考主题 CFS_Module::vip_tab 1:1,显示名称并入 Tab)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_level_tab_fields(int $level): array {
|
||||
$dep = ['key' => 'pay_user_vip_' . $level . '_s', 'value' => true];
|
||||
$equity = '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>';
|
||||
return [
|
||||
['key' => 'pay_user_vip_' . $level . '_name', 'label' => '会员显示名称', 'type' => 'input', 'default' => $level === 1 ? '黄金会员' : '钻石会员', 'dependsOn' => $dep, 'help' => '会员名称(例如“黄金会员”、“超级会员”)'],
|
||||
['key' => 'pay_user_vip_' . $level . '_desc', 'label' => '一句话简介', 'type' => 'input', 'default' => '尊享专属特权', 'dependsOn' => $dep],
|
||||
['key' => 'pay_user_vip_' . $level . '_equity', 'label' => '会员权益 HTML', 'type' => 'textarea', 'sanitize_type' => 'html', 'default' => $equity, 'rows' => 4, 'dependsOn' => $dep, 'help' => '建议使用 <li> 列表项'],
|
||||
['key' => 'vip_' . $level . '_product_s', 'label' => '购买会员', 'type' => 'switch', 'default' => true, 'dependsOn' => $dep, 'help' => '关闭后则不能付费购买会员,但可通过积分兑换会员,请确保已开启积分以及积分兑换会员功能'],
|
||||
[
|
||||
'key' => 'vip_' . $level . '_product', 'label' => '会员商品', 'type' => 'group', 'max' => 8,
|
||||
'button_title' => '添加会员商品', 'default' => self::vip_product_defaults(),
|
||||
'dependsOn' => ['all' => [$dep, ['key' => 'vip_' . $level . '_product_s', 'value' => true]]],
|
||||
'help' => '会员购买价格配置:执行价、原价、促销标签、标签颜色与会员有效时间',
|
||||
'fields' => self::vip_product_fields(),
|
||||
],
|
||||
['key' => 'vip_' . $level . '_img_icon', 'label' => '会员图标', 'type' => 'upload', 'default' => '', 'dependsOn' => $dep, 'help' => '留空使用内置 vip-' . $level . '.svg 图标;建议尺寸300x300'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员商品默认两行(参考主题 vip_tab 的 group 默认值)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_product_defaults(): array {
|
||||
return [
|
||||
['price' => '99.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '199.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签颜色调色板(参考主题 zib_palette(array(), array('b','jb')))。
|
||||
*
|
||||
* @return array<int,array<string,string>>
|
||||
*/
|
||||
private static function vip_tag_palette(): array {
|
||||
$colors = [
|
||||
'b-red' => '#f74b3d',
|
||||
'b-yellow' => '#f3920a',
|
||||
'b-cyan' => '#08c4c1',
|
||||
'b-blue' => '#0a8cf3',
|
||||
'b-green' => '#1fd05a',
|
||||
'b-purple' => '#c133f5',
|
||||
'b-black' => '#121517',
|
||||
'jb-red' => 'linear-gradient(135deg, #ffbeb4 10%, #f61a1a 100%)',
|
||||
'jb-pink' => 'linear-gradient(135deg, #ff5e7f 30%, #ff967e 100%)',
|
||||
'jb-yellow' => 'linear-gradient(135deg, #ffd6b2 10%, #ff651c 100%)',
|
||||
'jb-cyan' => 'linear-gradient(140deg, #039ab3 10%, #58dbcf 90%)',
|
||||
'jb-blue' => 'linear-gradient(135deg, #b6e6ff 10%, #198aff 100%)',
|
||||
'jb-green' => 'linear-gradient(135deg, #ccffcd 10%, #52bb51 100%)',
|
||||
'jb-purple' => 'linear-gradient(135deg, #fec2ff 10%, #d000de 100%)',
|
||||
'jb-vip1' => 'linear-gradient(25deg, #eab869 10%, #fbecd4 60%, #ffe0ae 100%)',
|
||||
'jb-vip2' => 'linear-gradient(317deg, #4d4c4c 30%, #878787 70%, #5f5c5c 100%)',
|
||||
];
|
||||
$options = [];
|
||||
foreach ($colors as $value => $css) {
|
||||
$options[] = ['label' => $css, 'value' => $value];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
@@ -994,6 +994,12 @@ class SchemaProvider {
|
||||
if (!is_array($field) || empty($field['key'])) {
|
||||
continue;
|
||||
}
|
||||
// UI-only chrome (notice / alert rows) is presentation only and must
|
||||
// never become a persisted settings key, matching the top-level
|
||||
// behaviour of collect_persisted_fields().
|
||||
if (!empty($field['ui_only']) || ($field['type'] ?? '') === 'notice') {
|
||||
continue;
|
||||
}
|
||||
$key = (string) $field['key'];
|
||||
if (array_key_exists($key, $value)) {
|
||||
$clean[$key] = $this->sanitize_field_value($value[$key], $field);
|
||||
|
||||
@@ -214,11 +214,13 @@ trait VipServiceModal {
|
||||
}
|
||||
$level = max(1, min(2, (int) ($package['level'] ?? 1)));
|
||||
$points = max(0, (int) ($package['points'] ?? 0));
|
||||
$days = max(0, (int) ($package['days'] ?? 0));
|
||||
// 参考主题时长行:30<span class="px12">天</span> / 永久。
|
||||
$duration = $days > 0
|
||||
? (string) $days . '<span class="px12">' . esc_html__('天', 'aksrt') . '</span>'
|
||||
: esc_html__('永久', 'aksrt');
|
||||
// 参考主题时长行:30<span class="px12">天</span> / 永久;time 为 Permanent 时按永久渲染。
|
||||
$is_permanent = !empty($package['is_permanent']);
|
||||
$pkg_time = max(0, (int) ($package['time'] ?? 0));
|
||||
$pkg_unit = ((string) ($package['unit'] ?? 'day')) === 'month' ? 'month' : 'day';
|
||||
$duration = ($is_permanent || $pkg_time <= 0)
|
||||
? esc_html__('永久', 'aksrt')
|
||||
: (string) $pkg_time . '<span class="px12">' . esc_html($pkg_unit === 'month' ? __('个月', 'aksrt') : __('天', 'aksrt')) . '</span>';
|
||||
$level_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
||||
|
||||
$cards .= '<div class="vip-card vip-theme' . $level . ($index === 0 ? ' active' : '') . ' " data-for="product_id" data-value="' . $package_id . '">'
|
||||
|
||||
+120
-314
@@ -11,82 +11,6 @@ trait VipServicePackages {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Parse per-level product lines (legacy vip_product).
|
||||
*
|
||||
* Preferred: price|show_price|time|unit|tag|title
|
||||
* Legacy: price|days|title
|
||||
*
|
||||
* @return array<int, array{price:string,show_price:string,time:int,unit:string,tag:string,tag_class:string,title:string,days:int}>
|
||||
*/
|
||||
public function parse_level_products(string $raw): array {
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
$out = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
$parts = array_map('trim', explode('|', $line));
|
||||
$price_minor = $this->parse_vip_money_minor($parts[0] ?? '');
|
||||
if ($price_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$n = count($parts);
|
||||
// New format: price|show_price|time|unit|tag|title (6+)
|
||||
// Or: price|show_price|time|unit|title (5)
|
||||
if ($n >= 4 && (in_array(strtolower((string) ($parts[3] ?? '')), ['day', 'month', 'd', 'm'], true)
|
||||
|| (isset($parts[3]) && $parts[3] !== '' && !is_numeric($parts[3])))) {
|
||||
$show_minor = $this->parse_vip_money_minor($parts[1] ?? '0');
|
||||
$time = isset($parts[2]) ? (int) $parts[2] : 0;
|
||||
$unit_raw = strtolower((string) ($parts[3] ?? 'month'));
|
||||
$unit = ($unit_raw === 'day' || $unit_raw === 'd') ? 'day' : 'month';
|
||||
$tag = '';
|
||||
$title = '';
|
||||
if ($n >= 6) {
|
||||
$tag = sanitize_text_field((string) ($parts[4] ?? ''));
|
||||
$title = sanitize_text_field((string) ($parts[5] ?? ''));
|
||||
} elseif ($n === 5) {
|
||||
// price|show|time|unit|title
|
||||
$title = sanitize_text_field((string) ($parts[4] ?? ''));
|
||||
}
|
||||
} else {
|
||||
// Legacy: price|days|title
|
||||
$show_minor = 0;
|
||||
$days_legacy = isset($parts[1]) ? (int) $parts[1] : 0;
|
||||
$time = $days_legacy;
|
||||
$unit = 'day';
|
||||
$tag = '';
|
||||
$title = isset($parts[2]) ? sanitize_text_field($parts[2]) : '';
|
||||
}
|
||||
|
||||
$time = max(0, $time);
|
||||
$days = $this->duration_to_days($time, $unit);
|
||||
if ($title === '') {
|
||||
if ($time <= 0) {
|
||||
$title = __('永久会员', 'aksrt');
|
||||
} elseif ($unit === 'day') {
|
||||
$title = sprintf(__('%d天会员', 'aksrt'), $time);
|
||||
} else {
|
||||
$title = sprintf(__('%d个月会员', 'aksrt'), $time);
|
||||
}
|
||||
}
|
||||
|
||||
$out[] = [
|
||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor),
|
||||
'show_price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor),
|
||||
'time' => $time,
|
||||
'unit' => $unit,
|
||||
'tag' => $tag,
|
||||
'tag_class' => '',
|
||||
'title' => $title,
|
||||
'days' => $days,
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximate days for catalog labels / order meta; activate prefers time+unit calendar math.
|
||||
*/
|
||||
@@ -100,118 +24,59 @@ trait VipServicePackages {
|
||||
return $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use parse_level_products — kept as thin adapter for upgrade fixed lines.
|
||||
*
|
||||
* @return array<int, array{price:string,days:int,title:string}>
|
||||
*/
|
||||
public function parse_packages(string $raw): array {
|
||||
$parsed = $this->parse_level_products($raw);
|
||||
$out = [];
|
||||
foreach ($parsed as $p) {
|
||||
$out[] = [
|
||||
'price' => (string) $p['price'],
|
||||
'days' => (int) $p['days'],
|
||||
'title' => (string) $p['title'],
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Build upgrade package items (legacy vip_upgrade_product parity).
|
||||
*
|
||||
* upgrade.product multi-line formats:
|
||||
* - permanent VIP1: permanent|price|title[|show_price]
|
||||
* - fixed (alias): price|days|title
|
||||
* - timed unit: unit|daily_price|title
|
||||
* - timed jump permanent: jump|price|title[|show_price]
|
||||
*
|
||||
* Rules:
|
||||
* - permanent user: only permanent/fixed (days=0 preferred)
|
||||
* - timed user: unit (keep expiry) + optional jump (to permanent)
|
||||
*
|
||||
* package index: 901 permanent/fixed, 902 unit, 903 jump
|
||||
*
|
||||
* @return array<int, array<string,mixed>>
|
||||
*/
|
||||
private function build_upgrade_items(int $user_id, int $expires, bool $is_permanent, string $raw): array {
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
* Three blocks (reference theme vip_upgrade_product accordion):
|
||||
* - month_to_month_s: timed VIP1 → VIP2, priced by remaining days, expiry unchanged
|
||||
* - jump_s: timed VIP1 → permanent VIP2 at a fixed price
|
||||
* - permanent_*: permanent VIP1 → permanent VIP2 at a fixed price
|
||||
*
|
||||
* package index: 901 permanent, 902 unit, 903 jump
|
||||
*
|
||||
* @param array<string,mixed> $config
|
||||
* @return array<int, array<string,mixed>>
|
||||
*/
|
||||
private function build_upgrade_items(int $user_id, int $expires, bool $is_permanent, array $config): array {
|
||||
$items = [];
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
$has_permanent = false;
|
||||
$has_unit = false;
|
||||
$has_jump = false;
|
||||
$now = time();
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
// 月费会员升级月费会员:按剩余天数计价,会员有效期不变(仅提升等级)。
|
||||
if (!$is_permanent && $expires > $now && !empty($config['month_to_month_s'])) {
|
||||
$daily_minor = $this->parse_vip_money_minor((string) ($config['unit_price'] ?? ''));
|
||||
if ($daily_minor > 0) {
|
||||
$remaining = (int) ceil(max(1, ($expires - $now) / DAY_IN_SECONDS));
|
||||
if ($daily_minor <= intdiv(PHP_INT_MAX, $remaining)) {
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($remaining * $daily_minor);
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 902),
|
||||
'level' => 2,
|
||||
'index' => 902,
|
||||
'action' => 'upgrade',
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => '0.00',
|
||||
'discount_tag' => (string) ($config['unit_tag'] ?? ''),
|
||||
'tag_class' => (string) ($config['unit_tag_class'] ?? ''),
|
||||
'days' => $remaining,
|
||||
'title' => sprintf(__('按剩余 %d 天升级', 'aksrt'), $remaining),
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => '',
|
||||
'days_label' => sprintf(__('%d 天', 'aksrt'), $remaining),
|
||||
'cta_label' => __('立即升级', 'aksrt'),
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => 'unit',
|
||||
];
|
||||
}
|
||||
}
|
||||
$parts = array_map('trim', explode('|', $line));
|
||||
$mode = strtolower((string) ($parts[0] ?? ''));
|
||||
}
|
||||
|
||||
// unit|daily|title — timed VIP1 only.
|
||||
if ($mode === 'unit') {
|
||||
if ($is_permanent || $expires <= time() || $has_unit) {
|
||||
continue;
|
||||
}
|
||||
$daily_minor = $this->parse_vip_money_minor($parts[1] ?? '');
|
||||
if ($daily_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
$remaining = (int) ceil(max(1, ($expires - time()) / DAY_IN_SECONDS));
|
||||
if ($daily_minor > intdiv(PHP_INT_MAX, $remaining)) {
|
||||
continue;
|
||||
}
|
||||
$price_minor = $remaining * $daily_minor;
|
||||
// 月费会员升级永久会员:固定价格直接转为永久二级会员。
|
||||
if (!$is_permanent && !empty($config['jump_s'])) {
|
||||
$price_minor = $this->parse_vip_money_minor((string) ($config['jump_price'] ?? ''));
|
||||
if ($price_minor > 0) {
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = sprintf(__('按剩余 %d 天升级', 'aksrt'), $remaining);
|
||||
}
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 902),
|
||||
'level' => 2,
|
||||
'index' => 902,
|
||||
'action' => 'upgrade',
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => '0.00',
|
||||
'discount_tag' => __('剩余天数', 'aksrt'),
|
||||
'days' => $remaining,
|
||||
'title' => $title,
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => '',
|
||||
'days_label' => sprintf(__('%d 天', 'aksrt'), $remaining),
|
||||
'cta_label' => __('立即升级', 'aksrt'),
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => 'unit',
|
||||
];
|
||||
$has_unit = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// jump|price|title[|show_price] — timed VIP1 only → permanent VIP2.
|
||||
if ($mode === 'jump') {
|
||||
if ($is_permanent || $has_jump) {
|
||||
continue;
|
||||
}
|
||||
$price_minor = $this->parse_vip_money_minor($parts[1] ?? '');
|
||||
if ($price_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = __('升级为永久会员', 'aksrt');
|
||||
}
|
||||
$show_minor = $this->parse_vip_money_minor($parts[3] ?? '0');
|
||||
$show_minor = $this->parse_vip_money_minor((string) ($config['jump_show_price'] ?? ''));
|
||||
$show = \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor);
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 903),
|
||||
@@ -221,9 +86,10 @@ trait VipServicePackages {
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => $show,
|
||||
'discount_tag' => __('永久升级', 'aksrt'),
|
||||
'discount_tag' => (string) ($config['jump_tag'] ?? ''),
|
||||
'tag_class' => (string) ($config['jump_tag_class'] ?? ''),
|
||||
'days' => 0,
|
||||
'title' => $title,
|
||||
'title' => __('升级为永久会员', 'aksrt'),
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => $show_minor > 0 ? $this->format_cash_label($show) : '',
|
||||
'days_label' => __('永久', 'aksrt'),
|
||||
@@ -231,25 +97,15 @@ trait VipServicePackages {
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => 'jump',
|
||||
];
|
||||
$has_jump = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// permanent|price|title[|show_price]
|
||||
if ($mode === 'permanent') {
|
||||
if ($has_permanent) {
|
||||
continue;
|
||||
}
|
||||
$price_minor = $this->parse_vip_money_minor($parts[1] ?? '');
|
||||
if ($price_minor <= 0) {
|
||||
continue;
|
||||
}
|
||||
// 永久会员升级永久会员。
|
||||
if ($is_permanent) {
|
||||
$price_minor = $this->parse_vip_money_minor((string) ($config['permanent_price'] ?? ''));
|
||||
if ($price_minor > 0) {
|
||||
$price = \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor);
|
||||
$title = sanitize_text_field((string) ($parts[2] ?? ''));
|
||||
if ($title === '') {
|
||||
$title = __('永久会员升级', 'aksrt');
|
||||
}
|
||||
$show_minor = $this->parse_vip_money_minor($parts[3] ?? '0');
|
||||
$show_minor = $this->parse_vip_money_minor((string) ($config['permanent_show_price'] ?? ''));
|
||||
$show = \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor);
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 901),
|
||||
@@ -259,9 +115,10 @@ trait VipServicePackages {
|
||||
'price' => $price,
|
||||
'base_price' => $price,
|
||||
'show_price' => $show,
|
||||
'discount_tag' => __('升级专享', 'aksrt'),
|
||||
'discount_tag' => (string) ($config['permanent_tag'] ?? ''),
|
||||
'tag_class' => (string) ($config['permanent_tag_class'] ?? ''),
|
||||
'days' => 0,
|
||||
'title' => $title,
|
||||
'title' => __('永久会员升级', 'aksrt'),
|
||||
'price_label' => $this->format_cash_label($price),
|
||||
'show_price_label' => $show_minor > 0 ? $this->format_cash_label($show) : '',
|
||||
'days_label' => __('永久', 'aksrt'),
|
||||
@@ -269,49 +126,7 @@ trait VipServicePackages {
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => 'permanent',
|
||||
];
|
||||
$has_permanent = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// fixed: price|days|title
|
||||
$parsed = $this->parse_packages($line);
|
||||
if (empty($parsed[0]) || $has_permanent) {
|
||||
continue;
|
||||
}
|
||||
// For permanent VIP1, only accept days=0 fixed lines as permanent upgrade.
|
||||
if ($is_permanent && (int) $parsed[0]['days'] > 0) {
|
||||
continue;
|
||||
}
|
||||
$pkg = $parsed[0];
|
||||
$items[] = [
|
||||
'package_id' => $this->encode_package_id(2, 901),
|
||||
'level' => 2,
|
||||
'index' => 901,
|
||||
'action' => 'upgrade',
|
||||
'price' => (string) $pkg['price'],
|
||||
'base_price' => (string) $pkg['price'],
|
||||
'show_price' => '0.00',
|
||||
'discount_tag' => __('升级专享', 'aksrt'),
|
||||
'days' => (int) $pkg['days'],
|
||||
'title' => (string) $pkg['title'],
|
||||
'price_label' => $this->format_cash_label((string) $pkg['price']),
|
||||
'show_price_label' => '',
|
||||
'days_label' => ((int) $pkg['days'] <= 0)
|
||||
? __('永久', 'aksrt')
|
||||
: sprintf(__('%d 天', 'aksrt'), (int) $pkg['days']),
|
||||
'cta_label' => __('立即升级', 'aksrt'),
|
||||
'is_upgrade_product' => true,
|
||||
'upgrade_mode' => ((int) $pkg['days'] <= 0) ? 'permanent' : 'fixed',
|
||||
];
|
||||
$has_permanent = true;
|
||||
}
|
||||
|
||||
// Permanent VIP1 should not see unit/jump.
|
||||
if ($is_permanent) {
|
||||
$items = array_values(array_filter($items, static function ($it) {
|
||||
$m = (string) ($it['upgrade_mode'] ?? '');
|
||||
return in_array($m, ['permanent', 'fixed'], true);
|
||||
}));
|
||||
}
|
||||
|
||||
return $items;
|
||||
@@ -320,7 +135,10 @@ trait VipServicePackages {
|
||||
|
||||
|
||||
/**
|
||||
* Points exchange packages: level|points|days|title
|
||||
* Points exchange packages from the structured 积分兑换明细 group.
|
||||
*
|
||||
* Each row is points / level / time / unit, where time is the literal
|
||||
* string "Permanent" for a permanent grant (reference theme contract).
|
||||
*
|
||||
* @param array<string,mixed> $vip
|
||||
* @param array<int,mixed> $levels
|
||||
@@ -328,47 +146,49 @@ trait VipServicePackages {
|
||||
*/
|
||||
public function get_points_exchange_packages(array $vip, array $levels): array {
|
||||
$points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : [];
|
||||
$raw = (string) ($points_ex['packages_raw'] ?? '');
|
||||
$rows = isset($points_ex['packages']) && is_array($points_ex['packages']) ? $points_ex['packages'] : [];
|
||||
$currency = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
|
||||
$out = [];
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
$i = 0;
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '') {
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$parts = array_map('trim', explode('|', $line));
|
||||
if (count($parts) < 3) {
|
||||
continue;
|
||||
}
|
||||
$lv = (int) ($parts[0] ?? 0);
|
||||
$pts = (int) ($parts[1] ?? 0);
|
||||
$days = (int) ($parts[2] ?? 0);
|
||||
$title = sanitize_text_field((string) ($parts[3] ?? ''));
|
||||
$lv = (int) ($row['level'] ?? 0);
|
||||
$pts = max(0, (int) ($row['points'] ?? 0));
|
||||
if ($lv < 1 || $lv > 2 || $pts <= 0) {
|
||||
continue;
|
||||
}
|
||||
if (empty($levels[$lv]['enabled'])) {
|
||||
continue;
|
||||
}
|
||||
$i++;
|
||||
if ($title === '') {
|
||||
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||
$title = $days > 0
|
||||
? sprintf(__('%1$s · %2$d天', 'aksrt'), $lv_name, $days)
|
||||
: sprintf(__('%s · 永久', 'aksrt'), $lv_name);
|
||||
$is_permanent = !empty($row['is_permanent']);
|
||||
$time = max(0, (int) ($row['time'] ?? 0));
|
||||
$unit = ((string) ($row['unit'] ?? 'day')) === 'month' ? 'month' : 'day';
|
||||
$days = $is_permanent ? 0 : max(0, (int) ($row['days'] ?? 0));
|
||||
if (!$is_permanent && $time <= 0) {
|
||||
continue;
|
||||
}
|
||||
$i++;
|
||||
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||
$out[] = [
|
||||
'package_id' => 5000 + $i,
|
||||
'level' => $lv,
|
||||
'index' => $i,
|
||||
'points' => $pts,
|
||||
'days' => max(0, $days),
|
||||
'title' => $title,
|
||||
'price_label' => $pts . ' ' . $currency,
|
||||
'days_label' => $days > 0 ? sprintf(__('%d 天', 'aksrt'), $days) : __('永久', 'aksrt'),
|
||||
'cta_label' => __('立即兑换', 'aksrt'),
|
||||
'package_id' => 5000 + $i,
|
||||
'level' => $lv,
|
||||
'index' => $i,
|
||||
'points' => $pts,
|
||||
'days' => $days,
|
||||
'time' => $time,
|
||||
'unit' => $unit,
|
||||
'is_permanent' => $is_permanent,
|
||||
'title' => $is_permanent
|
||||
? sprintf(__('%s · 永久', 'aksrt'), $lv_name)
|
||||
: sprintf(__('%1$s · %2$d%3$s', 'aksrt'), $lv_name, $time, $unit === 'month' ? __('个月', 'aksrt') : __('天', 'aksrt')),
|
||||
'price_label' => $pts . ' ' . $currency,
|
||||
'days_label' => $is_permanent
|
||||
? __('永久', 'aksrt')
|
||||
: ($unit === 'month' ? sprintf(__('%d 个月', 'aksrt'), $time) : sprintf(__('%d 天', 'aksrt'), $time)),
|
||||
'duration_label' => $is_permanent ? __('永久', 'aksrt') : ($unit === 'month' ? sprintf(__('%d 个月', 'aksrt'), $time) : sprintf(__('%d 天', 'aksrt'), $time)),
|
||||
'cta_label' => __('立即兑换', 'aksrt'),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
@@ -376,41 +196,18 @@ trait VipServicePackages {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Single upgrade product line: price|days|title
|
||||
*
|
||||
* @return array{price:float,days:int,title:string}|null
|
||||
*/
|
||||
public function parse_upgrade_product_line(string $raw): ?array {
|
||||
$raw = trim($raw);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '' || str_starts_with(strtolower($line), 'unit|')) {
|
||||
continue;
|
||||
}
|
||||
$parsed = $this->parse_packages($line);
|
||||
if (!empty($parsed[0])) {
|
||||
return $parsed[0];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Apply renew discount/reduce to same-level packages.
|
||||
*
|
||||
* `customize` reuses the dedicated renew product's own price/show_price/tag,
|
||||
* so the caller passes that row's whole item in as $pkg.
|
||||
*
|
||||
* @param array<string,mixed> $renew
|
||||
* @param array<string,mixed> $upgrade
|
||||
* @return array{price:string,show_price:string,tag:string}|null
|
||||
* @return array{price:string,show_price:string,tag:string,tag_class:string}|null
|
||||
*/
|
||||
private function apply_action_price(
|
||||
string $base_price,
|
||||
array $pkg,
|
||||
string $action,
|
||||
array $renew,
|
||||
array $upgrade,
|
||||
@@ -420,6 +217,7 @@ trait VipServicePackages {
|
||||
bool $is_permanent,
|
||||
int $days
|
||||
): ?array {
|
||||
$base_price = (string) ($pkg['price'] ?? '');
|
||||
$base_minor = $this->parse_vip_money_minor($base_price);
|
||||
if ($base_minor <= 0) {
|
||||
return null;
|
||||
@@ -434,32 +232,31 @@ trait VipServicePackages {
|
||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($base_minor),
|
||||
'show_price' => '0.00',
|
||||
'tag' => '',
|
||||
'tag_class' => '',
|
||||
];
|
||||
}
|
||||
|
||||
$type = sanitize_key((string) ($renew['price_type'] ?? 'discount'));
|
||||
// discount/reduce 的标签由优惠幅度计算得出,若实际没有优惠则属于虚假折扣。
|
||||
$computed_tag = in_array($type, ['discount', 'reduce'], true);
|
||||
$price_minor = $base_minor;
|
||||
$show_minor = $base_minor;
|
||||
$tag = '';
|
||||
$tag_class = '';
|
||||
|
||||
if ($type === 'discount') {
|
||||
$discount = (int) ($renew['discount'] ?? 8);
|
||||
$discount = (float) ($renew['discount'] ?? 8);
|
||||
if ($discount <= 0) {
|
||||
$discount = 8;
|
||||
}
|
||||
if ($discount <= 10) {
|
||||
$percent = $discount * 10;
|
||||
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $percent);
|
||||
$tag = sprintf(
|
||||
/* translators: %s: discount like 8 */
|
||||
__('%s折续费', 'aksrt'),
|
||||
(string) $discount
|
||||
);
|
||||
} else {
|
||||
$percent = min(100, $discount);
|
||||
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $percent);
|
||||
$tag = sprintf(__('%s%% 续费价', 'aksrt'), (string) (int) $discount);
|
||||
}
|
||||
// 折值:8 = 八折(参考主题 slider 0.1–9.9 语义)。
|
||||
$discount = max(0.1, min(9.9, $discount));
|
||||
$price_minor = \AKSRT\Ledger\AssetAmount::percentage($base_minor, $discount * 10);
|
||||
$tag = sprintf(
|
||||
/* translators: %s: discount like 8 */
|
||||
__('%s折优惠', 'aksrt'),
|
||||
rtrim(rtrim(number_format($discount, 1, '.', ''), '0'), '.') ?: '0'
|
||||
);
|
||||
} elseif ($type === 'reduce') {
|
||||
$reduce_minor = $this->parse_vip_money_minor($renew['reduce'] ?? '20');
|
||||
$price_minor = max(1, $base_minor - $reduce_minor);
|
||||
@@ -468,6 +265,11 @@ trait VipServicePackages {
|
||||
__('续费立减%s', 'aksrt'),
|
||||
$this->format_cash_label(\AKSRT\Ledger\AssetAmount::decimal_from_minor($reduce_minor))
|
||||
);
|
||||
} elseif ($type === 'customize') {
|
||||
// 自定义商品:沿用商品自身的原价、促销标签与标签颜色。
|
||||
$show_minor = $this->parse_vip_money_minor((string) ($pkg['show_price'] ?? ''));
|
||||
$tag = (string) ($pkg['tag'] ?? '');
|
||||
$tag_class = (string) ($pkg['tag_class'] ?? '');
|
||||
} else {
|
||||
$show_minor = 0;
|
||||
$tag = __('续费同价', 'aksrt');
|
||||
@@ -477,14 +279,18 @@ trait VipServicePackages {
|
||||
$price_minor = 1;
|
||||
}
|
||||
if ($show_minor > 0 && $price_minor >= $show_minor) {
|
||||
// 参考主题契约:原价仅在低于执行价时展示。
|
||||
$show_minor = 0;
|
||||
$tag = __('续费', 'aksrt');
|
||||
if ($computed_tag) {
|
||||
$tag = __('续费', 'aksrt');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($price_minor),
|
||||
'show_price' => \AKSRT\Ledger\AssetAmount::decimal_from_minor($show_minor > $price_minor ? $show_minor : 0),
|
||||
'tag' => $tag,
|
||||
'tag_class' => $tag_class,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -139,20 +139,30 @@ trait VipServicePurchase {
|
||||
|
||||
$name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
|
||||
$items = [];
|
||||
$packages = $product_s
|
||||
? $this->parse_level_products((string) ($levels[$lv]['products_raw'] ?? ''))
|
||||
: [];
|
||||
// 自定义续费商品时按续费商品组计价,否则按会员商品组计价。
|
||||
$level_action = 'pay';
|
||||
if ($level > 0 && $lv === $level) {
|
||||
$level_action = 'renew';
|
||||
} elseif ($level > 0 && $lv > $level) {
|
||||
$level_action = 'upgrade';
|
||||
}
|
||||
$renew_custom = $level_action === 'renew'
|
||||
&& sanitize_key((string) ($renew['price_type'] ?? 'discount')) === 'customize';
|
||||
$packages = [];
|
||||
if ($product_s) {
|
||||
$packages = $renew_custom
|
||||
? (array) ($levels[$lv]['renew_products'] ?? [])
|
||||
: (array) ($levels[$lv]['products'] ?? []);
|
||||
}
|
||||
foreach ($packages as $idx => $pkg) {
|
||||
if (!is_array($pkg)) {
|
||||
continue;
|
||||
}
|
||||
$base_price = (string) $pkg['price'];
|
||||
$days = (int) $pkg['days'];
|
||||
$action = 'pay';
|
||||
if ($level > 0 && $lv === $level) {
|
||||
$action = 'renew';
|
||||
} elseif ($level > 0 && $lv > $level) {
|
||||
$action = 'upgrade';
|
||||
}
|
||||
$action = $level_action;
|
||||
|
||||
$priced = $this->apply_action_price($base_price, $action, $renew, $upgrade, $lv, $level, $expires, $is_permanent, $days);
|
||||
$priced = $this->apply_action_price($pkg, $action, $renew, $upgrade, $lv, $level, $expires, $is_permanent, $days);
|
||||
if ($priced === null) {
|
||||
continue;
|
||||
}
|
||||
@@ -167,6 +177,10 @@ trait VipServicePurchase {
|
||||
if ($discount_tag === '' && $action === 'pay' && !empty($pkg['tag'])) {
|
||||
$discount_tag = (string) $pkg['tag'];
|
||||
}
|
||||
$tag_class = (string) ($priced['tag_class'] ?? '');
|
||||
if ($tag_class === '' && !empty($pkg['tag_class'])) {
|
||||
$tag_class = (string) $pkg['tag_class'];
|
||||
}
|
||||
|
||||
$days_label = __('永久', 'aksrt');
|
||||
if ((int) $pkg['time'] > 0) {
|
||||
@@ -184,10 +198,10 @@ trait VipServicePurchase {
|
||||
'base_price' => $base_price,
|
||||
'show_price' => $show_price,
|
||||
'discount_tag' => $discount_tag,
|
||||
'tag_class' => $tag_class,
|
||||
'days' => $days,
|
||||
'time' => (int) ($pkg['time'] ?? 0),
|
||||
'unit' => (string) ($pkg['unit'] ?? 'day'),
|
||||
'title' => (string) $pkg['title'],
|
||||
'price_label' => $this->format_cash_label((string) $priced['price']),
|
||||
'show_price_label' => $show_minor > 0
|
||||
? $this->format_cash_label($show_price)
|
||||
@@ -201,7 +215,7 @@ trait VipServicePurchase {
|
||||
|
||||
// Dedicated upgrade product: fixed or remaining-days unit price.
|
||||
if ($level === 1 && $lv === 2 && $upgrade_enabled) {
|
||||
$upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? ''));
|
||||
$upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, $upgrade);
|
||||
foreach ($upgrade_items as $ui) {
|
||||
$items[] = $ui;
|
||||
}
|
||||
@@ -441,11 +455,12 @@ trait VipServicePurchase {
|
||||
'note' => is_string($note) ? $note : '',
|
||||
'expires_at' => $expires_at,
|
||||
'subject' => sprintf(
|
||||
/* translators: 1: action 2: vip name 3: package title */
|
||||
__('%1$s%2$s · %3$s', 'aksrt'),
|
||||
/* translators: 1: action 2: vip name 3: price label 4: duration label */
|
||||
__('%1$s%2$s · %3$s / %4$s', 'aksrt'),
|
||||
$action_label,
|
||||
$level_name,
|
||||
(string) ($pkg['title'] ?? '')
|
||||
(string) ($pkg['price_label'] ?? ''),
|
||||
(string) ($pkg['days_label'] ?? '')
|
||||
),
|
||||
];
|
||||
|
||||
@@ -757,6 +772,11 @@ trait VipServicePurchase {
|
||||
$cost = (int) ($match['points'] ?? 0);
|
||||
$level = (int) ($match['level'] ?? 1);
|
||||
$days = (int) ($match['days'] ?? 0);
|
||||
$pkg_time = max(0, (int) ($match['time'] ?? 0));
|
||||
$pkg_unit = sanitize_key((string) ($match['unit'] ?? 'day'));
|
||||
if (!in_array($pkg_unit, ['day', 'month'], true)) {
|
||||
$pkg_unit = 'day';
|
||||
}
|
||||
if ($cost <= 0) {
|
||||
return new \WP_Error('aksrt_invalid_price', __('兑换积分无效', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
@@ -779,6 +799,8 @@ trait VipServicePurchase {
|
||||
'vip_pay_type' => 'points_exchange',
|
||||
'level' => $level,
|
||||
'days' => $days,
|
||||
'time' => $pkg_time,
|
||||
'unit' => $pkg_unit,
|
||||
'points' => $cost,
|
||||
'package_id' => $package_id,
|
||||
'package_title' => (string) ($match['title'] ?? ''),
|
||||
|
||||
@@ -210,42 +210,6 @@ class HomeCms {
|
||||
return $settings['home_exclude_cats'];
|
||||
}
|
||||
|
||||
public static function get_paid_content_settings(): array {
|
||||
return PaymentRuntime::get_paid_content_settings();
|
||||
}
|
||||
|
||||
public static function get_content_coupon_settings(): array {
|
||||
return PaymentRuntime::get_content_coupon_settings();
|
||||
}
|
||||
|
||||
public static function get_payment_recharge_settings(): array {
|
||||
return PaymentRuntime::get_payment_recharge_settings();
|
||||
}
|
||||
|
||||
public static function get_payment_vip_settings(): array {
|
||||
return PaymentRuntime::get_payment_vip_settings();
|
||||
}
|
||||
|
||||
public static function get_payment_revenue_settings(): array {
|
||||
return PaymentRuntime::get_payment_revenue_settings();
|
||||
}
|
||||
|
||||
public static function get_payment_affiliate_settings(): array {
|
||||
return PaymentRuntime::get_payment_affiliate_settings();
|
||||
}
|
||||
|
||||
public static function get_payment_gateway_settings(): array {
|
||||
return PaymentRuntime::get_payment_gateway_settings();
|
||||
}
|
||||
|
||||
public static function get_order_settings(): array {
|
||||
return PaymentRuntime::get_order_settings();
|
||||
}
|
||||
|
||||
public static function get_withdrawal_settings(): array {
|
||||
return PaymentRuntime::get_withdrawal_settings();
|
||||
}
|
||||
|
||||
private static function normalize_sort_options(array $raw): array {
|
||||
$allowed = self::homepage_allowed_sorts();
|
||||
$result = [];
|
||||
|
||||
+191
-23
@@ -182,22 +182,10 @@ class PaymentRuntime {
|
||||
$badge = $vip2_enabled ? $name2 : ($vip1_enabled ? $name1 : __('VIP', 'aksrt'));
|
||||
|
||||
$renew_price_type = sanitize_key((string) ($settings['vip_renew_price_type'] ?? 'discount'));
|
||||
if (!in_array($renew_price_type, ['same', 'discount', 'reduce'], true)) {
|
||||
if (!in_array($renew_price_type, ['same', 'discount', 'reduce', 'customize'], true)) {
|
||||
$renew_price_type = 'discount';
|
||||
}
|
||||
|
||||
$default_l1 = "30|59|1|month|限时|月卡\n98|199|3|month|特惠|季卡\n365|599|12|month|超值|年卡\n999|1299|0|day|永久|永久卡";
|
||||
$default_l2 = "58|99|1|month|限时|月卡\n168|299|3|month|特惠|季卡\n598|999|12|month|超值|年卡\n1299|1999|0|day|永久|永久卡";
|
||||
$products_1 = sanitize_textarea_field((string) ($settings['payment_vip_1_products'] ?? $default_l1));
|
||||
$products_2 = sanitize_textarea_field((string) ($settings['payment_vip_2_products'] ?? $default_l2));
|
||||
// Restore newlines lost only if empty after sanitize — sanitize_textarea_field keeps \n.
|
||||
if (trim($products_1) === '') {
|
||||
$products_1 = $default_l1;
|
||||
}
|
||||
if (trim($products_2) === '') {
|
||||
$products_2 = $default_l2;
|
||||
}
|
||||
|
||||
$desc = sanitize_text_field((string) ($settings['pay_user_vip_desc'] ?? __('开通VIP会员,享受会员专属折扣以及多项特权', 'aksrt')));
|
||||
$more = (string) ($settings['pay_user_vip_more'] ?? '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>');
|
||||
$allowed_equity = [
|
||||
@@ -231,7 +219,8 @@ class PaymentRuntime {
|
||||
'equity' => wp_kses((string) (($settings['pay_user_vip_1_equity'] ?? '') !== '' ? $settings['pay_user_vip_1_equity'] : '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>'), $allowed_equity),
|
||||
'img_icon' => self::get_vip_icon_url(1, (string) ($settings['vip_1_img_icon'] ?? '')),
|
||||
'product_s' => !isset($settings['vip_1_product_s']) || !empty($settings['vip_1_product_s']),
|
||||
'products_raw' => $products_1,
|
||||
'products' => self::normalize_vip_product_items($settings['vip_1_product'] ?? null, self::vip_product_defaults()),
|
||||
'renew_products' => self::normalize_vip_product_items($settings['vip_1_renew_product'] ?? null, self::vip_renew_product_defaults(1)),
|
||||
'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit_vip_1'] ?? 0))),
|
||||
],
|
||||
2 => [
|
||||
@@ -241,7 +230,8 @@ class PaymentRuntime {
|
||||
'equity' => wp_kses((string) (($settings['pay_user_vip_2_equity'] ?? '') !== '' ? $settings['pay_user_vip_2_equity'] : '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>'), $allowed_equity),
|
||||
'img_icon' => self::get_vip_icon_url(2, (string) ($settings['vip_2_img_icon'] ?? '')),
|
||||
'product_s' => !isset($settings['vip_2_product_s']) || !empty($settings['vip_2_product_s']),
|
||||
'products_raw' => $products_2,
|
||||
'products' => self::normalize_vip_product_items($settings['vip_2_product'] ?? null, self::vip_product_defaults()),
|
||||
'renew_products' => self::normalize_vip_product_items($settings['vip_2_renew_product'] ?? null, self::vip_renew_product_defaults(2)),
|
||||
'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit_vip_2'] ?? 0))),
|
||||
],
|
||||
],
|
||||
@@ -250,19 +240,16 @@ class PaymentRuntime {
|
||||
],
|
||||
'renew' => [
|
||||
'enabled' => !isset($settings['vip_renew']) || !empty($settings['vip_renew']),
|
||||
'desc' => sanitize_text_field((string) ($settings['vip_renew_desc'] ?? '立即续费会员,畅享VIP权益')),
|
||||
'price_type' => $renew_price_type,
|
||||
'discount' => max(1, min(100, (int) ($settings['vip_renew_discount'] ?? 8))),
|
||||
'reduce' => self::money_decimal($settings['vip_renew_reduce'] ?? '20.00', '20.00', 999900),
|
||||
],
|
||||
'upgrade' => [
|
||||
'enabled' => !isset($settings['vip_upgrade']) || !empty($settings['vip_upgrade']),
|
||||
// Multi-line upgrade product rules — must not collapse newlines.
|
||||
'product' => sanitize_textarea_field((string) ($settings['vip_upgrade_product'] ?? '')),
|
||||
'discount' => max(0.1, min(9.9, (float) ($settings['vip_renew_discount'] ?? 8))),
|
||||
'reduce' => self::money_decimal($settings['vip_renew_reduce'] ?? '20.00', '20.00', 50000),
|
||||
],
|
||||
'upgrade' => self::normalize_vip_upgrade_settings($settings),
|
||||
'points_exchange' => [
|
||||
'enabled' => !empty($settings['pay_vip_points_exchange_s']),
|
||||
'desc' => wp_kses_post((string) ($settings['pay_vip_points_exchange_desc'] ?? '')),
|
||||
'packages_raw' => sanitize_textarea_field((string) ($settings['payment_vip_points_packages'] ?? "1|500|30|积分月卡\n1|5000|0|积分永久")),
|
||||
'packages' => self::normalize_vip_points_packages($settings['pay_vip_points_exchange_product'] ?? null),
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -729,4 +716,185 @@ class PaymentRuntime {
|
||||
}
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximate days for catalog labels / order meta; activation prefers
|
||||
* calendar time+unit math.
|
||||
*/
|
||||
private static function duration_to_days(int $time, string $unit = 'day'): int {
|
||||
if ($time <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return $unit === 'month' ? $time * 30 : $time;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会员商品默认两行(参考主题 vip_tab 的 group 默认值)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_product_defaults(): array {
|
||||
return [
|
||||
['price' => '99.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '199.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_renew_product_defaults(int $level): array {
|
||||
if ($level === 2) {
|
||||
return [
|
||||
['price' => '269.00', 'show_price' => '599.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '369.00', 'show_price' => '899.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
];
|
||||
}
|
||||
return [
|
||||
['price' => '69.00', 'show_price' => '199.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', 'time' => 3, 'unit' => 'month'],
|
||||
['price' => '169.00', 'show_price' => '299.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', 'time' => 6, 'unit' => 'month'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签颜色可选值(参考主题 zib_palette(array(), array('b','jb')))。
|
||||
*
|
||||
* @return array<int,string>
|
||||
*/
|
||||
private static function vip_tag_palette_keys(): array {
|
||||
return ['b-red', 'b-yellow', 'b-cyan', 'b-blue', 'b-green', 'b-purple', 'b-black', 'jb-red', 'jb-pink', 'jb-yellow', 'jb-cyan', 'jb-blue', 'jb-green', 'jb-purple', 'jb-vip1', 'jb-vip2'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a structured 会员商品 group into purchasable rows.
|
||||
*
|
||||
* @param mixed $raw Stored group value.
|
||||
* @param array<int,array<string,mixed>> $fallback Default rows when unset.
|
||||
* @return array<int,array{price:string,show_price:string,tag:string,tag_class:string,time:int,unit:string,days:int}>
|
||||
*/
|
||||
private static function normalize_vip_product_items($raw, array $fallback): array {
|
||||
$rows = is_array($raw) ? array_values($raw) : [];
|
||||
if ($rows === []) {
|
||||
$rows = $fallback;
|
||||
}
|
||||
$palette = self::vip_tag_palette_keys();
|
||||
$out = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$price = self::money_decimal($row['price'] ?? '', '0.00');
|
||||
if (bccomp($price, '0.00', 2) <= 0) {
|
||||
continue;
|
||||
}
|
||||
$time = max(0, (int) ($row['time'] ?? 0));
|
||||
$unit = ((string) ($row['unit'] ?? 'month')) === 'day' ? 'day' : 'month';
|
||||
$tag_class = (string) ($row['tag_class'] ?? '');
|
||||
if (!in_array($tag_class, $palette, true)) {
|
||||
$tag_class = '';
|
||||
}
|
||||
$out[] = [
|
||||
'price' => $price,
|
||||
'show_price' => self::money_decimal($row['show_price'] ?? '', '0.00'),
|
||||
'tag' => wp_kses_post((string) ($row['tag'] ?? '')),
|
||||
'tag_class' => $tag_class,
|
||||
'time' => $time,
|
||||
'unit' => $unit,
|
||||
'days' => self::duration_to_days($time, $unit),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the structured 升级价格 accordion.
|
||||
*
|
||||
* @param array<string,mixed> $settings
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function normalize_vip_upgrade_settings(array $settings): array {
|
||||
$raw = isset($settings['vip_upgrade_product']) && is_array($settings['vip_upgrade_product'])
|
||||
? $settings['vip_upgrade_product']
|
||||
: [];
|
||||
$palette = self::vip_tag_palette_keys();
|
||||
$money = static function ($value, string $default): string {
|
||||
return self::money_decimal(is_string($value) ? $value : (is_numeric($value) ? (string) $value : ''), $default);
|
||||
};
|
||||
$tag = static function ($value, string $default): string {
|
||||
$text = trim((string) $value);
|
||||
return $text !== '' ? wp_kses_post($text) : $default;
|
||||
};
|
||||
$tag_class = static function ($value) use ($palette): string {
|
||||
$key = (string) $value;
|
||||
return in_array($key, $palette, true) ? $key : '';
|
||||
};
|
||||
$switch = static function ($value, bool $default): bool {
|
||||
return isset($value) ? !empty($value) : $default;
|
||||
};
|
||||
|
||||
return [
|
||||
'enabled' => !isset($settings['vip_upgrade']) || !empty($settings['vip_upgrade']),
|
||||
'desc' => sanitize_text_field((string) ($settings['vip_upgrade_desc'] ?? '升级VIP会员,享更多会员权益')),
|
||||
'month_to_month_s' => $switch($raw['month_to_month_s'] ?? null, true),
|
||||
'unit_price' => $money($raw['unit_price'] ?? null, '10.00'),
|
||||
'unit_tag' => $tag($raw['unit_tag'] ?? null, '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐'),
|
||||
'unit_tag_class' => $tag_class($raw['unit_tag_class'] ?? 'jb-yellow'),
|
||||
'jump_s' => $switch($raw['jump_s'] ?? null, false),
|
||||
'jump_price' => $money($raw['jump_price'] ?? null, '199.00'),
|
||||
'jump_show_price' => $money($raw['jump_show_price'] ?? null, '299.00'),
|
||||
'jump_tag' => $tag($raw['jump_tag'] ?? null, '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠'),
|
||||
'jump_tag_class' => $tag_class($raw['jump_tag_class'] ?? 'jb-red'),
|
||||
'permanent_price' => $money($raw['permanent_price'] ?? null, '199.00'),
|
||||
'permanent_show_price' => $money($raw['permanent_show_price'] ?? null, '299.00'),
|
||||
'permanent_tag' => $tag($raw['permanent_tag'] ?? null, '<i class="fa fas fa-fw fa-bolt"></i> 升级特惠'),
|
||||
'permanent_tag_class' => $tag_class($raw['permanent_tag_class'] ?? 'jb-red'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 积分兑换明细默认行(对齐旧 textarea 的开箱配置)。
|
||||
*
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
private static function vip_points_package_defaults(): array {
|
||||
return [
|
||||
['points' => 500, 'level' => 1, 'time' => 30, 'unit' => 'day'],
|
||||
['points' => 5000, 'level' => 1, 'time' => 'Permanent'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the structured 积分兑换明细 group. `time` is the literal string
|
||||
* "Permanent" for a permanent grant, otherwise a day/month count.
|
||||
*
|
||||
* @param mixed $raw
|
||||
* @return array<int,array{points:int,level:int,time:int,unit:string,is_permanent:bool,days:int}>
|
||||
*/
|
||||
private static function normalize_vip_points_packages($raw): array {
|
||||
$rows = is_array($raw) ? array_values($raw) : self::vip_points_package_defaults();
|
||||
$out = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$points = max(0, (int) ($row['points'] ?? 0));
|
||||
$level = (int) ($row['level'] ?? 1);
|
||||
$time_raw = trim((string) ($row['time'] ?? ''));
|
||||
if ($points <= 0 || $level < 1 || $level > 2 || $time_raw === '') {
|
||||
continue;
|
||||
}
|
||||
$is_permanent = strcasecmp($time_raw, 'Permanent') === 0;
|
||||
$time = $is_permanent ? 0 : max(0, (int) $time_raw);
|
||||
$unit = ((string) ($row['unit'] ?? 'day')) === 'month' ? 'month' : 'day';
|
||||
$out[] = [
|
||||
'points' => $points,
|
||||
'level' => $level,
|
||||
'time' => $time,
|
||||
'unit' => $unit,
|
||||
'is_permanent' => $is_permanent,
|
||||
'days' => $is_permanent ? 0 : self::duration_to_days($time, $unit),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
@@ -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'"));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* VIP 后台设置结构化契约(对照参考主题 zibll 1:1)。
|
||||
*
|
||||
* 硬约束:
|
||||
* 1) 会员商品 / 续费商品 / 升级价格 / 积分兑换明细全部为结构化 group 或
|
||||
* accordion,`|` 分隔的 textarea 配置已彻底移除(不写双路/迁移);
|
||||
* 2) 「会员等级」组只留导航按钮与两个等级开关,等级身份字段(显示名称/
|
||||
* 简介/权益HTML/图标URL)与「会员商品」同处「会员购买选项」的等级 Tab;
|
||||
* 3) 会员商品字段集恰好 6 项:执行价/原价/促销标签/标签颜色/会员有效时间/
|
||||
* 时长单位;标签颜色为参考主题 b+jb 全量 16 色;
|
||||
* 4) 续费价格类型含「自定义商品」,升级价格为参考主题同款三块 accordion;
|
||||
* 5) tag_class 一路透传到前端套餐卡;等级 Tab 标题由显示名称动态驱动。
|
||||
*/
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const root = path.join(__dirname, '..', '..');
|
||||
const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8');
|
||||
|
||||
const vipSchemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'vip.js');
|
||||
const schemaPhp = read('src', 'Admin', 'Schema', 'SchemaPayment.php');
|
||||
const paymentRuntime = read('src', 'Settings', 'PaymentRuntime.php');
|
||||
const packagesTrait = read('src', 'Content', 'VipServicePackages.php');
|
||||
const purchaseSource = read('src', 'Content', 'VipServicePurchase.php');
|
||||
const modalSource = read('src', 'Content', 'VipServiceModal.php');
|
||||
const rendererVue = read('assets', 'admin', 'src', 'components', 'form', 'AksFormRenderer.vue');
|
||||
const payVipJs = read('assets', 'js', 'pay-vip.js');
|
||||
const vipTabPhp = read('inc', 'helpers', 'user-center-renderers', 'vip-tab.php');
|
||||
const homeCms = read('src', 'Settings', 'HomeCms.php');
|
||||
|
||||
const TAG_PALETTE = [
|
||||
'b-red', 'b-yellow', 'b-cyan', 'b-blue', 'b-green', 'b-purple', 'b-black',
|
||||
'jb-red', 'jb-pink', 'jb-yellow', 'jb-cyan', 'jb-blue', 'jb-green', 'jb-purple',
|
||||
'jb-vip1', 'jb-vip2',
|
||||
];
|
||||
const PRODUCT_FIELDS = ['price', 'show_price', 'tag', 'tag_class', 'time', 'unit'];
|
||||
const UPGRADE_FIELDS = [
|
||||
'month_to_month_s', 'unit_price', 'unit_tag', 'unit_tag_class',
|
||||
'jump_s', 'jump_price', 'jump_show_price', 'jump_tag', 'jump_tag_class',
|
||||
'permanent_price', 'permanent_show_price', 'permanent_tag', 'permanent_tag_class',
|
||||
];
|
||||
const LEGACY_KEYS = [
|
||||
'payment_vip_1_products',
|
||||
'payment_vip_2_products',
|
||||
'payment_vip_points_packages',
|
||||
];
|
||||
|
||||
function runHarness() {
|
||||
const out = execFileSync(process.env.PHP_BINARY || 'php', [
|
||||
path.join(__dirname, 'vip-settings-structured-harness.php'),
|
||||
], { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
|
||||
return JSON.parse(out);
|
||||
}
|
||||
|
||||
// 只切 VIP 设置函数:充值/积分设置里的 products_raw 与本文无关。
|
||||
function vipSettingsSlice() {
|
||||
const start = paymentRuntime.indexOf('public static function get_payment_vip_settings');
|
||||
assert.notStrictEqual(start, -1, 'missing get_payment_vip_settings');
|
||||
const end = paymentRuntime.indexOf('public static function get_payment_revenue_settings', start);
|
||||
return paymentRuntime.slice(start, end === -1 ? undefined : end);
|
||||
}
|
||||
|
||||
let harness;
|
||||
test('PHP harness 可执行并产出结构化设置', () => {
|
||||
harness = runHarness();
|
||||
assert.ok(harness && harness.schema && harness.runtime, 'harness 输出不完整');
|
||||
});
|
||||
|
||||
test('旧 `|` 分隔配置键已从两份 schema 与运行时彻底移除', () => {
|
||||
for (const [name, src] of [['vip.js', vipSchemaJs], ['SchemaPayment.php', schemaPhp], ['PaymentRuntime.php', paymentRuntime]]) {
|
||||
for (const key of LEGACY_KEYS) {
|
||||
assert.ok(!src.includes(key), `${name} 不应再含 ${key}`);
|
||||
}
|
||||
}
|
||||
// 运行时不再暴露任何 *_raw 原始字符串。
|
||||
const vipRuntime = vipSettingsSlice();
|
||||
for (const key of ['products_raw', 'packages_raw']) {
|
||||
assert.ok(!vipRuntime.includes(`'${key}'`), `PaymentRuntime VIP 设置不应再产出 ${key}`);
|
||||
}
|
||||
assert.ok(!vipRuntime.includes("'product' => sanitize_textarea_field"), 'upgrade.product 管道配置应已移除');
|
||||
for (const key of LEGACY_KEYS) {
|
||||
assert.ok(!vipRuntime.includes(key), `PaymentRuntime VIP 设置不应再读 ${key}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('会员等级组只留导航按钮与等级开关,身份字段搬进等级 Tab', () => {
|
||||
const basic = harness.schema.basic_field_keys;
|
||||
assert.deepStrictEqual(basic, [
|
||||
'nav_pay_vip', 'nav_user_pay_vip', 'pay_user_vip_1_s', 'vip_level_order_notice', 'pay_user_vip_2_s',
|
||||
]);
|
||||
const tabs = harness.schema.opt_tabs;
|
||||
assert.deepStrictEqual(Object.keys(tabs), ['vip1', 'vip2', 'renew', 'upgrade']);
|
||||
for (const level of [1, 2]) {
|
||||
const fields = Object.keys(tabs[`vip${level}`].fields);
|
||||
assert.deepStrictEqual(fields, [
|
||||
`pay_user_vip_${level}_name`,
|
||||
`pay_user_vip_${level}_desc`,
|
||||
`pay_user_vip_${level}_equity`,
|
||||
`vip_${level}_product_s`,
|
||||
`vip_${level}_product`,
|
||||
`vip_${level}_img_icon`,
|
||||
]);
|
||||
assert.strictEqual(tabs[`vip${level}`].fields[`vip_${level}_img_icon`].type, 'upload');
|
||||
}
|
||||
});
|
||||
|
||||
test('等级 Tab 标题由显示名称动态驱动', () => {
|
||||
for (const level of [1, 2]) {
|
||||
const tab = harness.schema.opt_tabs[`vip${level}`];
|
||||
assert.strictEqual(tab.titleKey, `pay_user_vip_${level}_name`);
|
||||
assert.strictEqual(tab.titleFallback, level === 1 ? '一级会员' : '二级会员');
|
||||
}
|
||||
// 会员权益的两个等级 Tab 同样动态。
|
||||
assert.strictEqual(harness.schema.benefit_tabs.vip1.titleKey, 'pay_user_vip_1_name');
|
||||
assert.strictEqual(harness.schema.benefit_tabs.vip2.titleKey, 'pay_user_vip_2_name');
|
||||
// 渲染器支持 titleKey / titleFallback。
|
||||
assert.match(rendererVue, /:name="tabName\(tab\)"/);
|
||||
assert.match(rendererVue, /:tab="tabName\(tab\)"/);
|
||||
assert.match(rendererVue, /function tabName\(tab\) \{[\s\S]*?tab\.titleKey[\s\S]*?tab\.titleFallback/);
|
||||
});
|
||||
|
||||
test('会员商品字段集恰好 6 项且标签颜色为参考主题 16 色', () => {
|
||||
const group = harness.schema.opt_tabs.vip1.fields.vip_1_product;
|
||||
assert.strictEqual(group.type, 'group');
|
||||
assert.strictEqual(group.max, 8);
|
||||
assert.strictEqual(group.button_title, '添加会员商品');
|
||||
assert.deepStrictEqual(
|
||||
group.fields.filter((f) => !f.ui_only).map((f) => f.key),
|
||||
PRODUCT_FIELDS
|
||||
);
|
||||
assert.strictEqual(group.fields.find((f) => f.key === 'price').sanitize_type, 'money_decimal');
|
||||
assert.strictEqual(group.fields.find((f) => f.key === 'show_price').sanitize_type, 'money_decimal');
|
||||
assert.strictEqual(group.fields.find((f) => f.key === 'time').type, 'spinner');
|
||||
assert.strictEqual(group.fields.find((f) => f.key === 'time').max, 3600);
|
||||
assert.deepStrictEqual(
|
||||
group.fields.find((f) => f.key === 'unit').options.map((o) => o.value),
|
||||
['day', 'month']
|
||||
);
|
||||
const palette = group.fields.find((f) => f.key === 'tag_class');
|
||||
assert.strictEqual(palette.type, 'palette');
|
||||
assert.strictEqual(palette.default, 'jb-yellow');
|
||||
assert.deepStrictEqual(palette.options.map((o) => o.value), TAG_PALETTE);
|
||||
// 标签颜色仅在填写了促销标签时出现。
|
||||
assert.deepStrictEqual(palette.dependsOn, { key: 'tag', operator: 'not_empty' });
|
||||
// 时长单位在 time 为 0(永久)时隐藏。
|
||||
assert.deepStrictEqual(group.fields.find((f) => f.key === 'unit').dependsOn, { key: 'time', operator: 'truthy' });
|
||||
// 二级会员 Tab 复用同一字段集。
|
||||
assert.deepStrictEqual(
|
||||
harness.schema.opt_tabs.vip2.fields.vip_2_product.fields.filter((f) => !f.ui_only).map((f) => f.key),
|
||||
PRODUCT_FIELDS
|
||||
);
|
||||
});
|
||||
|
||||
test('续费价格类型含自定义商品,折扣为折值滑块', () => {
|
||||
const renew = harness.schema.opt_tabs.renew.fields;
|
||||
assert.deepStrictEqual(renew.vip_renew_price_type.options.map((o) => o.value), [
|
||||
'same', 'discount', 'reduce', 'customize',
|
||||
]);
|
||||
const discount = renew.vip_renew_discount;
|
||||
assert.strictEqual(discount.type, 'slider');
|
||||
assert.strictEqual(discount.sanitize_type, 'float');
|
||||
assert.strictEqual(discount.min, 0.1);
|
||||
assert.strictEqual(discount.max, 9.9);
|
||||
assert.strictEqual(discount.step, 0.1);
|
||||
assert.strictEqual(renew.vip_renew_reduce.type, 'spinner');
|
||||
assert.strictEqual(renew.vip_renew_reduce.max, 500);
|
||||
assert.strictEqual(renew.vip_renew_reduce.step, 5);
|
||||
// 自定义续费商品组仅在 customize 时出现。
|
||||
for (const level of [1, 2]) {
|
||||
const group = renew[`vip_${level}_renew_product`];
|
||||
assert.strictEqual(group.type, 'group');
|
||||
assert.strictEqual(group.max, 8);
|
||||
assert.strictEqual(group.button_title, '添加续费商品');
|
||||
assert.deepStrictEqual(group.dependsOn, { key: 'vip_renew_price_type', value: 'customize' });
|
||||
assert.deepStrictEqual(
|
||||
group.fields.filter((f) => !f.ui_only).map((f) => f.key),
|
||||
PRODUCT_FIELDS
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('升级价格为参考主题同款三块 accordion', () => {
|
||||
const upgrade = harness.schema.opt_tabs.upgrade.fields.vip_upgrade_product;
|
||||
assert.strictEqual(upgrade.type, 'accordion');
|
||||
const keys = upgrade.fields.filter((f) => !f.ui_only).map((f) => f.key);
|
||||
assert.deepStrictEqual(keys, UPGRADE_FIELDS);
|
||||
assert.strictEqual(upgrade.fields.find((f) => f.key === 'month_to_month_s').default, true);
|
||||
assert.strictEqual(upgrade.fields.find((f) => f.key === 'jump_s').default, false);
|
||||
for (const key of ['unit_price', 'jump_price', 'jump_show_price', 'permanent_price', 'permanent_show_price']) {
|
||||
assert.strictEqual(upgrade.fields.find((f) => f.key === key).sanitize_type, 'money_decimal', `${key} 应为 money_decimal`);
|
||||
}
|
||||
for (const key of ['unit_tag_class', 'jump_tag_class', 'permanent_tag_class']) {
|
||||
const field = upgrade.fields.find((f) => f.key === key);
|
||||
assert.strictEqual(field.type, 'palette', `${key} 应为 palette`);
|
||||
assert.deepStrictEqual(field.options.map((o) => o.value), TAG_PALETTE);
|
||||
}
|
||||
});
|
||||
|
||||
test('积分兑换明细为结构化 group,时长为 time + Permanent 语义', () => {
|
||||
assert.deepStrictEqual(harness.schema.points_fields, [
|
||||
'pay_vip_points_exchange_s', 'pay_vip_points_exchange_product', 'pay_vip_points_exchange_desc',
|
||||
]);
|
||||
const group = harness.schema.points_group;
|
||||
assert.strictEqual(group.type, 'group');
|
||||
assert.strictEqual(group.max, 16);
|
||||
assert.strictEqual(group.button_title, '添加积分兑换商品');
|
||||
assert.deepStrictEqual(group.fields.map((f) => f.key), ['points', 'level', 'time', 'unit']);
|
||||
assert.strictEqual(group.fields.find((f) => f.key === 'points').type, 'number');
|
||||
assert.strictEqual(group.fields.find((f) => f.key === 'level').type, 'radio');
|
||||
assert.strictEqual(group.fields.find((f) => f.key === 'time').type, 'input');
|
||||
// 永久时隐藏时长单位。
|
||||
assert.deepStrictEqual(group.fields.find((f) => f.key === 'unit').dependsOn, { key: 'time', operator: 'neq', value: 'Permanent' });
|
||||
});
|
||||
|
||||
test('运行时归一化:默认值、价格净化、零价与非法行剔除', () => {
|
||||
const defaults = harness.runtime.defaults;
|
||||
// 参考主题默认两行。
|
||||
assert.strictEqual(defaults.levels['1'].products.length, 2);
|
||||
assert.strictEqual(defaults.levels['1'].products[0].price, '99.00');
|
||||
assert.strictEqual(defaults.levels['1'].products[0].time, 3);
|
||||
assert.strictEqual(defaults.levels['1'].products[0].unit, 'month');
|
||||
assert.strictEqual(defaults.levels['1'].products[0].days, 90);
|
||||
assert.strictEqual(defaults.levels['1'].renew_products[0].price, '69.00');
|
||||
assert.strictEqual(defaults.levels['2'].renew_products[0].price, '269.00');
|
||||
// 积分兑换默认两行(含 Permanent)。
|
||||
assert.strictEqual(defaults.points_exchange.packages.length, 2);
|
||||
assert.strictEqual(defaults.points_exchange.packages[1].is_permanent, true);
|
||||
assert.strictEqual(defaults.points_exchange.packages[1].days, 0);
|
||||
// 续费/升级默认值。
|
||||
assert.strictEqual(defaults.renew.price_type, 'discount');
|
||||
assert.strictEqual(defaults.renew.discount, 8);
|
||||
assert.strictEqual(defaults.upgrade.month_to_month_s, true);
|
||||
assert.strictEqual(defaults.upgrade.jump_s, false);
|
||||
assert.strictEqual(defaults.upgrade.unit_price, '10.00');
|
||||
assert.strictEqual(defaults.upgrade.unit_tag_class, 'jb-yellow');
|
||||
assert.strictEqual(defaults.upgrade.jump_tag_class, 'jb-red');
|
||||
|
||||
const custom = harness.runtime.custom;
|
||||
// 零价行被剔除;价格规范化为两位小数。
|
||||
assert.strictEqual(custom.levels['1'].products.length, 2);
|
||||
assert.strictEqual(custom.levels['1'].products[0].price, '88.80');
|
||||
assert.strictEqual(custom.levels['1'].products[0].tag_class, 'jb-green');
|
||||
// 非法标签颜色回落为空串。
|
||||
assert.strictEqual(custom.levels['2'].products[0].tag_class, '');
|
||||
// 自定义续费价格类型与折扣。
|
||||
assert.strictEqual(custom.renew.price_type, 'customize');
|
||||
assert.strictEqual(custom.renew.discount, 7.5);
|
||||
assert.strictEqual(custom.renew.desc, '续费有礼');
|
||||
assert.strictEqual(custom.levels['1'].renew_products[0].tag_class, 'b-red');
|
||||
// 升级三块参数透传。
|
||||
assert.strictEqual(custom.upgrade.unit_price, '12.50');
|
||||
assert.strictEqual(custom.upgrade.unit_tag_class, 'jb-blue');
|
||||
assert.strictEqual(custom.upgrade.jump_s, true);
|
||||
assert.strictEqual(custom.upgrade.permanent_tag_class, 'jb-vip2');
|
||||
// 积分兑换:非法行(积分为 0 / 等级越界 / 时长为空)被剔除。
|
||||
assert.strictEqual(custom.points_exchange.packages.length, 3);
|
||||
assert.strictEqual(custom.points_exchange.packages[0].days, 30);
|
||||
assert.strictEqual(custom.points_exchange.packages[1].is_permanent, true);
|
||||
assert.strictEqual(custom.points_exchange.packages[2].days, 90);
|
||||
});
|
||||
|
||||
test('旧 `|` 管道存量值按新默认值回退,不会渲染成假条目', () => {
|
||||
const legacy = harness.runtime.legacy;
|
||||
assert.strictEqual(legacy.levels['1'].products.length, 2);
|
||||
assert.strictEqual(legacy.levels['1'].products[0].price, '99.00');
|
||||
assert.strictEqual(legacy.points_exchange.packages.length, 2);
|
||||
assert.strictEqual(legacy.upgrade.unit_price, '10.00');
|
||||
});
|
||||
|
||||
test('续费计价:折值/立减/自定义商品三种语义', () => {
|
||||
const p = harness.pricing;
|
||||
// 相同价格:不改价、不展示原价。
|
||||
assert.strictEqual(p.same.price, '100.00');
|
||||
assert.strictEqual(p.same.show_price, '0.00');
|
||||
// 8 折:执行价 80,原价划线为 100。
|
||||
assert.strictEqual(p.discount_8.price, '80.00');
|
||||
assert.strictEqual(p.discount_8.show_price, '100.00');
|
||||
assert.strictEqual(p.discount_8.tag, '8折优惠');
|
||||
// 支持小数折。
|
||||
assert.strictEqual(p.discount_7_5.price, '75.00');
|
||||
assert.strictEqual(p.discount_7_5.tag, '7.5折优惠');
|
||||
// 超出滑块上限时按 9.9 折钳制(参考主题 slider max)。
|
||||
assert.strictEqual(p.discount_10_clamped.price, '99.00');
|
||||
assert.strictEqual(p.discount_10_clamped.tag, '9.9折优惠');
|
||||
// 立减 20:执行价 80。
|
||||
assert.strictEqual(p.reduce_20.price, '80.00');
|
||||
assert.strictEqual(p.reduce_20.show_price, '100.00');
|
||||
assert.strictEqual(p.reduce_20.tag, '续费立减¥20.00');
|
||||
// 自定义商品:沿用商品自身的原价、促销标签与标签颜色。
|
||||
assert.strictEqual(p.customize.price, '100.00');
|
||||
assert.strictEqual(p.customize.show_price, '200.00');
|
||||
assert.strictEqual(p.customize.tag, '限时');
|
||||
assert.strictEqual(p.customize.tag_class, 'jb-red');
|
||||
// 开通动作不套用续费规则。
|
||||
assert.strictEqual(p.pay.price, '100.00');
|
||||
assert.strictEqual(p.pay.show_price, '0.00');
|
||||
assert.strictEqual(p.pay.tag, '');
|
||||
});
|
||||
|
||||
test('tag_class 透传到前端套餐卡与积分兑换弹窗', () => {
|
||||
assert.match(payVipJs, /item\.tag_class \|\| "jb-yellow"/);
|
||||
// 弹窗时长行按 time/unit 渲染,Permanent 走永久。
|
||||
assert.match(modalSource, /\$is_permanent = !empty\(\$package\['is_permanent'\]\)/);
|
||||
assert.match(modalSource, /\$pkg_unit === 'month' \? __\('个月', 'aksrt'\) : __\('天', 'aksrt'\)/);
|
||||
// 用户中心档位卡消费续费/升级简介。
|
||||
assert.match(vipTabPhp, /\$renew_desc = \(string\) \(\$vip_settings\['renew'\]\['desc'\]/);
|
||||
assert.match(vipTabPhp, /\$upgrade_desc = \(string\) \(\$vip_settings\['upgrade'\]\['desc'\]/);
|
||||
});
|
||||
|
||||
test('管道解析死代码已清理', () => {
|
||||
for (const [name, src] of [['VipServicePackages.php', packagesTrait], ['VipServicePurchase.php', purchaseSource]]) {
|
||||
assert.ok(!/function parse_level_products/.test(src), `${name} 不应再定义 parse_level_products`);
|
||||
assert.ok(!/function parse_packages\(/.test(src), `${name} 不应再定义 parse_packages`);
|
||||
assert.ok(!/function parse_upgrade_product_line/.test(src), `${name} 不应再定义 parse_upgrade_product_line`);
|
||||
assert.ok(!/preg_split\('\/\\r\\n\|\\r\|\\n\/'/.test(src), `${name} 不应再按行拆分管道配置`);
|
||||
}
|
||||
// HomeCms 的零调用 PaymentRuntime 代理方法已删除。
|
||||
for (const method of ['get_payment_vip_settings', 'get_payment_recharge_settings', 'get_withdrawal_settings']) {
|
||||
assert.ok(!homeCms.includes(`function ${method}`), `HomeCms 不应再含死代理 ${method}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('SPA 镜像 schema 与 PHP schema 同构', async () => {
|
||||
const mod = await import(pathToFileURL(path.join(root, 'assets/admin/src/schema/sections/payment/vip.js')).href);
|
||||
const schema = mod.vipSchema;
|
||||
assert.deepStrictEqual(schema.groups.map((g) => g.key), [
|
||||
'notice', 'basic', 'benefit', 'opt', 'points-exchange', 'modal-copy',
|
||||
]);
|
||||
const opt = schema.groups.find((g) => g.key === 'opt').fields[0];
|
||||
assert.strictEqual(opt.type, 'tabbed');
|
||||
assert.deepStrictEqual(opt.tabs.map((t) => t.key), ['vip1', 'vip2', 'renew', 'upgrade']);
|
||||
for (const level of [1, 2]) {
|
||||
const tab = opt.tabs[level - 1];
|
||||
assert.strictEqual(tab.titleKey, `pay_user_vip_${level}_name`);
|
||||
assert.deepStrictEqual(tab.fields.map((f) => f.key), [
|
||||
`pay_user_vip_${level}_name`,
|
||||
`pay_user_vip_${level}_desc`,
|
||||
`pay_user_vip_${level}_equity`,
|
||||
`vip_${level}_product_s`,
|
||||
`vip_${level}_product`,
|
||||
`vip_${level}_img_icon`,
|
||||
]);
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
opt.tabs[2].fields.find((f) => f.key === 'vip_renew_price_type').options.map((o) => o.value),
|
||||
['same', 'discount', 'reduce', 'customize']
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
opt.tabs[3].fields.find((f) => f.key === 'vip_upgrade_product').fields.filter((f) => !f.ui_only).map((f) => f.key),
|
||||
UPGRADE_FIELDS
|
||||
);
|
||||
const points = schema.groups.find((g) => g.key === 'points-exchange').fields[1];
|
||||
assert.strictEqual(points.type, 'group');
|
||||
assert.deepStrictEqual(points.fields.map((f) => f.key), ['points', 'level', 'time', 'unit']);
|
||||
// SPA 默认值与 PHP 侧一致。
|
||||
assert.strictEqual(schema.defaults.vip_1_product[0].price, 99);
|
||||
assert.strictEqual(schema.defaults.vip_1_renew_product[0].price, 69);
|
||||
assert.strictEqual(schema.defaults.vip_2_renew_product[0].price, 269);
|
||||
assert.strictEqual(schema.defaults.vip_renew_discount, 8);
|
||||
assert.strictEqual(schema.defaults.vip_upgrade_product.jump_s, false);
|
||||
// 旧键不出现在 SPA schema。
|
||||
for (const key of LEGACY_KEYS) {
|
||||
assert.ok(!vipSchemaJs.includes(key), `vip.js 不应再含 ${key}`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,207 @@
|
||||
<?php
|
||||
/**
|
||||
* Executable VIP structured-settings contract harness.
|
||||
*
|
||||
* Builds the payment schema with WordPress stubs, normalizes a stored
|
||||
* `aksrt_settings` snapshot through PaymentRuntime, and prints the result as
|
||||
* JSON so the Node contract test can assert on the real shapes.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace {
|
||||
define('ABSPATH', __DIR__ . '/');
|
||||
define('DAY_IN_SECONDS', 86400);
|
||||
|
||||
function __(string $text, string $domain = ''): string { return $text; }
|
||||
function _x(string $text, string $context, string $domain = ''): string { return $text; }
|
||||
function esc_html($value): string { return (string) $value; }
|
||||
function esc_attr($value): string { return (string) $value; }
|
||||
function esc_url($value): string { return (string) $value; }
|
||||
function esc_url_raw($value): string { return trim((string) $value); }
|
||||
function sanitize_key($value): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $value)) ?? ''; }
|
||||
function sanitize_text_field($value): string { return trim(strip_tags((string) $value)); }
|
||||
function sanitize_textarea_field($value): string { return trim((string) $value); }
|
||||
function wp_kses_post($value): string { return (string) $value; }
|
||||
function wp_kses($value, $allowed = null): string { return (string) $value; }
|
||||
function trailingslashit($value): string { return rtrim((string) $value, '/\\') . '/'; }
|
||||
function get_template_directory_uri(): string { return 'https://example.test/wp-content/themes/aksrt'; }
|
||||
function absint($value): int { return max(0, (int) $value); }
|
||||
function wp_json_encode($value, int $flags = 0): string { return json_encode($value, $flags | JSON_UNESCAPED_SLASHES); }
|
||||
function is_multisite(): bool { return false; }
|
||||
function get_option(string $key, $default = false) { return $default; }
|
||||
function did_action(string $hook): int { return 0; }
|
||||
function apply_filters(string $hook, $value, ...$args) { return $value; }
|
||||
function add_action(...$args): void {}
|
||||
function add_filter(...$args): void {}
|
||||
}
|
||||
|
||||
namespace AKSRT\Settings {
|
||||
/** Test double: PaymentRuntime reads every raw settings key through this. */
|
||||
final class Settings {
|
||||
/** @var array<string,mixed> */
|
||||
public static array $snapshot = [];
|
||||
|
||||
public static function get_all(): array {
|
||||
return self::$snapshot;
|
||||
}
|
||||
|
||||
public static function get_paid_content_settings(): array {
|
||||
return ['display' => ['currency_symbol' => '¥']];
|
||||
}
|
||||
|
||||
public static function reset(): void {
|
||||
self::$snapshot = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
require dirname(__DIR__, 2) . '/src/Ledger/AssetType.php';
|
||||
require dirname(__DIR__, 2) . '/src/Ledger/AssetAmount.php';
|
||||
require dirname(__DIR__, 2) . '/src/Settings/PaymentRuntime.php';
|
||||
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaPayment.php';
|
||||
require dirname(__DIR__, 2) . '/src/Content/VipServicePackages.php';
|
||||
|
||||
/** Exposes the trait's private renew-pricing math for contract assertions. */
|
||||
final class VipPricingProbe {
|
||||
use \AKSRT\Content\VipServicePackages;
|
||||
|
||||
/** @param array<string,mixed> $pkg */
|
||||
public function price(array $pkg, string $action, array $renew, int $days): ?array {
|
||||
return $this->apply_action_price($pkg, $action, $renew, [], 1, 1, 0, false, $days);
|
||||
}
|
||||
}
|
||||
|
||||
use AKSRT\Admin\Schema\SchemaPayment;
|
||||
use AKSRT\Settings\PaymentRuntime;
|
||||
use AKSRT\Settings\Settings;
|
||||
|
||||
$vip = SchemaPayment::build()['sections']['vip'];
|
||||
$groups = [];
|
||||
foreach ($vip['groups'] as $group) {
|
||||
$groups[$group['key']] = $group;
|
||||
}
|
||||
|
||||
$opt_tabs = [];
|
||||
foreach ($groups['opt']['fields'][0]['tabs'] as $tab) {
|
||||
$fields = [];
|
||||
foreach ($tab['fields'] as $field) {
|
||||
$fields[$field['key']] = $field;
|
||||
}
|
||||
$opt_tabs[$tab['key']] = [
|
||||
'title' => $tab['title'],
|
||||
'titleKey' => $tab['titleKey'] ?? null,
|
||||
'titleFallback' => $tab['titleFallback'] ?? null,
|
||||
'fields' => $fields,
|
||||
];
|
||||
}
|
||||
|
||||
$benefit_tabs = [];
|
||||
foreach ($groups['benefit']['fields'][0]['tabs'] as $tab) {
|
||||
$benefit_tabs[$tab['key']] = [
|
||||
'title' => $tab['title'],
|
||||
'titleKey' => $tab['titleKey'] ?? null,
|
||||
'titleFallback' => $tab['titleFallback'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
// ---- runtime: defaults (nothing stored) -------------------------------
|
||||
Settings::reset();
|
||||
$defaults = PaymentRuntime::get_payment_vip_settings();
|
||||
|
||||
// ---- runtime: fully customised stored snapshot -------------------------
|
||||
Settings::$snapshot = [
|
||||
'pay_user_vip_1_s' => true,
|
||||
'pay_user_vip_2_s' => true,
|
||||
'pay_user_vip_1_name' => '青铜会员',
|
||||
'pay_user_vip_2_name' => '王者会员',
|
||||
'vip_renew' => true,
|
||||
'vip_renew_desc' => '续费有礼',
|
||||
'vip_renew_price_type' => 'customize',
|
||||
'vip_renew_discount' => 7.5,
|
||||
'vip_renew_reduce' => '15.00',
|
||||
'vip_upgrade' => true,
|
||||
'vip_upgrade_desc' => '升级王者',
|
||||
'vip_upgrade_product' => [
|
||||
'month_to_month_s' => true,
|
||||
'unit_price' => '12.50',
|
||||
'unit_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 按天升级',
|
||||
'unit_tag_class' => 'jb-blue',
|
||||
'jump_s' => true,
|
||||
'jump_price' => '259.00',
|
||||
'jump_show_price' => '399.00',
|
||||
'jump_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 直升永久',
|
||||
'jump_tag_class' => 'jb-red',
|
||||
'permanent_price' => '159.00',
|
||||
'permanent_show_price' => '259.00',
|
||||
'permanent_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 王者特惠',
|
||||
'permanent_tag_class' => 'jb-vip2',
|
||||
],
|
||||
'vip_1_product' => [
|
||||
['price' => '88.80', 'show_price' => '188.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 新客', 'tag_class' => 'jb-green', 'time' => 1, 'unit' => 'month'],
|
||||
['price' => '0', 'show_price' => '', 'tag' => '', 'time' => 0, 'unit' => 'day'],
|
||||
['price' => '10', 'show_price' => '20', 'tag' => '', 'time' => 2, 'unit' => 'month'],
|
||||
],
|
||||
'vip_2_product' => [
|
||||
['price' => '300', 'show_price' => '600', 'tag' => 'x', 'tag_class' => 'not-a-real-class', 'time' => 12, 'unit' => 'month'],
|
||||
],
|
||||
'vip_1_renew_product' => [
|
||||
['price' => '60', 'show_price' => '99', 'tag' => '续费专享', 'tag_class' => 'b-red', 'time' => 1, 'unit' => 'month'],
|
||||
],
|
||||
'vip_2_renew_product' => [],
|
||||
'pay_vip_points_exchange_s' => true,
|
||||
'pay_vip_points_exchange_product' => [
|
||||
['points' => 800, 'level' => 1, 'time' => 30, 'unit' => 'day'],
|
||||
['points' => 9000, 'level' => 2, 'time' => 'Permanent'],
|
||||
['points' => 0, 'level' => 1, 'time' => 10, 'unit' => 'day'],
|
||||
['points' => 100, 'level' => 9, 'time' => 10, 'unit' => 'day'],
|
||||
['points' => 100, 'level' => 1, 'time' => '', 'unit' => 'day'],
|
||||
['points' => 120, 'level' => 2, 'time' => 3, 'unit' => 'month'],
|
||||
],
|
||||
];
|
||||
$custom = PaymentRuntime::get_payment_vip_settings();
|
||||
|
||||
// ---- legacy scalar payloads must fall back to defaults -----------------
|
||||
Settings::$snapshot = [
|
||||
'pay_user_vip_1_s' => true,
|
||||
'vip_1_product' => "30|59|1|month|限时|月卡",
|
||||
'pay_vip_points_exchange_product' => "1|500|30|积分月卡",
|
||||
'vip_upgrade_product' => "unit|1|按剩余天数升级",
|
||||
];
|
||||
$legacy = PaymentRuntime::get_payment_vip_settings();
|
||||
|
||||
echo wp_json_encode([
|
||||
'schema' => [
|
||||
'version' => $vip['version'],
|
||||
'group_keys' => array_column($vip['groups'], 'key'),
|
||||
'basic_field_keys' => array_column($groups['basic']['fields'], 'key'),
|
||||
'benefit_tabs' => $benefit_tabs,
|
||||
'opt_tabs' => $opt_tabs,
|
||||
'points_fields' => array_column($groups['points-exchange']['fields'], 'key'),
|
||||
'points_group' => $groups['points-exchange']['fields'][1],
|
||||
'tag_palette' => $opt_tabs['vip1']['fields']['vip_1_product']['fields'][3]['options'],
|
||||
],
|
||||
'runtime' => [
|
||||
'defaults' => $defaults,
|
||||
'custom' => $custom,
|
||||
'legacy' => $legacy,
|
||||
],
|
||||
'pricing' => (static function (): array {
|
||||
$probe = new VipPricingProbe();
|
||||
$base = [
|
||||
'price' => '100.00', 'show_price' => '200.00', 'tag' => '限时',
|
||||
'tag_class' => 'jb-red', 'time' => 1, 'unit' => 'month',
|
||||
];
|
||||
return [
|
||||
'same' => $probe->price($base, 'renew', ['price_type' => 'same'], 30),
|
||||
'discount_8' => $probe->price($base, 'renew', ['price_type' => 'discount', 'discount' => 8], 30),
|
||||
'discount_7_5' => $probe->price($base, 'renew', ['price_type' => 'discount', 'discount' => 7.5], 30),
|
||||
'discount_10_clamped' => $probe->price($base, 'renew', ['price_type' => 'discount', 'discount' => 10], 30),
|
||||
'reduce_20' => $probe->price($base, 'renew', ['price_type' => 'reduce', 'reduce' => '20.00'], 30),
|
||||
'customize' => $probe->price($base, 'renew', ['price_type' => 'customize'], 30),
|
||||
'pay' => $probe->price($base, 'pay', ['price_type' => 'discount'], 30),
|
||||
];
|
||||
})(),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), PHP_EOL;
|
||||
}
|
||||
Reference in new issue
Block a user