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
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user