diff --git a/assets/admin/src/schema/sections/payment/vip.js b/assets/admin/src/schema/sections/payment/vip.js index 4ac2a4ef..0ceca486 100644 --- a/assets/admin/src/schema/sections/payment/vip.js +++ b/assets/admin/src/schema/sections/payment/vip.js @@ -40,8 +40,6 @@ export const vipSchema = { vip_upgrade: true, vip_upgrade_desc: '', vip_upgrade_product: 'unit|1|按剩余天数升级\njump|199|升级永久二级会员\npermanent|299|永久一级升永久二级', - pay_vip_pass_charge_s: false, - pay_vip_pass_charge_desc: '', pay_vip_pass_charge_only_password: true, pay_vip_points_exchange_s: false, payment_vip_points_packages: '1|500|30|积分月卡\n1|5000|0|积分永久', @@ -205,12 +203,10 @@ export const vipSchema = { }, { key: 'pass-charge', - title: '卡密兑换会员', + title: '会员卡密', icon: 'fa fas fa-ticket-alt', fields: [ - { key: 'pay_vip_pass_charge_s', label: '启用卡密兑换会员功能', type: 'switch', default: false }, - { key: 'pay_vip_pass_charge_desc', label: '卡密兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '用户使用卡密兑换时显示的内容,建议为提醒事项、充值协议等,支持HTML代码' }, - { key: 'pay_vip_pass_charge_only_password', label: '单密码模式', type: 'switch', default: true, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '仅需输入密码,不需要卡号' }, + { key: 'pay_vip_pass_charge_only_password', label: '单密码模式', type: 'switch', default: true, help: '会员卡密核销是否仅需卡密;关闭后,带卡号的会员卡必须同时输入卡号。会员/积分卡密无独立开关,统一受「余额充值 → 卡密兑换 → 启用卡密兑换功能」控制' }, ], }, { diff --git a/assets/admin/src/schema/sections/users/credits.js b/assets/admin/src/schema/sections/users/credits.js index e4d93136..51d74513 100644 --- a/assets/admin/src/schema/sections/users/credits.js +++ b/assets/admin/src/schema/sections/users/credits.js @@ -26,7 +26,6 @@ export const creditsSchema = { points_deduction_s: false, points_deduction_rate: 100, points_deduction_max: 50, - asset_points_card_enabled: false, points_show_balance: true, points_show_log: true, points_expire_days: 0, @@ -88,14 +87,6 @@ export const creditsSchema = { { key: 'points_deduction_max', label: '最大抵扣比例(%)', type: 'number', default: 50, min: 1, max: 100, dependsOn: { key: 'points_deduction_s', value: true }, help: '预留:最多可抵订单金额的百分比。当前未参与下单计算。' }, ], }, - { - key: 'card_pass', - title: '积分卡密兑换', - icon: 'fa fas fa-ticket-alt', - fields: [ - { key: 'asset_points_card_enabled', label: '启用积分卡密兑换', type: 'switch', default: false, help: '开启后积分卡密可在用户中心「卡密兑换」弹窗兑换入账。' }, - ], - }, { key: 'display', title: '展示设置', diff --git a/src/Admin/Schema/SchemaPayment.php b/src/Admin/Schema/SchemaPayment.php index 5efc4c6a..da4dd93f 100644 --- a/src/Admin/Schema/SchemaPayment.php +++ b/src/Admin/Schema/SchemaPayment.php @@ -241,10 +241,8 @@ class SchemaPayment { ]], ]], ]], - ['key' => 'pass-charge', 'title' => '卡密兑换会员', 'fields' => [ - ['key' => 'pay_vip_pass_charge_s', 'label' => '启用卡密兑换会员功能', 'type' => 'switch', 'default' => false], - ['key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '用户使用卡密兑换时显示的内容,建议为提醒事项、充值协议等,支持HTML代码'], - ['key' => 'pay_vip_pass_charge_only_password', 'label' => '单密码模式', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true], 'help' => '仅需输入密码,不需要卡号'], + ['key' => 'pass-charge', 'title' => '会员卡密', 'fields' => [ + ['key' => 'pay_vip_pass_charge_only_password', 'label' => '单密码模式', 'type' => 'switch', 'default' => true, 'help' => '会员卡密核销是否仅需卡密;关闭后,带卡号的会员卡必须同时输入卡号。会员/积分卡密无独立开关,统一受「余额充值 → 卡密兑换 → 启用卡密兑换功能」控制'], ]], ['key' => 'points-exchange', 'title' => '积分兑换会员', 'fields' => [ ['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false], diff --git a/src/Admin/Schema/SchemaUsers.php b/src/Admin/Schema/SchemaUsers.php index ecce1f2e..ebb55c5e 100644 --- a/src/Admin/Schema/SchemaUsers.php +++ b/src/Admin/Schema/SchemaUsers.php @@ -486,9 +486,6 @@ class SchemaUsers { ['key' => 'points_deduction_rate', 'label' => '积分换算比例', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 10000, 'help' => '多少积分抵 1 元(legacy points_deduction_rate)。当前未参与下单。'], ['key' => 'points_deduction_max', 'label' => '最大抵扣比例(%)', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 100, 'help' => '预留:最多可抵订单金额百分比。当前未参与下单。'], ]], - ['key' => 'card_pass', 'title' => '积分卡密兑换', 'fields' => [ - ['key' => 'asset_points_card_enabled', 'label' => '启用积分卡密兑换', 'type' => 'switch', 'default' => false, 'help' => '开启后积分卡密可在用户中心「卡密兑换」弹窗兑换入账'], - ]], ['key' => 'display', 'title' => '展示设置', 'fields' => [ ['key' => 'points_show_balance', 'label' => '导航栏显示余额', 'type' => 'switch', 'default' => true], ['key' => 'points_show_log', 'label' => '显示积分记录', 'type' => 'switch', 'default' => true], diff --git a/src/Payment/ChargeCard.php b/src/Payment/ChargeCard.php index 5de3f20c..4e3055cc 100644 --- a/src/Payment/ChargeCard.php +++ b/src/Payment/ChargeCard.php @@ -112,35 +112,31 @@ class ChargeCard { } /** - * 前台卡密兑换开关视图(按类型拆分)。 + * 前台卡密兑换开关视图。 * - * balance.enabled 语义为「启用卡密兑换功能」总开关(后台 余额充值 → 卡密兑换), - * 与收款接口 card_pass 主开关共同决定前台兑换入口是否展示。 + * 唯一类型闸门:balance.enabled =「启用卡密兑换功能」总开关 + * (后台 余额充值 → 卡密兑换),并与收款接口 card_pass 主开关共同生效; + * 会员/积分卡密不再有独立开关。积分卡额外要求积分系统可用(系统级依赖)。 * * @return array */ public function get_public_options(): array { - // Master kill-switch on 收款接口 → 卡密充值. + // 收款接口 card_pass 主开关;关闭时所有卡密兑换一并失效。 $channel_on = !class_exists('\AKSRT\Settings\PaymentRuntime') || \AKSRT\Settings\PaymentRuntime::is_gateway_enabled('card_pass'); - if (!$channel_on) { - $off = ['enabled' => false, 'desc' => '', 'only_password' => true]; - return [ - 'balance' => $off, - 'vip' => $off, - 'credits' => ['enabled' => false], - ]; - } - $balance = ['enabled' => false, 'desc' => '', 'only_password' => true]; - $vip = ['enabled' => false, 'desc' => '', 'only_password' => true]; + $master_on = false; + $balance = ['enabled' => false, 'desc' => '', 'only_password' => true]; + $vip_only_password = true; + $credits_on = false; - if (class_exists('\AKSRT\Settings\Settings')) { + if ($channel_on && class_exists('\AKSRT\Settings\Settings')) { if (method_exists('\AKSRT\Settings\Settings', 'get_payment_recharge_settings')) { $recharge = \AKSRT\Settings\Settings::get_payment_recharge_settings(); $pc = is_array($recharge['pass_charge'] ?? null) ? $recharge['pass_charge'] : []; + $master_on = !empty($pc['enabled']); $balance = [ - 'enabled' => !empty($pc['enabled']), + 'enabled' => $master_on, 'desc' => (string) ($pc['desc'] ?? ''), 'only_password' => !isset($pc['only_password']) || !empty($pc['only_password']), ]; @@ -148,30 +144,18 @@ class ChargeCard { if (method_exists('\AKSRT\Settings\Settings', 'get_payment_vip_settings')) { $vip_s = \AKSRT\Settings\Settings::get_payment_vip_settings(); $benefit = is_array($vip_s['benefit'] ?? null) ? $vip_s['benefit'] : []; - $vip = [ - 'enabled' => !empty($benefit['pass_charge']), - 'desc' => (string) ($benefit['pass_charge_desc'] ?? ''), - 'only_password' => !isset($benefit['pass_charge_only_password']) || !empty($benefit['pass_charge_only_password']), - ]; + $vip_only_password = !isset($benefit['pass_charge_only_password']) || !empty($benefit['pass_charge_only_password']); + } + if ($master_on) { + // 积分卡随总开关生效,仅要求积分系统可用(系统级依赖,非独立开关)。 + $credits_on = !method_exists('\AKSRT\Settings\Settings', 'credits_enabled') || \AKSRT\Settings\Settings::credits_enabled(); } - } - - $credits = ['enabled' => false]; - $all = class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_all') - ? \AKSRT\Settings\Settings::get_all() - : (array) get_option('aksrt_settings', []); - if (!empty($all['asset_points_card_enabled'])) { - // 积分卡仅作为后端类型闸门(assert_type_enabled);前台兑换统一走 - // 通用「卡密兑换」弹窗,无独立入口与说明/单密码配置。 - $credits = [ - 'enabled' => !class_exists('\AKSRT\Settings\Settings') || !method_exists('\AKSRT\Settings\Settings', 'credits_enabled') || \AKSRT\Settings\Settings::credits_enabled(), - ]; } return [ 'balance' => $balance, - 'vip' => $vip, - 'credits' => $credits, + 'vip' => ['only_password' => $vip_only_password], + 'credits' => ['enabled' => $credits_on], ]; } @@ -835,14 +819,13 @@ class ChargeCard { */ private function assert_type_enabled(string $type) { $opts = $this->get_public_options(); - if ($type === self::TYPE_BALANCE && empty($opts['balance']['enabled'])) { - return new \WP_Error('aksrt_card_disabled', __('余额卡密兑换未开启', 'aksrt'), ['status' => 403]); - } - if ($type === self::TYPE_VIP && empty($opts['vip']['enabled'])) { - return new \WP_Error('aksrt_card_disabled', __('会员卡密兑换未开启', 'aksrt'), ['status' => 403]); + // 唯一类型闸门:后台「余额充值 → 卡密兑换」总开关(已含收款接口 card_pass 主开关)。 + if (empty($opts['balance']['enabled'])) { + return new \WP_Error('aksrt_card_disabled', __('卡密兑换功能未开启', 'aksrt'), ['status' => 403]); } + // 积分卡额外要求积分系统可用(系统级依赖,非独立开关)。 if ($type === self::TYPE_CREDITS && empty($opts['credits']['enabled'])) { - return new \WP_Error('aksrt_card_disabled', __('积分卡密兑换未开启', 'aksrt'), ['status' => 403]); + return new \WP_Error('aksrt_card_disabled', __('积分系统未开启,无法兑换积分卡密', 'aksrt'), ['status' => 403]); } return true; } diff --git a/src/Settings/PaymentRuntime.php b/src/Settings/PaymentRuntime.php index 1a30c6d2..80e151bf 100644 --- a/src/Settings/PaymentRuntime.php +++ b/src/Settings/PaymentRuntime.php @@ -229,8 +229,7 @@ class PaymentRuntime { ], 'benefit' => [ 'download_limit' => max(0, min(9999, (int) ($settings['pay_download_limit'] ?? 0))), - 'pass_charge' => !empty($settings['pay_vip_pass_charge_s']), - 'pass_charge_desc' => sanitize_textarea_field((string) ($settings['pay_vip_pass_charge_desc'] ?? '')), + // 会员卡密无独立开关(随「启用卡密兑换功能」总开关),仅保留单密码模式。 'pass_charge_only_password' => !isset($settings['pay_vip_pass_charge_only_password']) || !empty($settings['pay_vip_pass_charge_only_password']), ], 'renew' => [ diff --git a/tests/widget-system/card-pass-universal-redeem.test.cjs b/tests/widget-system/card-pass-universal-redeem.test.cjs index e7f37544..28b0322d 100644 --- a/tests/widget-system/card-pass-universal-redeem.test.cjs +++ b/tests/widget-system/card-pass-universal-redeem.test.cjs @@ -32,6 +32,7 @@ const schemaPhp = read('src', 'Admin', 'Schema', 'SchemaPayment.php'); const schemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'recharge.js'); const usersSchemaPhp = read('src', 'Admin', 'Schema', 'SchemaUsers.php'); const usersSchemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'users', 'credits.js'); +const vipSchemaJs = read('assets', 'admin', 'src', 'schema', 'sections', 'payment', 'vip.js'); const creditsRenderer = read('inc', 'helpers', 'user-center-renderers', 'credits-balance.php'); const creditsCss = read('assets', 'css', 'routes', 'user-center', 'credits.css'); @@ -63,10 +64,11 @@ test('前台唯一兑换入口:页内联块与死文件已删除,JS 不再 ); assert.doesNotMatch(creditsRenderer, /data-aksrt-card-pass|data-aksrt-card-redeem|card_pass_credits/); assert.doesNotMatch(creditsJs, /hasAttribute\('required'\)/); - // 积分卡的说明/单密码配置只服务于已删内联块,后台 schema 不应再有。 - assert.doesNotMatch(usersSchemaPhp, /asset_points_card_description|asset_points_card_password_only/); - assert.doesNotMatch(usersSchemaJs, /asset_points_card_description|asset_points_card_password_only/); - assert.doesNotMatch(chargeCard, /asset_points_card_description|asset_points_card_password_only/); + // 会员/积分卡密无独立开关与说明/单密码配置残留。 + assert.doesNotMatch(usersSchemaPhp, /asset_points_card_/); + assert.doesNotMatch(usersSchemaJs, /asset_points_card_/); + assert.doesNotMatch(chargeCard, /asset_points_card_/); + assert.doesNotMatch(chargeCard, /'pass_charge_desc'/); // 失去宿主的内联块样式不回潮(弹窗仍在用 __input/__btn/__tip 子元素样式)。 assert.doesNotMatch(creditsCss, /^\.uc-card-pass\s*\{/m); assert.doesNotMatch(creditsCss, /uc-card-pass__title|uc-card-pass__desc|\.uc-assets-col > \.uc-card-pass/); @@ -92,6 +94,21 @@ test('后端:核销订单金额恒为 0,options 路由与 labels 死代码 assert.match(chargeCard, /'credits' => \['enabled' => /); }); +test('唯一类型闸门:总开关统一约束三类卡密,独立开关不回潮', () => { + // assert_type_enabled 只查总开关(+ 积分系统级依赖),不再有按类型的独立开关。 + assert.match(chargeCard, /卡密兑换功能未开启/); + assert.match(chargeCard, /积分系统未开启/); + assert.doesNotMatch(chargeCard, /余额卡密兑换未开启|会员卡密兑换未开启|积分卡密兑换未开启/); + // 会员/积分卡密独立开关与说明为死代码。 + assert.doesNotMatch(chargeCard, /pay_vip_pass_charge_s|pay_vip_pass_charge_desc|pass_charge'\s*=>/); + for (const [name, src] of [['SchemaPayment.php', schemaPhp], ['recharge.js', schemaJs], ['vip.js', vipSchemaJs]]) { + assert.doesNotMatch(src, /pay_vip_pass_charge_s|pay_vip_pass_charge_desc|启用卡密兑换会员功能/, name); + assert.doesNotMatch(src, /启用积分卡密兑换/, name); + } + assert.match(vipSchemaJs, /title: '会员卡密'/); + assert.match(vipSchemaJs, /pay_vip_pass_charge_only_password/); +}); + test('订单中心:卡密核销订单实付归零并按 note 生成订单名称', () => { assert.match(userOrders, /if \(\$payment_method === 'card_pass'\)\s*\{\s*\$amount = \$is_points \? '0' : '0\.00';/); assert.match(userOrders, /if \(!empty\(\$meta\['card_pass'\]\)\)\s*\{[\s\S]*?resolve_card_pass_title\(\$meta\)/);