From 3481620ed0c2cd5800ee88bde749d3837af4bb5c Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 17 Sep 2026 19:04:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(payment):=20=E9=80=9A=E7=94=A8=E5=8D=A1?= =?UTF-8?q?=E5=AF=86=E5=85=91=E6=8D=A2=E5=BC=B9=E7=AA=97=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E6=A0=B8=E9=94=80=E8=AE=A2=E5=8D=95=E5=AE=9E=E4=BB=98?= =?UTF-8?q?=E4=B8=8E=E8=AE=A2=E5=8D=95=E5=90=8D=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /user/balance/ 余额卡片入口改为通用「卡密兑换」弹窗:请求不带类型, 后端按卡密自动识别余额/会员/积分;卡号输入恒展示且选填 - 后台「余额充值 → 卡密充值」语义更新为「卡密兑换」,开关更名 「启用卡密兑换功能」(设置键不变),双源 schema 同步并重建 SPA - 删除死代码:credits-vip.php 死文件、credits-balance.php 内联积分 兑换块、/pay/card-pass/options 路由与 rest_options、labels 数组、 asset_points_card_description/password_only 死字段及内联块专属 CSS - 卡密核销订单实付恒 0:create_redeem_order 源头写 0,UserOrders 对 card_pass 订单视图层归零覆盖历史行(不迁移数据) - 订单名称按 note.meta 生成:卡密兑换-余额充值X / 卡密兑换-积分充值X / 购入[会员档位名]X天(永久) - 新增契约 card-pass-universal-redeem.test.cjs,收紧 user-assets-layout 契约(禁 .uc-card-pass 根类回潮) --- .gitignore | 1 + .../src/schema/sections/payment/recharge.js | 12 +- .../src/schema/sections/users/credits.js | 6 +- assets/css/routes/user-center/credits.css | 43 +-- assets/js/credits.js | 12 +- .../user-center-renderers/credits-balance.php | 25 -- .../user-center-renderers/credits-vip.php | 269 ------------------ .../user-center-renderers/credits-wallet.php | 30 +- src/Admin/Schema/SchemaPayment.php | 8 +- src/Admin/Schema/SchemaUsers.php | 4 +- src/Payment/ChargeCard.php | 51 +--- src/Payment/UserOrders.php | 52 +++- .../card-pass-universal-redeem.test.cjs | 111 ++++++++ .../user-assets-layout-contract.test.cjs | 4 +- 14 files changed, 213 insertions(+), 415 deletions(-) delete mode 100644 inc/helpers/user-center-renderers/credits-vip.php create mode 100644 tests/widget-system/card-pass-universal-redeem.test.cjs diff --git a/.gitignore b/.gitignore index eed9250e..7ac8ecf0 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ !/tests/widget-system/circular-button-icon-margin-contract.test.cjs !/tests/widget-system/single-cover-height-contract.test.cjs !/tests/widget-system/frontend-lifecycle-contract.test.cjs +!/tests/widget-system/card-pass-universal-redeem.test.cjs !/tests/admin/ /tests/admin/* !/tests/admin/charge-cards-page-contract.test.cjs diff --git a/assets/admin/src/schema/sections/payment/recharge.js b/assets/admin/src/schema/sections/payment/recharge.js index bac9c1c8..973c76d7 100644 --- a/assets/admin/src/schema/sections/payment/recharge.js +++ b/assets/admin/src/schema/sections/payment/recharge.js @@ -1,13 +1,13 @@ /** * 余额充值 — 参考主题结构:用户余额(含余额提现)、转账、充值商品选项、 - * 卡密充值与余额兑换积分。卡密的生成/管理在后台「卡密管理」页完成, + * 卡密兑换与余额兑换积分。卡密的生成/管理在后台「卡密管理」页完成, * 此处不再保留「充值卡」配置项。 */ export const rechargeSchema = { key: 'payment.recharge', title: '余额充值', icon: 'fa fas fa-bolt', - description: '配置余额功能、充值商品选项、卡密充值与余额兑换积分。', + description: '配置余额功能、充值商品选项、卡密兑换与余额兑换积分。', defaults: { pay_balance_s: true, pay_balance_withdraw_s: false, @@ -95,12 +95,12 @@ export const rechargeSchema = { }, { key: 'pass-charge', - title: '卡密充值', + title: '卡密兑换', icon: 'fa fas fa-ticket-alt', fields: [ - { key: 'pay_balance_pass_charge_s', label: '启用卡密充值余额功能', type: 'switch', default: false }, - { key: 'pay_balance_pass_charge_desc', label: '卡密充值说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'pay_balance_pass_charge_s', value: true }, help: '用户使用卡密充值时显示的内容,建议为提醒事项、充值协议等,支持HTML代码' }, - { key: 'pay_balance_pass_charge_only_password', label: '单密码模式', type: 'switch', default: false, dependsOn: { key: 'pay_balance_pass_charge_s', value: true }, help: '仅需输入密码,不需要卡号' }, + { key: 'pay_balance_pass_charge_s', label: '启用卡密兑换功能', type: 'switch', default: false, help: '开启后用户中心余额卡片展示通用「卡密兑换」入口,支持余额 / 会员 / 积分卡密,类型按卡密自动识别' }, + { key: 'pay_balance_pass_charge_desc', label: '卡密兑换说明', type: 'textarea', default: '', rows: 2, dependsOn: { key: 'pay_balance_pass_charge_s', value: true }, help: '用户使用卡密兑换时显示的内容,建议为提醒事项、兑换协议等,支持HTML代码' }, + { key: 'pay_balance_pass_charge_only_password', label: '单密码模式', type: 'switch', default: false, dependsOn: { key: 'pay_balance_pass_charge_s', value: true }, help: '余额卡密仅需输入密码,不需要卡号' }, ], }, { diff --git a/assets/admin/src/schema/sections/users/credits.js b/assets/admin/src/schema/sections/users/credits.js index 35ac4274..e4d93136 100644 --- a/assets/admin/src/schema/sections/users/credits.js +++ b/assets/admin/src/schema/sections/users/credits.js @@ -27,8 +27,6 @@ export const creditsSchema = { points_deduction_rate: 100, points_deduction_max: 50, asset_points_card_enabled: false, - asset_points_card_description: '', - asset_points_card_password_only: true, points_show_balance: true, points_show_log: true, points_expire_days: 0, @@ -95,9 +93,7 @@ export const creditsSchema = { title: '积分卡密兑换', icon: 'fa fas fa-ticket-alt', fields: [ - { key: 'asset_points_card_enabled', label: '启用积分卡密兑换', type: 'switch', default: false, help: '后台充值卡可生成积分卡,用户中心兑换入账。' }, - { key: 'asset_points_card_description', label: '兑换说明', type: 'textarea', default: '', dependsOn: { key: 'asset_points_card_enabled', value: true } }, - { key: 'asset_points_card_password_only', label: '仅密码兑换', type: 'switch', default: true, dependsOn: { key: 'asset_points_card_enabled', value: true }, help: '开启后只需输入卡密;关闭则需卡号+卡密。' }, + { key: 'asset_points_card_enabled', label: '启用积分卡密兑换', type: 'switch', default: false, help: '开启后积分卡密可在用户中心「卡密兑换」弹窗兑换入账。' }, ], }, { diff --git a/assets/css/routes/user-center/credits.css b/assets/css/routes/user-center/credits.css index 4e9fbae2..8f5cf45a 100644 --- a/assets/css/routes/user-center/credits.css +++ b/assets/css/routes/user-center/credits.css @@ -33,16 +33,12 @@ .uc-assets-row--dual > .uc-assets-col:first-child > .uc-asset-card { grid-column: 1; } -.uc-assets-row--dual > .uc-assets-col:last-child > .uc-asset-card, -.uc-assets-row--dual > .uc-assets-col:last-child > .uc-card-pass { +.uc-assets-row--dual > .uc-assets-col:last-child > .uc-asset-card { grid-column: 2; } .uc-assets-row--dual > .uc-assets-col > .uc-asset-card { grid-row: 1; } -.uc-assets-row--dual > .uc-assets-col > .uc-card-pass { - grid-row: 2; -} .uc-assets-col { min-width: 0; display: flex; @@ -59,8 +55,7 @@ grid-column: auto; grid-row: auto; } - .uc-assets-row--dual > .uc-assets-col > .uc-asset-card, - .uc-assets-row--dual > .uc-assets-col > .uc-card-pass { + .uc-assets-row--dual > .uc-assets-col > .uc-asset-card { grid-column: auto; grid-row: auto; } @@ -87,15 +82,6 @@ flex-wrap: wrap; row-gap: 10px; } -/* Card-pass under a col must not look like a second stacked card */ -.uc-assets-col > .uc-card-pass { - margin: 0; - padding: 12px 14px; - border-radius: 8px; - border: 1px dashed var(--color-line, #e5e7eb); - background: var(--muted-bg-color, #f8fafc); - box-shadow: none; -} .uc-asset-card__head { margin-bottom: 8px; } @@ -830,30 +816,7 @@ body.uc-modal-open { border-color: var(--color-line, #e5e7eb); } -/* Card-pass redeem (balance / VIP) */ -.uc-card-pass { - margin: 16px 0 0; - padding: 14px 16px; - border: 1px solid var(--color-line, #e5e7eb); - border-radius: 12px; - background: #fff; -} -.uc-card-pass__title { - display: flex; - align-items: center; - gap: 8px; - font-size: 14px; - font-weight: 700; - color: #111827; - margin: 0 0 8px; -} -.uc-card-pass__title i { color: #2563eb; } -.uc-card-pass__desc { - margin: 0 0 10px; - font-size: 12px; - color: #6b7280; - line-height: 1.5; -} +/* Card-pass redeem modal inputs (universal modal in credits-wallet.php) */ .uc-card-pass__input { display: block; width: 100%; diff --git a/assets/js/credits.js b/assets/js/credits.js index cebda6c0..8e15442a 100644 --- a/assets/js/credits.js +++ b/assets/js/credits.js @@ -266,7 +266,7 @@ }); } - // ===== 卡密兑换(余额 / 会员) ===== + // ===== 卡密兑换(余额 / 会员 / 积分,通用弹窗由后端自动识别类型) ===== function initCardPass() { var roots = document.querySelectorAll('[data-aksrt-card-pass]'); if (!roots || !roots.length) return; @@ -306,16 +306,12 @@ var cardInput = root.querySelector('[data-aksrt-card-no]'); var password = pwInput ? String(pwInput.value || '').trim() : ''; var card = cardInput ? String(cardInput.value || '').trim() : ''; + // 卡号为选填:是否需要卡号由后端按卡密类型判定并返回错误提示。 if (!password) { setTip('请输入卡密', true); if (pwInput) pwInput.focus(); return; } - if (cardInput && !card) { - setTip('请输入卡号', true); - cardInput.focus(); - return; - } busy = true; btn.disabled = true; @@ -337,7 +333,9 @@ } var data = (r.json && r.json.data) ? r.json.data : r.json; resetOperationKey(root); - if (type === 'vip' && window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === 'function') { + // 通用兑换(type 为空)时以后端识别出的卡密类型为准。 + var redeemedType = (data && data.type) || type; + if (redeemedType === 'vip' && window.AksrtModalRuntime && typeof window.AksrtModalRuntime.invalidate === 'function') { window.AksrtModalRuntime.invalidate(['vip', 'orders']); } setTip((data && data.message) || '兑换成功,正在刷新…'); diff --git a/inc/helpers/user-center-renderers/credits-balance.php b/inc/helpers/user-center-renderers/credits-balance.php index 0acef0dd..a11192ea 100644 --- a/inc/helpers/user-center-renderers/credits-balance.php +++ b/inc/helpers/user-center-renderers/credits-balance.php @@ -13,11 +13,6 @@ if (!$credits_enabled) { return; } -$card_pass_credits = ['enabled' => false, 'desc' => '', 'only_password' => true]; -if (class_exists('\AKSRT\Payment\ChargeCard')) { - $opts = \AKSRT\Payment\ChargeCard::get_instance()->get_public_options(); - $card_pass_credits = is_array($opts['credits'] ?? null) ? $opts['credits'] : $card_pass_credits; -} $all_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : []; $points_uid = (int) ($user_id ?? get_current_user_id()); $points_transfer_on = !empty($all_settings['points_transfer_s']); @@ -83,23 +78,3 @@ $currency = (string) ($currency_name ?? __('积分', 'aksrt')); - - -
-
- - -
- -

- - - - - - -

-
- diff --git a/inc/helpers/user-center-renderers/credits-vip.php b/inc/helpers/user-center-renderers/credits-vip.php deleted file mode 100644 index fbf41822..00000000 --- a/inc/helpers/user-center-renderers/credits-vip.php +++ /dev/null @@ -1,269 +0,0 @@ - false, 'packages' => [], 'desc' => '']; -$user_points = 0; -$currency_name = __('积分', 'aksrt'); -$methods = [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]]; -if (class_exists('\AKSRT\Content\VipService')) { - $catalog = \AKSRT\Content\VipService::get_instance()->get_catalog($user_id); - $vip_level = (int) ($catalog['user_level'] ?? 0); - $expires = (int) ($catalog['user_expires'] ?? 0); - $expires_label = (string) ($catalog['user_expires_label'] ?? $expires_label); - $is_vip_user = $vip_level > 0; - $packages = is_array($catalog['level_packages'] ?? null) ? $catalog['level_packages'] : []; - $methods = is_array($catalog['payment_methods'] ?? null) ? $catalog['payment_methods'] : $methods; - $renew_desc = (string) ($catalog['renew_desc'] ?? ''); - $upgrade_desc = (string) ($catalog['upgrade_desc'] ?? ''); - $points_exchange = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : $points_exchange; - $user_points = (int) ($catalog['user_points'] ?? 0); - if (!empty($catalog['currency_name'])) { - $currency_name = (string) $catalog['currency_name']; - } -} elseif (class_exists('\\AKSRT\\Settings\\Settings')) { - $currency_name = \AKSRT\Settings\Settings::get_currency_name(); -} -if ($vip_level >= 1 && !empty($vip_settings['levels'][$vip_level]['name'])) { - $level_name = (string) $vip_settings['levels'][$vip_level]['name']; -} - -$level_icon_url = esc_url((string) ($vip_settings['levels'][$vip_level]['img_icon'] ?? '')); -$level_icon = $level_icon_url !== '' - ? '' - : ''; -if (class_exists('\AKSRT\Settings\Settings')) { - $paid_display = \AKSRT\Settings\Settings::get_paid_content_settings(); - $cash_symbol = (string) ($paid_display['display']['currency_symbol'] ?? '¥'); -} - -$shop_title = __('开通会员', 'aksrt'); -if ($vip_level > 0) { - $shop_title = __('续费 / 升级会员', 'aksrt'); -} -$points_pkgs = is_array($points_exchange['packages'] ?? null) ? $points_exchange['packages'] : []; - -$card_pass_vip = ['enabled' => false, 'desc' => '', 'only_password' => true]; -if (class_exists('\AKSRT\Payment\ChargeCard')) { - $card_opts = \AKSRT\Payment\ChargeCard::get_instance()->get_public_options(); - $card_pass_vip = is_array($card_opts['vip'] ?? null) ? $card_opts['vip'] : $card_pass_vip; -} elseif (!empty($vip_settings['benefit']['pass_charge'])) { - $card_pass_vip = [ - 'enabled' => true, - 'desc' => (string) ($vip_settings['benefit']['pass_charge_desc'] ?? ''), - 'only_password' => !isset($vip_settings['benefit']['pass_charge_only_password']) || !empty($vip_settings['benefit']['pass_charge_only_password']), - ]; -} -?> -
-
-
-
-
- -
-
- - - - - - -
-
-
- - - -
- -

- -
-
- - -
-
- - -
- - 0 && $renew_desc !== '') : ?> -

- - -

- - - = $g_level) { - $group_badge = __('当前档', 'aksrt'); - } - ?> -
-
- - - - -
-
- - - -
-
- - - 1) : ?> -
- - $m) : - $mid = (string) ($m['id'] ?? ''); - $mname = (string) ($m['name'] ?? $mid); - ?> - - -
- - - - -

