feat(pay): 卡密兑换入口收敛到三个购买弹窗并对齐参考主题

- 移除余额卡片独立「卡密兑换」通用弹窗,兑换入口分布到充值/积分/会员三个弹窗的支付方式中
- 充值弹窗补齐卡密 logo 与仅卡密时的金额选择隐藏逻辑
- 积分购买弹窗 1:1 复刻参考主题 points-pay modal(商品档位/自定义/支付方式/立即支付),REST 改为 product+custom 契约
- 会员弹窗注入卡密渠道:视图模型透传 card_pass,前端渲染卡密盒并直兑 /pay/card-pass/redeem type=vip
- 后台卡密兑换拆分为余额/积分/会员三个独立开关,VIP 段旧会员卡密组清理
- REST type 改为必填(balance/vip/credits),三类型独立闸门
- 测试重写为 card-pass-modal-redeem 契约,321 项全部通过
This commit is contained in:
2026-09-18 12:25:18 +08:00
parent 74e43aaa76
commit b88eab0bfe
18 changed files with 746 additions and 472 deletions
@@ -48,7 +48,7 @@ $currency = (string) ($currency_name ?? __('积分', 'aksrt'));
<button type="button" class="but c-green mr6 hollow" data-aksrt-transfer-open="points" data-aksrt-transfer-min="<?php echo esc_attr((string) $points_transfer_min); ?>"><?php esc_html_e('转账', 'aksrt'); ?></button>
<?php endif; ?>
<?php if ($points_exchange_on) : ?>
<button type="button" class="but c-green hollow" data-aksrt-points-exchange-open><?php esc_html_e('购买积分', 'aksrt'); ?></button>
<button type="button" class="but c-green hollow" data-aksrt-points-pay-open><?php esc_html_e('购买积分', 'aksrt'); ?></button>
<?php endif; ?>
</div>
</div>
@@ -56,25 +56,106 @@ $currency = (string) ($currency_name ?? __('积分', 'aksrt'));
</div>
<?php if ($points_exchange_on) :
// 购买积分弹窗 — Zibll zibpay_get_points_pay_modal 1:1(商品盒 / 自定义 / 支付方式含卡密 / 立即支付)。
$pb_rate = max(1, (int) ($points_exchange['rate'] ?? 100));
$pb_min_points = max(1, (int) ($points_exchange['min_points'] ?? 100));
$pb_daily_max_points = max(1, (int) ($points_exchange['daily_max_points'] ?? 100000));
$points_packages = class_exists('\AKSRT\Credits\BalanceService')
? \AKSRT\Credits\BalanceService::get_instance()->parse_points_packages((string) ($points_exchange['products_raw'] ?? ''))
: [];
// 卡密兑换积分渠道(后台 启用卡密兑换积分功能)。
$points_card_pass = ['enabled' => false, 'desc' => '', 'only_password' => false];
if (class_exists('\AKSRT\Payment\ChargeCard')) {
$points_card_opts = \AKSRT\Payment\ChargeCard::get_instance()->get_public_options();
$points_card_pass = is_array($points_card_opts['credits'] ?? null) ? $points_card_opts['credits'] : $points_card_pass;
}
$points_methods = [['id' => 'balance', 'name' => __('余额', 'aksrt')]];
if (!empty($points_card_pass['enabled'])) {
$points_methods[] = ['id' => 'card_pass', 'name' => __('卡密', 'aksrt')];
}
$points_custom_limit_html = sprintf(__('最低购买%d积分,最高购买%d积分', 'aksrt'), $pb_min_points, $pb_daily_max_points);
$points_gateway = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_payment_gateway_settings() : [];
$points_mark = (string) ($points_gateway['currency']['mark'] ?? '¥');
$points_default_product = $points_packages ? (string) $points_packages[0]['key'] : 'custom';
?>
<div class="uc-modal" id="aksrt-points-exchange-modal" hidden data-aksrt-points-exchange-modal>
<div class="uc-modal__overlay" data-aksrt-points-exchange-close></div>
<div class="uc-modal__dialog" role="dialog" aria-modal="true">
<button type="button" class="uc-modal__close" data-aksrt-points-exchange-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button>
<h3 class="uc-modal__title"><i class="fa fas fa-money-bill mr6" aria-hidden="true"></i><?php esc_html_e('购买积分', 'aksrt'); ?></h3>
<?php if (!empty($points_exchange['desc'])) : ?>
<div class="uc-modal__desc muted-box em09"><?php echo esc_html((string) $points_exchange['desc']); ?></div>
<?php endif; ?>
<form data-aksrt-points-exchange-form>
<label class="muted-color em09"><?php esc_html_e('兑换余额', 'aksrt'); ?></label>
<input type="number" class="uc-card-pass__input" name="amount" min="0.01" step="0.01" inputmode="decimal" data-aksrt-points-exchange-amount data-aksrt-points-exchange-rate="<?php echo esc_attr((string) $pb_rate); ?>">
<p class="muted-2-color em09 mt6"><?php printf(esc_html__('1 余额 = %d 积分;单笔至少 %d 积分,每日最多 %d 积分。', 'aksrt'), $pb_rate, $pb_min_points, $pb_daily_max_points); ?></p>
<p class="muted-2-color em09 mt6" data-aksrt-points-exchange-preview></p>
<button type="submit" class="but jb-blue btn-block mt10"><?php esc_html_e('立即购买', 'aksrt'); ?></button>
</form>
<div class="modal flex jc fade" id="aksrt-points-pay-modal" tabindex="-1" role="dialog" data-aksrt-points-pay-modal>
<div class="modal-dialog modal-mini full-sm" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="mb10 touch"><button type="button" class="close" data-dismiss="modal" aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button><b class="modal-title flex ac"><span class="mr6 em14"><i class="fa fas fa-coins" aria-hidden="true"></i></span><?php esc_html_e('购买积分', 'aksrt'); ?></b></div>
<form class="balance-charge-form mini-scrollbar scroll-y max-vh7" data-aksrt-points-pay-form>
<div class="charge-box mb20">
<?php if ($points_packages) : ?>
<div class="muted-color mb6"><?php esc_html_e('请选择需购买的积分', 'aksrt'); ?></div>
<?php foreach ($points_packages as $pidx => $ppkg) : ?>
<div class="zib-widget vip-product relative product-box<?php echo $pidx === 0 ? ' active' : ''; ?>" data-for="points_product" data-value="<?php echo esc_attr((string) $ppkg['key']); ?>">
<?php if (!empty($ppkg['tag'])) : ?>
<div class="abs-right vip-tag badg jb-yellow"><?php echo esc_html((string) $ppkg['tag']); ?></div>
<?php endif; ?>
<div class="em14"><span class="px12"><i class="fa fas fa-coins" aria-hidden="true"></i></span><?php echo esc_html(aksrt_format_points((int) $ppkg['points'])); ?></div>
<div class="c-red"><span class="px12"><?php echo esc_html($points_mark); ?></span><span class="em12"><?php echo esc_html((string) $ppkg['pay_price']); ?></span></div>
</div>
<?php endforeach; ?>
<div class="muted-color mt20 mb6"><?php printf(esc_html__('自定义积分数量(1%1$s=%2$s积分)', 'aksrt'), esc_html($points_mark), esc_html((string) $pb_rate)); ?></div>
<div data-for="points_product" data-value="custom">
<div class="relative flex ab">
<span class="ml6 mr10 muted-color shrink0"><i class="fa fas fa-coins" aria-hidden="true"></i></span>
<input class="line-form-input em16 key-color" style="padding: 1px;" name="points_custom" type="number" limit-min="<?php echo esc_attr((string) $pb_min_points); ?>" limit-max="<?php echo esc_attr((string) $pb_daily_max_points); ?>" placeholder="<?php echo esc_attr($pb_min_points . ' ~ ' . $pb_daily_max_points); ?>">
<i class="line-form-line"></i>
</div>
<div class="muted-2-color em09 mt6"><?php echo esc_html($points_custom_limit_html); ?></div>
</div>
<?php else : ?>
<div class="muted-color mb6"><?php esc_html_e('请输入需购买的积分数额', 'aksrt'); ?></div>
<div data-for="points_product" data-value="custom">
<div class="relative flex ab">
<span class="ml6 mr10 muted-color shrink0"><i class="fa fas fa-coins" aria-hidden="true"></i></span>
<input class="line-form-input em16 key-color" style="padding: 1px;" name="points_custom" type="number" limit-min="<?php echo esc_attr((string) $pb_min_points); ?>" limit-max="<?php echo esc_attr((string) $pb_daily_max_points); ?>" placeholder="<?php echo esc_attr($pb_min_points . ' ~ ' . $pb_daily_max_points); ?>">
<i class="line-form-line"></i>
</div>
<div class="muted-2-color em09 mt6"><?php echo esc_html($points_custom_limit_html); ?></div>
</div>
<?php endif; ?>
</div>
<?php if (!empty($points_exchange['desc'])) : ?>
<div class="muted-box muted-2-color padding-10 mb20 em09"><?php echo esc_html((string) $points_exchange['desc']); ?></div>
<?php endif; ?>
<div class="dependency-box">
<?php if (count($points_methods) > 1) : ?>
<div class="muted-2-color em09 mb6"><?php esc_html_e('请选择支付方式', 'aksrt'); ?></div>
<div class="flex mb10">
<?php foreach ($points_methods as $pmi => $pm) :
$pm_id = (string) ($pm['id'] ?? '');
$pm_logo = 'card_pass' === $pm_id ? 'pay-card-pass-logo.svg' : 'pay-balance-logo.svg';
?>
<div class="flex jc hh payment-method-radio hollow-radio flex-auto pointer<?php echo $pmi === 0 ? ' active' : ''; ?>" data-for="payment_method" data-value="<?php echo esc_attr($pm_id); ?>">
<img src="<?php echo esc_url(get_template_directory_uri() . '/assets/img/' . $pm_logo); ?>" alt="<?php echo esc_attr($pm_id); ?>-logo">
<div><?php echo esc_html((string) ($pm['name'] ?? $pm_id)); ?></div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($points_card_pass['enabled'])) : ?>
<div class="mb10 padding-h10 padding-w6" data-controller="payment_method" data-condition="==" data-value="card_pass" style="display: none;">
<?php if (!empty($points_card_pass['desc'])) : ?>
<div class="muted-box muted-2-color padding-10 mb10 em09"><?php echo esc_html((string) $points_card_pass['desc']); ?></div>
<?php endif; ?>
<div class="muted-2-color em09 mb6"><?php echo !empty($points_card_pass['only_password']) ? esc_html__('请输入卡密', 'aksrt') : esc_html__('请输入卡号和密码', 'aksrt'); ?></div>
<?php if (empty($points_card_pass['only_password'])) : ?>
<div class="mb6"><input type="input" class="form-control" name="card_pass[card]" placeholder="<?php esc_attr_e('卡号', 'aksrt'); ?>" value=""></div>
<?php endif; ?>
<div class="">
<input type="input" class="form-control" name="card_pass[password]" placeholder="<?php esc_attr_e('卡密', 'aksrt'); ?>" value="">
</div>
</div>
<?php endif; ?>
<input type="hidden" name="points_product" value="<?php echo esc_attr($points_default_product); ?>">
<input type="hidden" name="payment_method" value="<?php echo esc_attr((string) ($points_methods[0]['id'] ?? '')); ?>">
<button type="submit" class="mt6 but jb-red initiate-pay btn-block radius"><?php esc_html_e('立即支付', 'aksrt'); ?></button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php endif; ?>
@@ -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_redeem = ['enabled' => false, 'desc' => ''];
// 卡密支付渠道(后台 余额充值 → 卡密兑换 → 启用卡密兑换余额功能):
// 兑换入口收敛到充值弹窗的支付方式,不再有独立入口。
$card_pass_redeem = ['enabled' => false, 'desc' => '', 'only_password' => false];
if (class_exists('\AKSRT\Payment\ChargeCard')) {
$opts = \AKSRT\Payment\ChargeCard::get_instance()->get_public_options();
$card_pass_redeem = is_array($opts['balance'] ?? null) ? $opts['balance'] : $card_pass_redeem;
$card_opts = \AKSRT\Payment\ChargeCard::get_instance()->get_public_options();
$card_pass_redeem = is_array($card_opts['balance'] ?? null) ? $card_opts['balance'] : $card_pass_redeem;
}
$charge_view = [];
@@ -92,43 +92,14 @@ $mark = (string) ($charge_view['currency_mark'] ?? '元');
<button type="button" class="but b-blue-2" data-aksrt-charge-open><?php esc_html_e('充值', 'aksrt'); ?></button>
</div>
</div>
<?php if ($withdraw_on || !empty($card_pass_redeem['enabled'])) : ?>
<?php if ($withdraw_on) : ?>
<div class="uc-asset-card__foot muted-2-color em09 mt6 flex jsb ac">
<?php if ($withdraw_on) : ?>
<span><?php printf(esc_html__('冻结中 %s', 'aksrt'), esc_html($normalize_balance($withdraw_pending_hold)->formatted())); ?></span>
<?php else : ?>
<span></span>
<?php endif; ?>
<?php if (!empty($card_pass_redeem['enabled'])) : ?>
<button type="button" class="uc-asset-card__head-action muted-color px12" data-aksrt-card-pass-open>
<?php esc_html_e('卡密兑换', 'aksrt'); ?>
<i class="fa fas fa-angle-right em12 ml3" aria-hidden="true"></i>
</button>
<?php endif; ?>
<span><?php printf(esc_html__('冻结中 %s', 'aksrt'), esc_html($normalize_balance($withdraw_pending_hold)->formatted())); ?></span>
<span></span>
</div>
<?php endif; ?>
</div>
<?php if (!empty($card_pass_redeem['enabled'])) : ?>
<div class="uc-modal" id="aksrt-card-pass-modal" hidden data-aksrt-card-pass-modal data-aksrt-card-pass="">
<div class="uc-modal__overlay" data-aksrt-card-pass-close></div>
<div class="uc-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="aksrt-card-pass-title">
<button type="button" class="uc-modal__close" data-aksrt-card-pass-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fa fas fa-times" aria-hidden="true"></i></button>
<h3 id="aksrt-card-pass-title" class="uc-modal__title"><i class="fa fas fa-ticket-alt mr6" aria-hidden="true"></i><?php esc_html_e('卡密兑换', 'aksrt'); ?></h3>
<?php if (!empty($card_pass_redeem['desc'])) : ?>
<div class="uc-modal__desc muted-box em09"><?php echo esc_html((string) $card_pass_redeem['desc']); ?></div>
<?php endif; ?>
<input type="text" class="uc-card-pass__input" name="aksrt_card_no" data-aksrt-card-no autocomplete="off" placeholder="<?php esc_attr_e('请输入卡号(选填)', 'aksrt'); ?>">
<input type="text" class="uc-card-pass__input" name="aksrt_card_password" data-aksrt-card-password autocomplete="off" placeholder="<?php esc_attr_e('请输入卡密', 'aksrt'); ?>">
<div class="uc-card-pass-actions">
<button type="button" class="uc-card-pass__btn" data-aksrt-card-redeem>
<?php esc_html_e('立即兑换', 'aksrt'); ?>
</button>
</div>
</div>
</div>
<?php endif; ?>
<!-- Charge modal — Zibll balance-charge modal 1:1 (refresh_modal shell + zibpay_get_balance_charge_modal) -->
<?php
$charge_method_logos = [
@@ -139,9 +110,13 @@ $charge_method_logos = [
'epay_wxpay' => 'pay-wechat-logo.svg',
'paypal' => 'pay-paypal-logo.svg',
'stripe' => 'pay-stripe-logo.svg',
'card_pass' => 'pay-card-pass-logo.svg',
];
$charge_img_uri = get_template_directory_uri() . '/assets/img/';
$charge_default_product = $packages ? (string) $packages[0]['key'] : 'custom';
// 仅卡密可用时隐藏金额选择(参考主题:卡密决定充值金额)。
$charge_card_only = count($methods) === 1 && (string) ($methods[0]['id'] ?? '') === 'card_pass';
$charge_card_in_methods = count($methods) > 0 && $methods[count($methods) - 1]['id'] === 'card_pass';
$charge_mark_symbol = !in_array($mark, ['', '元'], true);
$charge_mark_html = $charge_mark_symbol
? '<span class="px12">' . esc_html($mark) . '</span>'
@@ -170,32 +145,34 @@ $charge_custom_block = static function () use ($mark, $charge_mark_html, $charge
<div class="modal-body">
<div class="mb10 touch"><button type="button" class="close" data-dismiss="modal" aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button><b class="modal-title flex ac"><span class="mr6 em14"><svg class="icon" aria-hidden="true"><use xlink:href="#icon-money-color-2"></use></svg></span><?php esc_html_e('余额充值', 'aksrt'); ?></b></div>
<form class="balance-charge-form mini-scrollbar scroll-y max-vh7" data-aksrt-charge-form>
<?php if ($packages) : ?>
<div class="charge-box mb20">
<div class="muted-color mb6"><?php esc_html_e('请选择充值金额', 'aksrt'); ?></div>
<?php foreach ($packages as $idx => $pkg) :
$package_credit = $normalize_balance($pkg['price'] ?? '');
$package_pay = $normalize_balance($pkg['pay_price'] ?? '');
$package_saving_minor = max(0, $package_credit->minor() - $package_pay->minor());
?>
<div class="zib-widget vip-product relative product-box<?php echo $idx === 0 ? ' active' : ''; ?>" data-for="balance_product" data-value="<?php echo esc_attr((string) $pkg['key']); ?>">
<?php if (!empty($pkg['tag'])) : ?>
<div class="abs-right vip-tag badg jb-yellow"><?php echo esc_html((string) $pkg['tag']); ?></div>
<?php endif; ?>
<div class="em14"><?php echo $charge_mark_html; // phpcs:ignore WordPress.Security.EscapeOutput -- escaped span + esc_html mark above ?><?php echo esc_html((string) $pkg['price']); ?></div>
<div class="c-red"><?php echo $charge_mark_html; // phpcs:ignore WordPress.Security.EscapeOutput ?><span><?php echo esc_html((string) $pkg['pay_price']); ?></span><?php if ($package_saving_minor > 0) : ?><span class="ml6 c-yellow smail"><?php printf(esc_html__('省%s', 'aksrt'), esc_html(\AKSRT\Ledger\AssetAmount::decimal_from_minor($package_saving_minor) . $charge_unit)); ?></span><?php endif; ?></div>
</div>
<?php endforeach; ?>
<?php if ($custom_s) : ?>
<div class="muted-color mt20 mb6"><?php esc_html_e('自定义充值金额', 'aksrt'); ?></div>
<?php if (!$charge_card_only) : ?>
<?php if ($packages) : ?>
<div class="charge-box mb20">
<div class="muted-color mb6"><?php esc_html_e('请选择充值金额', 'aksrt'); ?></div>
<?php foreach ($packages as $idx => $pkg) :
$package_credit = $normalize_balance($pkg['price'] ?? '');
$package_pay = $normalize_balance($pkg['pay_price'] ?? '');
$package_saving_minor = max(0, $package_credit->minor() - $package_pay->minor());
?>
<div class="zib-widget vip-product relative product-box<?php echo $idx === 0 ? ' active' : ''; ?>" data-for="balance_product" data-value="<?php echo esc_attr((string) $pkg['key']); ?>">
<?php if (!empty($pkg['tag'])) : ?>
<div class="abs-right vip-tag badg jb-yellow"><?php echo esc_html((string) $pkg['tag']); ?></div>
<?php endif; ?>
<div class="em14"><?php echo $charge_mark_html; // phpcs:ignore WordPress.Security.EscapeOutput -- escaped span + esc_html mark above ?><?php echo esc_html((string) $pkg['price']); ?></div>
<div class="c-red"><?php echo $charge_mark_html; // phpcs:ignore WordPress.Security.EscapeOutput ?><span><?php echo esc_html((string) $pkg['pay_price']); ?></span><?php if ($package_saving_minor > 0) : ?><span class="ml6 c-yellow smail"><?php printf(esc_html__('省%s', 'aksrt'), esc_html(\AKSRT\Ledger\AssetAmount::decimal_from_minor($package_saving_minor) . $charge_unit)); ?></span><?php endif; ?></div>
</div>
<?php endforeach; ?>
<?php if ($custom_s) : ?>
<div class="muted-color mt20 mb6"><?php esc_html_e('自定义充值金额', 'aksrt'); ?></div>
<?php $charge_custom_block(); ?>
<?php endif; ?>
</div>
<?php elseif ($custom_s) : ?>
<div class="charge-box mb20">
<div class="muted-color mb6"><?php esc_html_e('请输入充值金额', 'aksrt'); ?></div>
<?php $charge_custom_block(); ?>
<?php endif; ?>
</div>
<?php elseif ($custom_s) : ?>
<div class="charge-box mb20">
<div class="muted-color mb6"><?php esc_html_e('请输入充值金额', 'aksrt'); ?></div>
<?php $charge_custom_block(); ?>
</div>
</div>
<?php endif; ?>
<?php endif; ?>
<?php if (!empty($charge_view['desc'])) : ?>
<div class="muted-box muted-2-color padding-10 mb10 em09"><?php echo esc_html((string) $charge_view['desc']); ?></div>
@@ -217,6 +194,20 @@ $charge_custom_block = static function () use ($mark, $charge_mark_html, $charge
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($charge_card_in_methods) : ?>
<div class="mb10 padding-h10 padding-w6" data-controller="payment_method" data-condition="==" data-value="card_pass"<?php echo !$charge_card_only ? ' style="display: none;"' : ''; ?>>
<?php if (!empty($card_pass_redeem['desc'])) : ?>
<div class="muted-box muted-2-color padding-10 mb10 em09"><?php echo esc_html((string) $card_pass_redeem['desc']); ?></div>
<?php endif; ?>
<div class="muted-2-color em09 mb6"><?php echo !empty($card_pass_redeem['only_password']) ? esc_html__('请输入卡密', 'aksrt') : esc_html__('请输入卡号和密码', 'aksrt'); ?></div>
<?php if (empty($card_pass_redeem['only_password'])) : ?>
<div class="mb6"><input type="input" class="form-control" name="card_pass[card]" placeholder="<?php esc_attr_e('卡号', 'aksrt'); ?>" value=""></div>
<?php endif; ?>
<div class="">
<input type="input" class="form-control" name="card_pass[password]" placeholder="<?php esc_attr_e('卡密', 'aksrt'); ?>" value="">
</div>
</div>
<?php endif; ?>
<input type="hidden" name="balance_product" value="<?php echo esc_attr($charge_default_product); ?>">
<input type="hidden" name="payment_method" value="<?php echo esc_attr((string) ($methods[0]['id'] ?? '')); ?>">
<?php if ($methods) : ?>