feat(payment): 通用卡密兑换弹窗并修正核销订单实付与订单名称
- /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 根类回潮)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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: '余额卡密仅需输入密码,不需要卡号' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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: '开启后积分卡密可在用户中心「卡密兑换」弹窗兑换入账。' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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%;
|
||||
|
||||
@@ -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) || '兑换成功,正在刷新…');
|
||||
|
||||
@@ -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'));
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($card_pass_credits['enabled'])) : ?>
|
||||
<div class="uc-card-pass" data-aksrt-card-pass="credits">
|
||||
<div class="uc-card-pass__title">
|
||||
<i class="fa fas fa-ticket-alt" aria-hidden="true"></i>
|
||||
<span><?php esc_html_e('卡密兑换积分', 'aksrt'); ?></span>
|
||||
</div>
|
||||
<?php if (!empty($card_pass_credits['desc'])) : ?>
|
||||
<p class="uc-card-pass__desc"><?php echo esc_html((string) $card_pass_credits['desc']); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if (empty($card_pass_credits['only_password'])) : ?>
|
||||
<input type="text" class="uc-card-pass__input" name="aksrt_card_no" data-aksrt-card-no autocomplete="off" placeholder="<?php esc_attr_e('请输入卡号', 'aksrt'); ?>">
|
||||
<?php endif; ?>
|
||||
<input type="text" class="uc-card-pass__input" name="aksrt_card_password" data-aksrt-card-password autocomplete="off" placeholder="<?php esc_attr_e('请输入卡密', 'aksrt'); ?>">
|
||||
<button type="button" class="uc-card-pass__btn" data-aksrt-card-redeem data-type="credits">
|
||||
<?php esc_html_e('兑换积分', 'aksrt'); ?>
|
||||
</button>
|
||||
<p class="uc-card-pass__tip" data-aksrt-card-tip></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -1,269 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* User center credits: VIP membership card + purchase packages + points exchange.
|
||||
*
|
||||
* Expects extracted $context vars.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
$vip_settings = is_array($vip_settings ?? null) ? $vip_settings : [];
|
||||
if (empty($vip_settings['enabled'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = (int) ($user_id ?? get_current_user_id());
|
||||
$vip_level = 0;
|
||||
$expires = 0;
|
||||
$expires_label = __('未开通', 'aksrt');
|
||||
$is_vip_user = false;
|
||||
$level_name = (string) ($vip_settings['name'] ?? __('VIP会员', 'aksrt'));
|
||||
|
||||
$packages = [];
|
||||
$renew_desc = '';
|
||||
$upgrade_desc = '';
|
||||
$points_exchange = ['enabled' => 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 !== ''
|
||||
? '<img' . aksrt_get_lazy_attr('lazy_other', $level_icon_url, 'aksrt-vip-level-icon') . ' alt="" decoding="async">'
|
||||
: '<i class="fa fas fa-certificate" aria-hidden="true"></i>';
|
||||
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']),
|
||||
];
|
||||
}
|
||||
?>
|
||||
<div class="uc-vip-card<?php echo $is_vip_user ? ' uc-vip-card--active' : ''; ?>" data-aksrt-vip-panel>
|
||||
<div class="uc-vip-card__bg"></div>
|
||||
<div class="uc-vip-card__content">
|
||||
<div class="uc-vip-card__header">
|
||||
<div class="uc-vip-card__icon">
|
||||
<?php echo $level_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
</div>
|
||||
<div class="uc-vip-card__info">
|
||||
<span class="uc-vip-card__label"><?php echo esc_html($level_name); ?></span>
|
||||
<?php if ($is_vip_user) : ?>
|
||||
<span class="uc-vip-card__status uc-vip-card__status--active"><?php echo esc_html($expires_label); ?></span>
|
||||
<?php else : ?>
|
||||
<span class="uc-vip-card__status"><?php esc_html_e('未开通', 'aksrt'); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uc-vip-card__benefits">
|
||||
<span class="uc-vip-card__benefit"><i class="fa fas fa-download" aria-hidden="true"></i> <?php esc_html_e('免费下载', 'aksrt'); ?></span>
|
||||
<span class="uc-vip-card__benefit"><i class="fa fas fa-certificate" aria-hidden="true"></i> <?php esc_html_e('专属身份', 'aksrt'); ?></span>
|
||||
<span class="uc-vip-card__benefit"><i class="fa fas fa-tag" aria-hidden="true"></i> <?php esc_html_e('会员价优惠', 'aksrt'); ?></span>
|
||||
</div>
|
||||
<?php if (!empty($vip_settings['desc'])) : ?>
|
||||
<p class="uc-vip-card__desc"><?php echo esc_html((string) $vip_settings['desc']); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($packages)) : ?>
|
||||
<div class="uc-vip-shop" data-aksrt-vip-shop>
|
||||
<div class="uc-vip-shop__title">
|
||||
<i class="fa fas fa-gem" aria-hidden="true"></i>
|
||||
<span><?php echo esc_html($shop_title); ?></span>
|
||||
</div>
|
||||
|
||||
<?php if ($vip_level > 0 && $renew_desc !== '') : ?>
|
||||
<p class="uc-vip-shop__note"><?php echo esc_html($renew_desc); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if ($vip_level === 1 && $upgrade_desc !== '') : ?>
|
||||
<p class="uc-vip-shop__note"><?php echo esc_html($upgrade_desc); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($packages as $group) :
|
||||
$g_level = (int) ($group['level'] ?? 0);
|
||||
$g_name = (string) ($group['name'] ?? ('VIP' . $g_level));
|
||||
$g_action = (string) ($group['action'] ?? 'pay');
|
||||
$items = is_array($group['items'] ?? null) ? $group['items'] : [];
|
||||
if ($items === []) {
|
||||
continue;
|
||||
}
|
||||
$group_badge = '';
|
||||
if ($g_action === 'renew') {
|
||||
$group_badge = __('续费', 'aksrt');
|
||||
} elseif ($g_action === 'upgrade') {
|
||||
$group_badge = __('升级', 'aksrt');
|
||||
} elseif ($vip_level >= $g_level) {
|
||||
$group_badge = __('当前档', 'aksrt');
|
||||
}
|
||||
?>
|
||||
<div class="uc-vip-shop__group" data-vip-level="<?php echo esc_attr((string) $g_level); ?>" data-action="<?php echo esc_attr($g_action); ?>">
|
||||
<div class="uc-vip-shop__group-title">
|
||||
<?php echo esc_html($g_name); ?>
|
||||
<?php if ($group_badge !== '') : ?>
|
||||
<span class="uc-vip-shop__badge"><?php echo esc_html($group_badge); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="uc-vip-shop__grid">
|
||||
<?php foreach ($items as $item) :
|
||||
$pid = (int) ($item['package_id'] ?? 0);
|
||||
$price_label = (string) ($item['price_label'] ?? ($cash_symbol . ($item['price'] ?? 0)));
|
||||
$show_price_label = (string) ($item['show_price_label'] ?? '');
|
||||
$discount_tag = (string) ($item['discount_tag'] ?? '');
|
||||
$days_label = (string) ($item['days_label'] ?? '');
|
||||
$title = (string) ($item['title'] ?? '');
|
||||
$cta = (string) ($item['cta_label'] ?? __('立即开通', 'aksrt'));
|
||||
?>
|
||||
<button
|
||||
type="button"
|
||||
class="uc-vip-shop__item"
|
||||
data-aksrt-vip-buy
|
||||
data-package-id="<?php echo esc_attr((string) $pid); ?>"
|
||||
data-price-label="<?php echo esc_attr($price_label); ?>"
|
||||
data-title="<?php echo esc_attr($title); ?>"
|
||||
data-action="<?php echo esc_attr((string) ($item['action'] ?? 'pay')); ?>"
|
||||
>
|
||||
<?php if ($discount_tag !== '') : ?>
|
||||
<span class="uc-vip-shop__item-tag"><?php echo esc_html($discount_tag); ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="uc-vip-shop__item-title"><?php echo esc_html($title); ?></span>
|
||||
<span class="uc-vip-shop__item-days"><?php echo esc_html($days_label); ?></span>
|
||||
<span class="uc-vip-shop__item-price">
|
||||
<?php echo esc_html($price_label); ?>
|
||||
<?php if ($show_price_label !== '') : ?>
|
||||
<small class="uc-vip-shop__item-show"><?php echo esc_html($show_price_label); ?></small>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<span class="uc-vip-shop__item-cta"><?php echo esc_html($cta); ?></span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if (count($methods) > 1) : ?>
|
||||
<div class="uc-vip-shop__methods" data-aksrt-vip-methods>
|
||||
<span class="uc-vip-shop__methods-label"><?php esc_html_e('支付方式', 'aksrt'); ?></span>
|
||||
<?php foreach ($methods as $i => $m) :
|
||||
$mid = (string) ($m['id'] ?? '');
|
||||
$mname = (string) ($m['name'] ?? $mid);
|
||||
?>
|
||||
<label class="uc-vip-shop__method">
|
||||
<input type="radio" name="aksrt_vip_pay_method" value="<?php echo esc_attr($mid); ?>" <?php checked($i === 0); ?>>
|
||||
<span><?php echo esc_html($mname); ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php elseif (!empty($methods[0]['id'])) : ?>
|
||||
<input type="hidden" name="aksrt_vip_pay_method" value="<?php echo esc_attr((string) $methods[0]['id']); ?>" data-aksrt-vip-method-fixed>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="uc-vip-shop__tip" data-aksrt-vip-tip></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($points_exchange['enabled']) && $points_pkgs !== []) : ?>
|
||||
<div class="uc-vip-shop uc-vip-shop--points" data-aksrt-vip-points>
|
||||
<div class="uc-vip-shop__title">
|
||||
<i class="fa fas fa-money-bill" aria-hidden="true"></i>
|
||||
<span><?php esc_html_e('积分兑换会员', 'aksrt'); ?></span>
|
||||
</div>
|
||||
<div class="uc-vip-shop__balance">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: 1: points balance 2: currency name */
|
||||
esc_html__('我的积分:%1$s %2$s', 'aksrt'),
|
||||
esc_html((string) $user_points),
|
||||
esc_html($currency_name)
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
<?php if (!empty($points_exchange['desc'])) : ?>
|
||||
<p class="uc-vip-shop__note"><?php echo esc_html((string) $points_exchange['desc']); ?></p>
|
||||
<?php endif; ?>
|
||||
<div class="uc-vip-shop__grid">
|
||||
<?php foreach ($points_pkgs as $item) :
|
||||
$pid = (int) ($item['package_id'] ?? 0);
|
||||
$cost = (int) ($item['points'] ?? 0);
|
||||
$enough = $user_points >= $cost;
|
||||
?>
|
||||
<button
|
||||
type="button"
|
||||
class="uc-vip-shop__item<?php echo $enough ? '' : ' is-disabled'; ?>"
|
||||
data-aksrt-vip-points-buy
|
||||
data-package-id="<?php echo esc_attr((string) $pid); ?>"
|
||||
data-points="<?php echo esc_attr((string) $cost); ?>"
|
||||
<?php disabled(!$enough); ?>
|
||||
>
|
||||
<span class="uc-vip-shop__item-title"><?php echo esc_html((string) ($item['title'] ?? '')); ?></span>
|
||||
<span class="uc-vip-shop__item-days"><?php echo esc_html((string) ($item['days_label'] ?? '')); ?></span>
|
||||
<span class="uc-vip-shop__item-price"><?php echo esc_html((string) ($item['price_label'] ?? '')); ?></span>
|
||||
<span class="uc-vip-shop__item-cta">
|
||||
<?php echo $enough ? esc_html__('立即兑换', 'aksrt') : esc_html__('积分不足', 'aksrt'); ?>
|
||||
</span>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<p class="uc-vip-shop__tip" data-aksrt-vip-points-tip></p>
|
||||
</div>
|
||||
<?php elseif (empty($packages) && !empty($vip_settings['help_text'])) : ?>
|
||||
<p class="uc-vip-shop__empty"><?php echo esc_html((string) $vip_settings['help_text']); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($card_pass_vip['enabled'])) : ?>
|
||||
<div class="uc-card-pass" data-aksrt-card-pass="vip">
|
||||
<div class="uc-card-pass__title">
|
||||
<i class="fa fas fa-ticket-alt" aria-hidden="true"></i>
|
||||
<span><?php esc_html_e('卡密兑换会员', 'aksrt'); ?></span>
|
||||
</div>
|
||||
<?php if (!empty($card_pass_vip['desc'])) : ?>
|
||||
<p class="uc-card-pass__desc"><?php echo esc_html((string) $card_pass_vip['desc']); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if (empty($card_pass_vip['only_password'])) : ?>
|
||||
<input type="text" class="uc-card-pass__input" name="aksrt_card_no" data-aksrt-card-no autocomplete="off" placeholder="<?php esc_attr_e('请输入卡号', 'aksrt'); ?>">
|
||||
<?php endif; ?>
|
||||
<input type="text" class="uc-card-pass__input" name="aksrt_card_password" data-aksrt-card-password autocomplete="off" placeholder="<?php esc_attr_e('请输入卡密', 'aksrt'); ?>">
|
||||
<button type="button" class="uc-card-pass__btn" data-aksrt-card-redeem data-type="vip">
|
||||
<?php esc_html_e('兑换会员', 'aksrt'); ?>
|
||||
</button>
|
||||
<p class="uc-card-pass__tip" data-aksrt-card-tip></p>
|
||||
</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_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'] ?? '元');
|
||||
<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_balance['enabled'])) : ?>
|
||||
<?php if ($withdraw_on || !empty($card_pass_redeem['enabled'])) : ?>
|
||||
<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_balance['enabled'])) : ?>
|
||||
<?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>
|
||||
@@ -109,22 +109,20 @@ $mark = (string) ($charge_view['currency_mark'] ?? '元');
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($card_pass_balance['enabled'])) : ?>
|
||||
<div class="uc-modal" id="aksrt-card-pass-modal" hidden data-aksrt-card-pass-modal data-aksrt-card-pass="balance">
|
||||
<?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_balance['desc'])) : ?>
|
||||
<div class="uc-modal__desc muted-box em09"><?php echo esc_html((string) $card_pass_balance['desc']); ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if (empty($card_pass_balance['only_password'])) : ?>
|
||||
<input type="text" class="uc-card-pass__input" name="aksrt_card_no" data-aksrt-card-no autocomplete="off" placeholder="<?php esc_attr_e('请输入卡号', 'aksrt'); ?>">
|
||||
<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 data-type="balance">
|
||||
<?php esc_html_e('兑换余额', 'aksrt'); ?>
|
||||
<button type="button" class="uc-card-pass__btn" data-aksrt-card-redeem>
|
||||
<?php esc_html_e('立即兑换', 'aksrt'); ?>
|
||||
</button>
|
||||
</div>
|
||||
<p class="uc-card-pass__tip" data-aksrt-card-tip></p>
|
||||
|
||||
@@ -154,10 +154,10 @@ class SchemaPayment {
|
||||
['key' => 'pay_balance_product_custom_s', 'label' => '自定义充值金额', 'type' => 'switch', 'default' => true, 'help' => '允许用户手动输入充值金额'],
|
||||
['key' => 'pay_balance_product_custom_limit', 'label' => '自定义充值金额上限', 'type' => 'number', 'default' => 9999, 'min' => 1, 'max' => 99999, 'dependsOn' => ['key' => 'pay_balance_product_custom_s', 'value' => true]],
|
||||
]],
|
||||
['key' => 'pass-charge', 'title' => '卡密充值', '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' => 'pass-charge', 'title' => '卡密兑换', 'fields' => [
|
||||
['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' => '余额卡密仅需输入密码,不需要卡号'],
|
||||
]],
|
||||
['key' => 'points-exchange', 'title' => '余额兑换积分', 'fields' => [
|
||||
['key' => 'pay_balance_points_exchange_s', 'label' => '启用余额兑换积分', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]],
|
||||
|
||||
@@ -487,9 +487,7 @@ class SchemaUsers {
|
||||
['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],
|
||||
['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]],
|
||||
['key' => 'asset_points_card_enabled', 'label' => '启用积分卡密兑换', 'type' => 'switch', 'default' => false, 'help' => '开启后积分卡密可在用户中心「卡密兑换」弹窗兑换入账'],
|
||||
]],
|
||||
['key' => 'display', 'title' => '展示设置', 'fields' => [
|
||||
['key' => 'points_show_balance', 'label' => '导航栏显示余额', 'type' => 'switch', 'default' => true],
|
||||
|
||||
+13
-38
@@ -42,11 +42,6 @@ class ChargeCard {
|
||||
}
|
||||
|
||||
public function register_public_routes(): void {
|
||||
register_rest_route('aksrt/v1', '/pay/card-pass/options', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_options'],
|
||||
'permission_callback' => [$this, 'check_rest_nonce'],
|
||||
]);
|
||||
register_rest_route('aksrt/v1', '/pay/card-pass/redeem', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_redeem'],
|
||||
@@ -89,13 +84,6 @@ class ChargeCard {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rest_options(\WP_REST_Request $request) {
|
||||
return new \WP_REST_Response([
|
||||
'success' => true,
|
||||
'data' => $this->get_public_options(),
|
||||
], 200);
|
||||
}
|
||||
|
||||
public function rest_redeem(\WP_REST_Request $request) {
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
@@ -124,20 +112,14 @@ class ChargeCard {
|
||||
}
|
||||
|
||||
/**
|
||||
* 前台卡密兑换开关视图(按类型拆分)。
|
||||
*
|
||||
* balance.enabled 语义为「启用卡密兑换功能」总开关(后台 余额充值 → 卡密兑换),
|
||||
* 与收款接口 card_pass 主开关共同决定前台兑换入口是否展示。
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function get_public_options(): array {
|
||||
$labels = [
|
||||
'title' => __('卡密兑换', 'aksrt'),
|
||||
'password' => __('卡密', 'aksrt'),
|
||||
'card' => __('卡号', 'aksrt'),
|
||||
'submit_balance' => __('兑换余额', 'aksrt'),
|
||||
'submit_vip' => __('兑换会员', 'aksrt'),
|
||||
'submit_credits' => __('兑换积分', 'aksrt'),
|
||||
'placeholder_pw' => __('请输入卡密', 'aksrt'),
|
||||
'placeholder_card' => __('请输入卡号', 'aksrt'),
|
||||
];
|
||||
|
||||
// Master kill-switch on 收款接口 → 卡密充值.
|
||||
$channel_on = !class_exists('\AKSRT\Settings\PaymentRuntime')
|
||||
|| \AKSRT\Settings\PaymentRuntime::is_gateway_enabled('card_pass');
|
||||
@@ -146,8 +128,7 @@ class ChargeCard {
|
||||
return [
|
||||
'balance' => $off,
|
||||
'vip' => $off,
|
||||
'credits' => $off,
|
||||
'labels' => $labels,
|
||||
'credits' => ['enabled' => false],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -175,17 +156,15 @@ class ChargeCard {
|
||||
}
|
||||
}
|
||||
|
||||
$credits = ['enabled' => false, 'desc' => '', 'only_password' => true];
|
||||
$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'])) {
|
||||
// Require credits system on when available.
|
||||
$credits_on = !class_exists('\AKSRT\Settings\Settings') || !method_exists('\AKSRT\Settings\Settings', 'credits_enabled') || \AKSRT\Settings\Settings::credits_enabled();
|
||||
// 积分卡仅作为后端类型闸门(assert_type_enabled);前台兑换统一走
|
||||
// 通用「卡密兑换」弹窗,无独立入口与说明/单密码配置。
|
||||
$credits = [
|
||||
'enabled' => $credits_on,
|
||||
'desc' => sanitize_textarea_field((string) ($all['asset_points_card_description'] ?? '')),
|
||||
'only_password' => !isset($all['asset_points_card_password_only']) || !empty($all['asset_points_card_password_only']),
|
||||
'enabled' => !class_exists('\AKSRT\Settings\Settings') || !method_exists('\AKSRT\Settings\Settings', 'credits_enabled') || \AKSRT\Settings\Settings::credits_enabled(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -193,7 +172,6 @@ class ChargeCard {
|
||||
'balance' => $balance,
|
||||
'vip' => $vip,
|
||||
'credits' => $credits,
|
||||
'labels' => $labels,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -808,6 +786,8 @@ class ChargeCard {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 卡密核销不发生真实支付(现金在售卡时已确认),订单金额恒为 0;
|
||||
// 卡密面额/时长记录在 note.meta,订单名称由 UserOrders 按 note 生成。
|
||||
$product_type = \AKSRT\Core\DomainModel::ORDER_PRODUCT_RECHARGE;
|
||||
$product_id = $card_id;
|
||||
$amount = '0.00';
|
||||
@@ -815,14 +795,9 @@ class ChargeCard {
|
||||
|
||||
if ($type === self::TYPE_VIP) {
|
||||
$product_type = \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP;
|
||||
$amount = '0.00';
|
||||
} elseif ($type === self::TYPE_BALANCE) {
|
||||
$parsed_amount = \AKSRT\Ledger\AssetAmount::balance(is_string($meta['amount'] ?? null) ? $meta['amount'] : '');
|
||||
$amount = $parsed_amount->decimal();
|
||||
$currency = 'CNY';
|
||||
} elseif ($type === self::TYPE_CREDITS) {
|
||||
$product_type = \AKSRT\Core\DomainModel::ORDER_PRODUCT_POINTS;
|
||||
$amount = (int) ($meta['points'] ?? 0);
|
||||
$amount = '0';
|
||||
$currency = 'points';
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +186,12 @@ class UserOrders {
|
||||
$image = $this->resolve_image($meta, $product_id);
|
||||
$group = $this->status_group($status);
|
||||
$payment_method = sanitize_key((string) ($order['payment_method'] ?? ''));
|
||||
// 卡密核销不发生任何真实支付(现金在售卡时已确认),实付恒为 0,
|
||||
// 视图层统一归零以覆盖历史行(历史数据不做迁移)。
|
||||
$amount = (string) ($order['amount'] ?? '0.00');
|
||||
if ($payment_method === 'card_pass') {
|
||||
$amount = $is_points ? '0' : '0.00';
|
||||
}
|
||||
$can_pay = $group === OrderCenterQuery::STATUS_PENDING;
|
||||
$can_cancel = $group === OrderCenterQuery::STATUS_PENDING;
|
||||
$order_number = (string) ($order['order_number'] ?? '');
|
||||
@@ -194,7 +200,7 @@ class UserOrders {
|
||||
$view = [
|
||||
'id' => (int) ($order['id'] ?? 0),
|
||||
'order_number' => $order_number,
|
||||
'amount' => (string) ($order['amount'] ?? '0.00'),
|
||||
'amount' => $amount,
|
||||
'currency' => $currency,
|
||||
'currency_symbol' => $is_points
|
||||
? (class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt'))
|
||||
@@ -288,6 +294,13 @@ class UserOrders {
|
||||
|
||||
/** @param array<string, mixed> $meta */
|
||||
private function resolve_title(array $meta, string $type, int $product_id, string $fallback): string {
|
||||
// 卡密核销订单:按 note.meta 生成可读名称(余额/积分卡记面额,会员卡记档位与时长)。
|
||||
if (!empty($meta['card_pass'])) {
|
||||
$title = $this->resolve_card_pass_title($meta);
|
||||
if ($title !== '') {
|
||||
return $title;
|
||||
}
|
||||
}
|
||||
foreach (['product_title', 'title', 'package_title'] as $key) {
|
||||
if (!empty($meta[$key]) && is_string($meta[$key])) {
|
||||
return (string) $meta[$key];
|
||||
@@ -315,6 +328,43 @@ class UserOrders {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡密核销订单名称:
|
||||
* - 余额卡:卡密兑换-余额充值{面额}
|
||||
* - 积分卡:卡密兑换-积分充值{点数}
|
||||
* - 会员卡:购入[{会员档位名}]{N}天(永久卡为「永久」)
|
||||
*
|
||||
* @param array<string, mixed> $meta
|
||||
*/
|
||||
private function resolve_card_pass_title(array $meta): string {
|
||||
$card_meta = is_array($meta['meta'] ?? null) ? $meta['meta'] : [];
|
||||
$card_type = sanitize_key((string) ($meta['card_type'] ?? ''));
|
||||
if ($card_type === ChargeCard::TYPE_BALANCE) {
|
||||
$amount = (string) ($card_meta['amount'] ?? '');
|
||||
if (str_contains($amount, '.')) {
|
||||
$amount = rtrim(rtrim($amount, '0'), '.');
|
||||
}
|
||||
return __('卡密兑换-余额充值', 'aksrt') . $amount;
|
||||
}
|
||||
if ($card_type === ChargeCard::TYPE_CREDITS) {
|
||||
return __('卡密兑换-积分充值', 'aksrt') . (string) max(0, (int) ($card_meta['points'] ?? 0));
|
||||
}
|
||||
if ($card_type === ChargeCard::TYPE_VIP) {
|
||||
$level = max(1, min(2, (int) ($card_meta['level'] ?? 1)));
|
||||
$days = max(0, (int) ($card_meta['days'] ?? 0));
|
||||
$levels = class_exists('\AKSRT\Settings\Settings')
|
||||
? (\AKSRT\Settings\Settings::get_payment_vip_settings()['levels'] ?? [])
|
||||
: [];
|
||||
$level_name = is_array($levels) ? (string) ($levels[$level]['name'] ?? '') : '';
|
||||
if ($level_name === '') {
|
||||
$level_name = 'VIP' . $level;
|
||||
}
|
||||
$suffix = $days > 0 ? sprintf(__('%d天', 'aksrt'), $days) : __('永久', 'aksrt');
|
||||
return sprintf(__('购入[%1$s]%2$s', 'aksrt'), $level_name, $suffix);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $meta */
|
||||
private function resolve_image(array $meta, int $product_id): string {
|
||||
if (!empty($meta['thumb']) && is_string($meta['thumb'])) {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* 通用卡密兑换契约(/user/balance/ 余额卡片弹窗)。
|
||||
*
|
||||
* 背景:余额卡片右下角入口原为「卡密兑换余额」专用弹窗(强制 data-type="balance")。
|
||||
* 现改为通用「卡密兑换」弹窗:标题、文案与提交不再绑定余额类型,
|
||||
* 请求不带类型,由后端 ChargeCard::redeem_for_user() 按卡密自动识别
|
||||
* (balance / vip / credits),各类型仍受自身开关与收款接口 card_pass 主开关约束。
|
||||
* 前台只保留这一个兑换入口:页内联兑换块(积分/会员)与死文件 credits-vip.php
|
||||
* 已删除,会员/积分卡密统一经通用弹窗兑换。
|
||||
*
|
||||
* 后台语义:余额充值 → 卡密兑换 → 「启用卡密兑换功能」
|
||||
* (原「启用卡密充值余额功能」,设置键不变,仅语义/文案更新)。
|
||||
*
|
||||
* 卡密核销订单契约:核销不发生真实支付(现金在售卡时已确认),
|
||||
* 订单金额恒为 0(create_redeem_order 源头 + UserOrders 视图层归零覆盖历史行,
|
||||
* 不做数据迁移);订单名称按 note.meta 生成:
|
||||
* 卡密兑换-余额充值X / 卡密兑换-积分充值X / 购入[会员档位名]X天(永久)。
|
||||
*/
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const root = path.join(__dirname, '..', '..');
|
||||
const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8');
|
||||
|
||||
const wallet = read('inc', 'helpers', 'user-center-renderers', 'credits-wallet.php');
|
||||
const creditsJs = read('assets', 'js', 'credits.js');
|
||||
const chargeCard = read('src', 'Payment', 'ChargeCard.php');
|
||||
const userOrders = read('src', 'Payment', 'UserOrders.php');
|
||||
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 creditsRenderer = read('inc', 'helpers', 'user-center-renderers', 'credits-balance.php');
|
||||
const creditsCss = read('assets', 'css', 'routes', 'user-center', 'credits.css');
|
||||
|
||||
test('余额卡片弹窗为通用卡密兑换:标题与提交不再绑定余额类型', () => {
|
||||
assert.match(wallet, /esc_html_e\('卡密兑换', 'aksrt'\)/);
|
||||
assert.doesNotMatch(wallet, /卡密兑换余额/);
|
||||
assert.doesNotMatch(wallet, /data-type="balance"/);
|
||||
assert.doesNotMatch(wallet, /esc_html__\('兑换余额', 'aksrt'\)/);
|
||||
// 弹窗根节点不带类型值 → 前端 type='' → 后端按卡密自动识别。
|
||||
assert.match(wallet, /data-aksrt-card-pass-modal data-aksrt-card-pass=""/);
|
||||
assert.match(wallet, /data-aksrt-card-redeem(?![^>]*data-type)/);
|
||||
// 卡号输入恒展示且为选填(是否必填由后端按卡密类型判定)。
|
||||
assert.match(wallet, /data-aksrt-card-no(?![^>]*required)/);
|
||||
assert.match(wallet, /请输入卡号(选填)/);
|
||||
});
|
||||
|
||||
test('入口展示受「启用卡密兑换功能」总开关控制', () => {
|
||||
assert.match(wallet, /\$card_pass_redeem\['enabled'\]/);
|
||||
assert.doesNotMatch(wallet, /\$card_pass_balance/);
|
||||
// ChargeCard 缺失时不做 pass_charge 兜底(兑换 REST 依赖该类,兜底无意义)。
|
||||
assert.doesNotMatch(wallet, /\$recharge\['pass_charge'\]/);
|
||||
});
|
||||
|
||||
test('前台唯一兑换入口:页内联块与死文件已删除,JS 不再有 required 卡号校验', () => {
|
||||
assert.equal(
|
||||
fs.existsSync(path.join(root, 'inc', 'helpers', 'user-center-renderers', 'credits-vip.php')),
|
||||
false,
|
||||
'credits-vip.php 死文件应已删除'
|
||||
);
|
||||
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/);
|
||||
// 失去宿主的内联块样式不回潮(弹窗仍在用 __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/);
|
||||
});
|
||||
|
||||
test('前端:VIP 缓存失效按后端识别类型', () => {
|
||||
assert.match(creditsJs, /var redeemedType = \(data && data\.type\) \|\| type;/);
|
||||
assert.match(creditsJs, /redeemedType === 'vip'/);
|
||||
});
|
||||
|
||||
test('后端:核销订单金额恒为 0,options 路由与 labels 死代码不回潮', () => {
|
||||
assert.match(chargeCard, /\/pay\/card-pass\/redeem/);
|
||||
assert.doesNotMatch(chargeCard, /card-pass\/options/);
|
||||
assert.doesNotMatch(chargeCard, /rest_options/);
|
||||
assert.doesNotMatch(chargeCard, /'labels'\s+=>\s+\$labels/);
|
||||
// 通用兑换能力后端契约保持:三类可兑换 + 空类型自动识别。
|
||||
assert.match(chargeCard, /REDEEMABLE_TYPES = \[self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS\]/);
|
||||
assert.match(chargeCard, /'enum' => \['', self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS\]/);
|
||||
// create_redeem_order:余额卡不再把面额写进订单金额。
|
||||
assert.doesNotMatch(chargeCard, /TYPE_BALANCE\)\s*\{[^}]*parsed_amount/s);
|
||||
assert.match(chargeCard, /订单金额恒为 0/);
|
||||
// 积分卡开关仅作为后端类型闸门。
|
||||
assert.match(chargeCard, /'credits' => \['enabled' => /);
|
||||
});
|
||||
|
||||
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\)/);
|
||||
assert.match(userOrders, /卡密兑换-余额充值/);
|
||||
assert.match(userOrders, /卡密兑换-积分充值/);
|
||||
assert.match(userOrders, /购入\[%1\$s\]%2\$s/);
|
||||
assert.match(userOrders, /永久/);
|
||||
});
|
||||
|
||||
test('后台 schema 语义更新为「启用卡密兑换功能」', () => {
|
||||
for (const [name, src] of [['SchemaPayment.php', schemaPhp], ['recharge.js', schemaJs]]) {
|
||||
assert.match(src, /启用卡密兑换功能/, name);
|
||||
assert.match(src, /title['"]?\s*(?::|=>)\s*'卡密兑换'/, name);
|
||||
assert.doesNotMatch(src, /启用卡密充值余额功能/, name);
|
||||
assert.doesNotMatch(src, /卡密充值说明/, name);
|
||||
}
|
||||
});
|
||||
@@ -20,7 +20,8 @@ test('dual asset cards use an independent equal-height grid row', () => {
|
||||
assert.match(css, /\.uc-assets-row--dual\s*>\s*\.uc-assets-col:first-child\s*>\s*\.uc-asset-card[\s\S]*?grid-column:\s*1;/)
|
||||
assert.match(css, /\.uc-assets-row--dual\s*>\s*\.uc-assets-col:last-child\s*>\s*\.uc-asset-card[\s\S]*?grid-column:\s*2;/)
|
||||
assert.match(css, /\.uc-assets-row--dual\s*>\s*\.uc-assets-col\s*>\s*\.uc-asset-card\s*\{[\s\S]*?grid-row:\s*1;/)
|
||||
assert.match(css, /\.uc-assets-row--dual\s*>\s*\.uc-assets-col\s*>\s*\.uc-card-pass\s*\{[\s\S]*?grid-row:\s*2;/)
|
||||
// 内联 uc-card-pass 块已删除:双卡网格不再为它预留第二行(弹窗子元素样式除外)。
|
||||
assert.doesNotMatch(css, /\.uc-card-pass(?:[\s:{])/)
|
||||
assert.match(css, /\.uc-asset-card\s*\{[\s\S]*?display:\s*flex;[\s\S]*?flex-direction:\s*column;/)
|
||||
assert.match(css, /\.uc-asset-card__link\s*\{[\s\S]*?margin-top:\s*auto;/)
|
||||
assert.match(css, /\.uc-asset-card__foot\s*\{[\s\S]*?margin-top:\s*auto;/)
|
||||
@@ -74,4 +75,5 @@ test('removed asset card selectors do not return in loaded user-center styles',
|
||||
assert.doesNotMatch(source, /\.uc-assets-row\.uc-asset-card\.aksrt-panel/)
|
||||
assert.doesNotMatch(source, /\.uc-asset-tabs\.aksrt-panel/)
|
||||
assert.doesNotMatch(source, /\.uc-asset-card__foot\s+button/)
|
||||
assert.doesNotMatch(source, /\.uc-card-pass(?:[\s:{])/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user