-
- - - -
-
- - -
-
- -
- -

- -
- = $cost; - ?> - - -
-

-
- -

- - - -
-
- - -
- -

- - - - - - -

-
- diff --git a/inc/helpers/user-center-renderers/credits-wallet.php b/inc/helpers/user-center-renderers/credits-wallet.php index fa021f26..125024e4 100644 --- a/inc/helpers/user-center-renderers/credits-wallet.php +++ b/inc/helpers/user-center-renderers/credits-wallet.php @@ -45,12 +45,12 @@ if ($withdraw_on && class_exists('\AKSRT\Payment\Withdrawal')) { $withdraw_pending_hold = $wd_svc->sum_pending_hold((int) ($user_id ?? 0)); $withdraw_usage = $wd_svc->daily_usage((int) ($user_id ?? 0), $withdraw_settings); } -$card_pass_balance = ['enabled' => false, 'desc' => '', 'only_password' => true]; +// 卡密兑换总开关(后台 余额充值 → 卡密兑换 → 启用卡密兑换功能), +// 通用弹窗支持余额 / 会员 / 积分三类卡密,类型由后端按卡密自动识别。 +$card_pass_redeem = ['enabled' => false, 'desc' => '']; if (class_exists('\AKSRT\Payment\ChargeCard')) { $opts = \AKSRT\Payment\ChargeCard::get_instance()->get_public_options(); - $card_pass_balance = is_array($opts['balance'] ?? null) ? $opts['balance'] : $card_pass_balance; -} elseif (!empty($recharge['pass_charge'])) { - $card_pass_balance = is_array($recharge['pass_charge']) ? $recharge['pass_charge'] : $card_pass_balance; + $card_pass_redeem = is_array($opts['balance'] ?? null) ? $opts['balance'] : $card_pass_redeem; } $charge_view = []; @@ -92,14 +92,14 @@ $mark = (string) ($charge_view['currency_mark'] ?? '元'); - +
formatted())); ?> - +
- -