diff --git a/assets/admin/src/schema/sections/payment/vip.js b/assets/admin/src/schema/sections/payment/vip.js index 8958a2ca..6a8ef4a4 100644 --- a/assets/admin/src/schema/sections/payment/vip.js +++ b/assets/admin/src/schema/sections/payment/vip.js @@ -2,14 +2,24 @@ export const vipSchema = { key: 'payment.vip', title: 'VIP会员', icon: 'fas fa-crown', - description: '对齐 Zibll 双级会员:pay_user_vip_1 / pay_user_vip_2 与下载权益。', + description: '对齐 Zibll 双级会员:分档套餐、权益、续费/升级与积分兑换。', defaults: { pay_user_vip_1_s: true, pay_user_vip_1_name: '一级会员', + pay_user_vip_1_desc: '尊享一级会员特权', + pay_user_vip_1_equity: '
  • 免费下载部分资源
  • 会员专属身份标识
  • 付费内容会员价
  • ', + 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_1_img_icon: '', pay_user_vip_2_s: true, pay_user_vip_2_name: '二级会员', + pay_user_vip_2_desc: '尊享二级会员特权', + pay_user_vip_2_equity: '
  • 更高免费下载额度
  • 更高等级身份标识
  • 更多付费内容会员价
  • ', + 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_2_img_icon: '', pay_user_vip_desc: '', - payment_vip_packages: '30|30|月卡\n365|365|年卡', + pay_user_vip_more: '', pay_download_limit: 0, pay_download_speed: 0, pay_download_limit_vip_1: 0, @@ -19,6 +29,9 @@ export const vipSchema = { pay_vip_pass_charge_s: false, pay_vip_pass_charge_desc: '', pay_vip_pass_charge_only_password: true, + pay_vip_points_exchange_s: false, + pay_vip_points_exchange_desc: '', + payment_vip_points_packages: '1|500|30|积分月卡\n1|5000|0|积分永久', vip_renew: true, vip_renew_desc: '', vip_renew_price_type: 'discount', @@ -26,7 +39,7 @@ export const vipSchema = { vip_renew_reduce: 20, vip_upgrade: true, vip_upgrade_desc: '', - vip_upgrade_product: '', + vip_upgrade_product: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级', }, groups: [ { @@ -36,17 +49,58 @@ export const vipSchema = { fields: [ { 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', default: '
  • 免费下载部分资源
  • 会员专属身份标识
  • 付费内容会员价
  • ', dependsOn: { key: 'pay_user_vip_1_s', value: true }, help: '建议使用
  • 列表项' }, + { key: 'vip_1_img_icon', label: '一级会员图标 URL', type: 'input', default: '', dependsOn: { key: 'pay_user_vip_1_s', value: true } }, { 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_desc', label: '会员说明', type: 'textarea', default: '' }, + { 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', default: '
  • 更高免费下载额度
  • 更高等级身份标识
  • 更多付费内容会员价
  • ', 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 } }, + { key: 'pay_user_vip_desc', label: '弹窗顶部会员说明', type: 'textarea', default: '' }, + { key: 'pay_user_vip_more', label: '弹窗底部补充说明 HTML', type: 'textarea', default: '' }, ], }, { key: 'packages', - title: '会员套餐', + title: '分档套餐', icon: 'fas fa-gem', fields: [ - { key: 'payment_vip_packages', label: '套餐列表', type: 'textarea', default: '30|30|月卡\n365|365|年卡', help: '每行一个套餐:价格|时长(天)|标题' }, + { 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: '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: 'points_exchange', + title: '积分兑换会员', + icon: 'fas fa-coins', + 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', default: '', dependsOn: { key: 'pay_vip_points_exchange_s', value: true } }, ], }, { @@ -73,7 +127,7 @@ export const vipSchema = { { key: 'vip_renew', label: '允许续费', type: 'switch', default: true }, { key: 'vip_renew_desc', label: '续费说明', type: 'textarea', default: '', dependsOn: { key: 'vip_renew', value: true } }, { key: 'vip_renew_price_type', label: '续费价格类型', type: 'radio', default: 'discount', options: [{ label: '相同价格', value: 'same' }, { label: '折扣价格', value: 'discount' }, { label: '优惠立减', value: 'reduce' }], dependsOn: { key: 'vip_renew', value: true } }, - { key: 'vip_renew_discount', label: '续费折扣(%)', type: 'number', default: 8, min: 1, max: 100, dependsOn: { key: 'vip_renew_price_type', value: 'discount' } }, + { 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: 'number', default: 20, min: 0, max: 9999, dependsOn: { key: 'vip_renew_price_type', value: 'reduce' } }, ], }, @@ -88,9 +142,9 @@ export const vipSchema = { key: 'vip_upgrade_product', label: '升级产品', type: 'textarea', - default: '', + default: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级', dependsOn: { key: 'vip_upgrade', value: true }, - help: '每行一条。永久VIP1升级:permanent|价格|标题;按剩余天:unit|每日单价|标题;限时跳转永久:jump|价格|标题;兼容旧格式:价格|天数|标题', + help: '每行一条。永久VIP1升级:permanent|价格|标题;按剩余天:unit|每日单价|标题;限时跳转永久:jump|价格|标题', }, ], }, diff --git a/src/AKSRT/Admin/Schema/SchemaPayment.php b/src/AKSRT/Admin/Schema/SchemaPayment.php index 9477137d..83176572 100644 --- a/src/AKSRT/Admin/Schema/SchemaPayment.php +++ b/src/AKSRT/Admin/Schema/SchemaPayment.php @@ -100,18 +100,28 @@ class SchemaPayment { 'vip' => [ 'key' => 'vip', 'title' => 'VIP会员', - 'version' => '1.0.0', + 'version' => '1.1.0', 'option_key' => 'aksrt_settings', 'groups' => [ ['key' => 'basic', 'title' => '会员等级', 'fields' => [ ['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', 'default' => '
  • 免费下载部分资源
  • 会员专属身份标识
  • 付费内容会员价
  • ', '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]], ['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_desc', 'label' => '会员说明', 'type' => 'textarea', 'default' => ''], + ['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', 'default' => '
  • 更高免费下载额度
  • 更高等级身份标识
  • 更多付费内容会员价
  • ', '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]], + ['key' => 'pay_user_vip_desc', 'label' => '弹窗顶部会员说明', 'type' => 'textarea', 'default' => ''], + ['key' => 'pay_user_vip_more', 'label' => '弹窗底部补充说明 HTML', 'type' => 'textarea', 'default' => ''], ]], - ['key' => 'packages', 'title' => '会员套餐', 'fields' => [ - ['key' => 'payment_vip_packages', 'label' => '套餐列表', 'type' => 'textarea', 'default' => "30|30|月卡\n365|365|年卡", 'help' => '每行:现金价|天数|标题(天数为 0 表示永久)'], + ['key' => 'packages', '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' => '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' => 'points_exchange', 'title' => '积分兑换会员', 'fields' => [ ['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false], @@ -137,13 +147,13 @@ class SchemaPayment { ['label' => '折扣价格', 'value' => 'discount'], ['label' => '优惠立减', 'value' => 'reduce'], ]], - ['key' => 'vip_renew_discount', 'label' => '续费折扣(%)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'discount']], + ['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' => 'number', 'default' => 20, 'min' => 0, 'max' => 9999, 'dependsOn' => ['key' => 'vip_renew_price_type', 'value' => 'reduce']], ]], ['key' => 'upgrade', 'title' => '升级设置', 'fields' => [ ['key' => 'vip_upgrade', 'label' => '允许升级', 'type' => 'switch', 'default' => true], ['key' => 'vip_upgrade_desc', 'label' => '升级说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true]], - ['key' => 'vip_upgrade_product', 'label' => '升级产品', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '每行一条。永久VIP1:permanent|价格|标题;剩余天:unit|每日单价|标题;跳转永久:jump|价格|标题;兼容:价格|天数|标题'], + ['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|价格|标题'], ]], ], ], diff --git a/src/AKSRT/Content/VipServicePackages.php b/src/AKSRT/Content/VipServicePackages.php index bce6fe77..d27527d4 100644 --- a/src/AKSRT/Content/VipServicePackages.php +++ b/src/AKSRT/Content/VipServicePackages.php @@ -12,11 +12,14 @@ trait VipServicePackages { /** - * Parse packages_raw lines: price|days|title + * Parse per-level product lines (zibll vip_product). * - * @return array + * Preferred: price|show_price|time|unit|tag|title + * Legacy: price|days|title + * + * @return array */ - public function parse_packages(string $raw): array { + public function parse_level_products(string $raw): array { $lines = preg_split('/\r\n|\r|\n/', $raw) ?: []; $out = []; foreach ($lines as $line) { @@ -26,20 +29,90 @@ trait VipServicePackages { } $parts = array_map('trim', explode('|', $line)); $price = isset($parts[0]) ? (float) $parts[0] : 0.0; - $days = isset($parts[1]) ? (int) $parts[1] : 0; - $title = isset($parts[2]) ? sanitize_text_field($parts[2]) : ''; if ($price <= 0) { continue; } - if ($title === '') { - $title = $days > 0 - ? sprintf(__('%d天会员', 'aksrt'), $days) - : __('永久会员', 'aksrt'); + + $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_price = isset($parts[1]) ? (float) $parts[1] : 0.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_price = 0.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' => round($price, 2), - 'days' => max(0, $days), - 'title' => $title, + 'price' => round($price, 2), + 'show_price' => max(0.0, round($show_price, 2)), + '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. + */ + public function duration_to_days(int $time, string $unit = 'day'): int { + if ($time <= 0) { + return 0; + } + if ($unit === 'month') { + return $time * 30; + } + return $time; + } + + /** + * @deprecated Use parse_level_products — kept as thin adapter for upgrade fixed lines. + * + * @return array + */ + public function parse_packages(string $raw): array { + $parsed = $this->parse_level_products($raw); + $out = []; + foreach ($parsed as $p) { + $out[] = [ + 'price' => (float) $p['price'], + 'days' => (int) $p['days'], + 'title' => (string) $p['title'], ]; } return $out; diff --git a/src/AKSRT/Content/VipServicePurchase.php b/src/AKSRT/Content/VipServicePurchase.php index 4273f0e4..f8137703 100644 --- a/src/AKSRT/Content/VipServicePurchase.php +++ b/src/AKSRT/Content/VipServicePurchase.php @@ -17,12 +17,11 @@ trait VipServicePurchase { public function get_catalog(int $user_id = 0): array { $vip = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_payment_vip_settings() - : ['enabled' => false, 'packages_raw' => '', 'levels' => [], 'renew' => [], 'upgrade' => [], 'points_exchange' => []]; + : ['enabled' => false, 'levels' => [], 'renew' => [], 'upgrade' => [], 'points_exchange' => []]; $level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0; $expires = $user_id > 0 ? (int) get_user_meta($user_id, 'aksrt_vip_expires', true) : 0; $is_permanent = ($level > 0 && $expires <= 0); - $packages = $this->parse_packages((string) ($vip['packages_raw'] ?? '')); $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : []; $renew = is_array($vip['renew'] ?? null) ? $vip['renew'] : []; $upgrade = is_array($vip['upgrade'] ?? null) ? $vip['upgrade'] : []; @@ -37,6 +36,8 @@ trait VipServicePurchase { if (empty($levels[$lv]['enabled'])) { continue; } + // Cash products can be disabled per level. + $product_s = !isset($levels[$lv]['product_s']) || !empty($levels[$lv]['product_s']); if ($is_permanent && $lv <= $level) { continue; } @@ -49,6 +50,9 @@ trait VipServicePurchase { $name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv)); $items = []; + $packages = $product_s + ? $this->parse_level_products((string) ($levels[$lv]['products_raw'] ?? '')) + : []; foreach ($packages as $idx => $pkg) { $base_price = (float) $pkg['price']; $days = (int) $pkg['days']; @@ -64,6 +68,22 @@ trait VipServicePurchase { continue; } + $show_price = (float) ($priced['show_price'] ?? 0); + if ($show_price <= 0 && !empty($pkg['show_price']) && $action === 'pay') { + $show_price = (float) $pkg['show_price']; + } + $discount_tag = (string) ($priced['tag'] ?? ''); + if ($discount_tag === '' && $action === 'pay' && !empty($pkg['tag'])) { + $discount_tag = (string) $pkg['tag']; + } + + $days_label = __('永久', 'aksrt'); + if ((int) $pkg['time'] > 0) { + $days_label = ((string) ($pkg['unit'] ?? 'day') === 'month') + ? sprintf(__('%d 个月', 'aksrt'), (int) $pkg['time']) + : sprintf(__('%d 天', 'aksrt'), (int) $pkg['time']); + } + $items[] = [ 'package_id' => $this->encode_package_id($lv, $idx + 1), 'level' => $lv, @@ -71,17 +91,17 @@ trait VipServicePurchase { 'action' => $action, 'price' => (float) $priced['price'], 'base_price' => $base_price, - 'show_price' => (float) ($priced['show_price'] ?? 0), - 'discount_tag' => (string) ($priced['tag'] ?? ''), + 'show_price' => $show_price, + 'discount_tag' => $discount_tag, 'days' => $days, + 'time' => (int) ($pkg['time'] ?? 0), + 'unit' => (string) ($pkg['unit'] ?? 'day'), 'title' => (string) $pkg['title'], 'price_label' => $this->format_cash_label((float) $priced['price']), - 'show_price_label' => !empty($priced['show_price']) - ? $this->format_cash_label((float) $priced['show_price']) + 'show_price_label' => $show_price > 0 + ? $this->format_cash_label($show_price) : '', - 'days_label' => ($days <= 0) - ? __('永久', 'aksrt') - : sprintf(__('%d 天', 'aksrt'), $days), + 'days_label' => $days_label, 'cta_label' => $action === 'renew' ? __('立即续费', 'aksrt') : ($action === 'upgrade' ? __('立即升级', 'aksrt') : __('立即开通', 'aksrt')), @@ -163,7 +183,7 @@ trait VipServicePurchase { $vip = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_payment_vip_settings() - : ['enabled' => false, 'packages_raw' => '', 'levels' => [], 'renew' => [], 'upgrade' => []]; + : ['enabled' => false, 'levels' => [], 'renew' => [], 'upgrade' => []]; if (empty($vip['enabled'])) { return new \WP_Error('aksrt_vip_disabled', __('会员功能未启用', 'aksrt'), ['status' => 400]); } @@ -203,6 +223,8 @@ trait VipServicePurchase { // Dedicated upgrade product: // 901 permanent/fixed, 902 unit remaining days, 903 jump permanent. $upgrade_mode = ''; + $pkg_time = 0; + $pkg_unit = 'day'; if (in_array($index, [901, 902, 903], true)) { $upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? '')); $match = null; @@ -222,18 +244,30 @@ trait VipServicePurchase { ]; $amount = round((float) $match['price'], 2); $days = (int) $match['days']; + $pkg_time = $days; // unit upgrade keeps days as remaining; jump/permanent use 0 + $pkg_unit = 'day'; $base_price = (float) ($match['base_price'] ?? $amount); $discount_tag = (string) ($match['discount_tag'] ?? __('升级专享', 'aksrt')); $upgrade_mode = sanitize_key((string) ($match['upgrade_mode'] ?? '')); $action = 'upgrade'; + if ($upgrade_mode === 'jump' || $upgrade_mode === 'permanent') { + $pkg_time = 0; + $days = 0; + } } else { - $packages = $this->parse_packages((string) ($vip['packages_raw'] ?? '')); + $level_cfg = is_array($levels[$level] ?? null) ? $levels[$level] : []; + if (isset($level_cfg['product_s']) && empty($level_cfg['product_s'])) { + return new \WP_Error('aksrt_vip_product_disabled', __('该等级暂不支持购买', 'aksrt'), ['status' => 400]); + } + $packages = $this->parse_level_products((string) ($level_cfg['products_raw'] ?? '')); $pkg = $packages[$index - 1] ?? null; if (!$pkg) { return new \WP_Error('aksrt_invalid_package', __('套餐不存在', 'aksrt'), ['status' => 400]); } $base_price = (float) $pkg['price']; $days = (int) $pkg['days']; + $pkg_time = (int) ($pkg['time'] ?? 0); + $pkg_unit = (string) ($pkg['unit'] ?? 'day'); $priced = $this->apply_action_price($base_price, $action, $renew, $upgrade, $level, $user_level, $expires, $is_permanent, $days); if ($priced === null) { return new \WP_Error('aksrt_invalid_package', __('当前无法购买该套餐', 'aksrt'), ['status' => 400]); @@ -259,6 +293,8 @@ trait VipServicePurchase { 'vip_pay' => 1, 'level' => $level, 'days' => $days, + 'time' => $pkg_time, + 'unit' => $pkg_unit, 'package_index' => $index, 'package_title' => (string) ($pkg['title'] ?? ''), 'package_id' => $package_id, @@ -401,6 +437,11 @@ trait VipServicePurchase { $level = (int) ($meta['level'] ?? 0); $days = (int) ($meta['days'] ?? 0); + $time = (int) ($meta['time'] ?? 0); + $unit = sanitize_key((string) ($meta['unit'] ?? 'day')); + if (!in_array($unit, ['day', 'month'], true)) { + $unit = 'day'; + } $upgrade_mode = sanitize_key((string) ($meta['upgrade_mode'] ?? '')); if ($level <= 0) { $decoded = $this->decode_package_id((int) ($paid_order['product_id'] ?? 0)); @@ -418,6 +459,9 @@ trait VipServicePurchase { // unit upgrade: raise level only, keep current expiry (zibll). if ($upgrade_mode === 'unit') { $this->activate_vip($user_id, $level, 0, true); + } elseif ($time > 0 || (isset($meta['time']) && (int) $meta['time'] === 0 && $days === 0)) { + // Prefer calendar time+unit when present (including permanent time=0). + $this->activate_vip_duration($user_id, $level, $time, $unit); } else { $this->activate_vip($user_id, $level, $days, false); } @@ -439,6 +483,8 @@ trait VipServicePurchase { $current_level = (int) get_user_meta($user_id, 'aksrt_vip_level', true); $new_level = max($current_level, $level); update_user_meta($user_id, 'aksrt_vip_level', $new_level); + // Clear expired memory when re-granting. + delete_user_meta($user_id, 'aksrt_vip_level_expired'); // Unit upgrade (zibll): keep vip_exp_date, only raise level. if ($keep_expiry) { @@ -460,6 +506,41 @@ trait VipServicePurchase { update_user_meta($user_id, 'aksrt_vip_expires', $base + ($days * DAY_IN_SECONDS)); } + /** + * Activate VIP using calendar duration (zibll month/day). + * time=0 → permanent. Extends from max(now, current_exp). + */ + public function activate_vip_duration(int $user_id, int $level, int $time, string $unit = 'day'): void { + if ($user_id <= 0) { + return; + } + $level = max(1, min(2, $level)); + $unit = $unit === 'month' ? 'month' : 'day'; + $current_level = (int) get_user_meta($user_id, 'aksrt_vip_level', true); + $new_level = max($current_level, $level); + update_user_meta($user_id, 'aksrt_vip_level', $new_level); + delete_user_meta($user_id, 'aksrt_vip_level_expired'); + + if ($time <= 0) { + update_user_meta($user_id, 'aksrt_vip_expires', 0); + return; + } + + $current_exp = (int) get_user_meta($user_id, 'aksrt_vip_expires', true); + if ($current_exp === 0 && $current_level > 0) { + return; // keep permanent + } + $base = max($current_exp, time()); + $modifier = $unit === 'month' + ? ('+' . $time . ' months') + : ('+' . $time . ' days'); + $new_exp = strtotime($modifier, $base); + if ($new_exp === false) { + $new_exp = $base + ($this->duration_to_days($time, $unit) * DAY_IN_SECONDS); + } + update_user_meta($user_id, 'aksrt_vip_expires', (int) $new_exp); + } + /** diff --git a/src/AKSRT/Settings/CreditsPaymentGroups.php b/src/AKSRT/Settings/CreditsPaymentGroups.php index 009a34b2..37cf5cc5 100644 --- a/src/AKSRT/Settings/CreditsPaymentGroups.php +++ b/src/AKSRT/Settings/CreditsPaymentGroups.php @@ -153,7 +153,9 @@ trait CreditsPaymentGroups { } /** - * @return array{enabled:bool,name:string,badge_text:string,packages_raw:string,package_count:int,help_text:string} + * Dual-level VIP settings (Zibll pay_user_vip_1 / pay_user_vip_2). + * + * @return array */ public static function get_payment_vip_settings(): array { return PaymentRuntime::get_payment_vip_settings(); diff --git a/src/AKSRT/Settings/PaymentRuntime.php b/src/AKSRT/Settings/PaymentRuntime.php index e90df501..198d9e37 100644 --- a/src/AKSRT/Settings/PaymentRuntime.php +++ b/src/AKSRT/Settings/PaymentRuntime.php @@ -150,27 +150,60 @@ class PaymentRuntime { $renew_price_type = 'discount'; } - $packages = (string) ($settings['payment_vip_packages'] ?? "30|30|月卡\n365|365|年卡"); + $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_textarea_field((string) ($settings['pay_user_vip_desc'] ?? '')); + $more = (string) ($settings['pay_user_vip_more'] ?? ''); + $allowed_equity = [ + 'li' => [], + 'ul' => [], + 'ol' => [], + 'strong' => [], + 'b' => [], + 'em' => [], + 'i' => ['class' => true], + 'span' => ['class' => true], + 'br' => [], + 'a' => ['href' => true, 'target' => true, 'rel' => true, 'class' => true], + ]; return [ 'enabled' => $enabled, 'name' => $vip1_enabled ? $name1 : $badge, 'badge_text' => $badge, 'desc' => $desc, - 'packages_raw' => $packages, - 'package_count' => self::count_structured_lines($packages), + 'more' => wp_kses($more, $allowed_equity + ['div' => ['class' => true], 'p' => ['class' => true]]), 'help_text' => $desc !== '' ? $desc : __('会员权益以免扣下载和身份展示为主,开通后立即生效。', 'aksrt'), 'levels' => [ 1 => [ - 'enabled' => $vip1_enabled, - 'name' => $name1, + 'enabled' => $vip1_enabled, + 'name' => $name1, + 'desc' => sanitize_text_field((string) ($settings['pay_user_vip_1_desc'] ?? '')), + 'equity' => wp_kses((string) ($settings['pay_user_vip_1_equity'] ?? ''), $allowed_equity), + 'img_icon' => esc_url_raw((string) ($settings['vip_1_img_icon'] ?? '')), + 'product_s' => !isset($settings['vip_1_product_s']) || !empty($settings['vip_1_product_s']), + 'products_raw' => $products_1, 'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit_vip_1'] ?? 0))), 'download_speed' => max(0, min(99999, (int) ($settings['pay_download_speed_vip_1'] ?? 0))), ], 2 => [ - 'enabled' => $vip2_enabled, - 'name' => $name2, + 'enabled' => $vip2_enabled, + 'name' => $name2, + 'desc' => sanitize_text_field((string) ($settings['pay_user_vip_2_desc'] ?? '')), + 'equity' => wp_kses((string) ($settings['pay_user_vip_2_equity'] ?? ''), $allowed_equity), + 'img_icon' => esc_url_raw((string) ($settings['vip_2_img_icon'] ?? '')), + 'product_s' => !isset($settings['vip_2_product_s']) || !empty($settings['vip_2_product_s']), + 'products_raw' => $products_2, 'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit_vip_2'] ?? 0))), 'download_speed' => max(0, min(99999, (int) ($settings['pay_download_speed_vip_2'] ?? 0))), ], @@ -198,7 +231,7 @@ class PaymentRuntime { 'points_exchange' => [ 'enabled' => !empty($settings['pay_vip_points_exchange_s']), 'desc' => sanitize_textarea_field((string) ($settings['pay_vip_points_exchange_desc'] ?? '')), - 'packages_raw' => (string) ($settings['payment_vip_points_packages'] ?? "1|500|30|积分月卡\n1|5000|0|积分永久"), + 'packages_raw' => sanitize_textarea_field((string) ($settings['payment_vip_points_packages'] ?? "1|500|30|积分月卡\n1|5000|0|积分永久")), ], ]; }