Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
360 lines
18 KiB
PHP
360 lines
18 KiB
PHP
<?php
|
||
/**
|
||
* VIP modal view model + card HTML (legacy pay-vip modal / vip-card parity).
|
||
*
|
||
* @package AKSRT
|
||
*/
|
||
|
||
namespace AKSRT\Content;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
trait VipServiceModal {
|
||
|
||
/**
|
||
* Build the minimal JSON view model consumed by pay-vip.js.
|
||
*
|
||
* The catalog, member state, package prices, payment methods, and allowed
|
||
* actions are all calculated server-side. No payment gateway response or
|
||
* server-rendered modal HTML is included in this payload.
|
||
*
|
||
* @return array<string,mixed>
|
||
*/
|
||
public function get_modal_view_model(int $user_id, int $prefer_level = 1): array {
|
||
$catalog = $this->get_catalog($user_id);
|
||
$vip = $this->get_vip_settings_context();
|
||
$member = $this->get_vip_member_context($user_id);
|
||
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
|
||
$prefer_level = max(1, min(2, $prefer_level));
|
||
|
||
$level_view = [];
|
||
foreach ([1, 2] as $level) {
|
||
if (empty($levels[$level]['enabled'])) {
|
||
continue;
|
||
}
|
||
$equity = (string) ($levels[$level]['equity'] ?? '');
|
||
$equity = function_exists('aksrt_format_vip_equity_html')
|
||
? (string) aksrt_format_vip_equity_html($equity)
|
||
: wp_kses_post($equity);
|
||
$level_view[] = [
|
||
'level' => $level,
|
||
'name' => wp_kses_post((string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level))),
|
||
'name_text' => (string) ($levels[$level]['name'] ?? ('VIP' . $level)),
|
||
'desc' => wp_kses_post((string) ($levels[$level]['desc'] ?? '')),
|
||
'equity' => $equity,
|
||
'img_icon' => esc_url_raw((string) ($levels[$level]['img_icon'] ?? '')),
|
||
];
|
||
}
|
||
|
||
$package_ids = [];
|
||
$has_renew = false;
|
||
$has_upgrade = false;
|
||
$groups = is_array($catalog['level_packages'] ?? null) ? $catalog['level_packages'] : [];
|
||
foreach ($groups as $group) {
|
||
if (!is_array($group)) {
|
||
continue;
|
||
}
|
||
$action = sanitize_key((string) ($group['action'] ?? 'pay'));
|
||
$has_renew = $has_renew || $action === 'renew';
|
||
$has_upgrade = $has_upgrade || $action === 'upgrade';
|
||
foreach ((array) ($group['items'] ?? []) as $item) {
|
||
if (is_array($item) && (int) ($item['package_id'] ?? 0) > 0) {
|
||
$package_ids[] = (int) $item['package_id'];
|
||
}
|
||
}
|
||
}
|
||
$points_exchange = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : [];
|
||
$methods = is_array($catalog['payment_methods'] ?? null) ? $catalog['payment_methods'] : [];
|
||
// 支付方式 logo(与充值/积分弹窗的 payment-method-radio 组件同源)。
|
||
$methods = array_map(function ($m) {
|
||
$id = sanitize_key((string) ($m['id'] ?? ''));
|
||
return [
|
||
'id' => $id,
|
||
'name' => (string) ($m['name'] ?? $id),
|
||
'logo' => $this->payment_method_logo_url($id),
|
||
];
|
||
}, $methods);
|
||
$card_pass = is_array($catalog['card_pass'] ?? null)
|
||
? array_merge(['enabled' => false, 'desc' => '', 'only_password' => false], $catalog['card_pass'])
|
||
: ['enabled' => false, 'desc' => '', 'only_password' => false];
|
||
$no_payment_method = $methods === [];
|
||
$action_reasons = [
|
||
'purchase' => $no_payment_method
|
||
? 'no_payment_method'
|
||
: ($package_ids === [] ? 'no_package' : ''),
|
||
'renew' => $no_payment_method
|
||
? 'no_payment_method'
|
||
: ($has_renew ? '' : 'not_available'),
|
||
'upgrade' => $no_payment_method
|
||
? 'no_payment_method'
|
||
: ($has_upgrade ? '' : 'not_available'),
|
||
'points_exchange' => !empty($points_exchange['enabled']) && !empty($points_exchange['packages'])
|
||
? ''
|
||
: 'not_available',
|
||
];
|
||
|
||
$member_state = (string) ($member['state'] ?? 'unopened');
|
||
$member_version = substr(hash('sha256', (string) wp_json_encode([
|
||
'level' => (int) ($member['level'] ?? 0),
|
||
'expires' => (int) ($member['expires'] ?? 0),
|
||
'state' => $member_state,
|
||
])), 0, 16);
|
||
$catalog_version = substr(hash('sha256', (string) wp_json_encode([
|
||
'packages' => $groups,
|
||
'points' => $points_exchange,
|
||
'methods' => $methods,
|
||
'levels' => $level_view,
|
||
])), 0, 16);
|
||
|
||
$user = get_userdata($user_id);
|
||
$avatar_url = function_exists('get_avatar_url')
|
||
? (string) get_avatar_url($user_id, ['size' => 42])
|
||
: '';
|
||
|
||
return [
|
||
'enabled' => !empty($catalog['enabled']),
|
||
'prefer_level' => $prefer_level,
|
||
'catalog' => [
|
||
'desc' => (string) ($catalog['desc'] ?? ''),
|
||
'more' => (string) ($vip['more'] ?? ''),
|
||
'currency_name' => (string) ($catalog['currency_name'] ?? ''),
|
||
'level_packages' => $groups,
|
||
'points_exchange' => $points_exchange,
|
||
'wallet_balance' => (string) ($catalog['wallet_balance'] ?? '0.00'),
|
||
],
|
||
'member' => [
|
||
'id' => $user_id,
|
||
'name' => $user ? (string) $user->display_name : '',
|
||
'avatar_url' => $avatar_url,
|
||
'level' => (int) ($member['level'] ?? 0),
|
||
'expires' => (int) ($member['expires'] ?? 0),
|
||
'expires_label' => (string) ($member['expires_label'] ?? ''),
|
||
'state' => (string) ($member['state'] ?? 'unopened'),
|
||
'is_permanent' => !empty($member['is_permanent']),
|
||
'logged_in' => $user_id > 0,
|
||
],
|
||
'levels' => $level_view,
|
||
'payment_methods' => $methods,
|
||
'card_pass' => $card_pass,
|
||
'currency_mark' => (string) (\AKSRT\Settings\Settings::get_payment_gateway_settings()['currency']['mark'] ?? '¥'),
|
||
'allowed_actions' => [
|
||
'purchase' => $package_ids !== [] && $methods !== [],
|
||
'renew' => $has_renew && $methods !== [],
|
||
'upgrade' => $has_upgrade && $methods !== [],
|
||
'points_exchange' => !empty($points_exchange['enabled']) && !empty($points_exchange['packages']),
|
||
],
|
||
'action_reasons' => $action_reasons,
|
||
'version' => [
|
||
'schema' => 1,
|
||
'member' => $member_version,
|
||
'catalog' => $catalog_version,
|
||
// Stable revision token; do not use request time and bust short cache.
|
||
'updated_at' => $member_version . ':' . $catalog_version,
|
||
'tags' => ['vip', 'orders'],
|
||
],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 积分兑换会员弹窗(参考主题 zibpay_get_vip_points_exchange_modal 1:1 移植)。
|
||
*
|
||
* 服务端静态输出参考主题 refresh_modal 同构外壳:
|
||
* .modal.flex.jc.fade > .modal-dialog.modal-mini.full-sm
|
||
* > .modal-content > .modal-body
|
||
* 与余额充值/购买积分弹窗同构(静止态由 vip-modal.css 显式隐藏,打开态由
|
||
* pay-vip.js 的自包含开关以内联 display:flex 驱动)。
|
||
*
|
||
* 入口有两处(均带 data-aksrt-vip-points-open,pay-vip.js 文档级委托开合):
|
||
* 会员购买弹窗内的「积分兑换会员」按钮,以及 /user/vip/ 档位购买卡下方的
|
||
* 入口横条(aksrt_get_vip_points_exchange_entry_html)。.pay-vip 触发点
|
||
* 全站可见,因此由 aksrt_render_vip_points_exchange_modal() 在 wp_footer
|
||
* 全局输出一次。
|
||
*
|
||
* @return string 空串表示积分兑换不可用(不输出弹窗壳)。
|
||
*/
|
||
public function get_points_exchange_modal_html(int $user_id = 0): string {
|
||
$user_id = max(0, $user_id);
|
||
if ($user_id <= 0 || !class_exists('\AKSRT\Settings\Settings')) {
|
||
return '';
|
||
}
|
||
|
||
// 先做零成本门控,避免每次页面渲染都去查积分余额。
|
||
$vip = $this->get_vip_settings_context();
|
||
if (empty($vip['enabled']) || empty($vip['points_exchange']['enabled']) || !\AKSRT\Settings\Settings::credits_enabled()) {
|
||
return '';
|
||
}
|
||
|
||
$catalog = $this->get_catalog($user_id);
|
||
$points_ex = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : [];
|
||
$packages = is_array($points_ex['packages'] ?? null) ? $points_ex['packages'] : [];
|
||
if ($packages === []) {
|
||
return '';
|
||
}
|
||
|
||
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
|
||
$user_points = max(0, (int) ($catalog['user_points'] ?? 0));
|
||
$currency = (string) ($catalog['currency_name'] ?? __('积分', 'aksrt'));
|
||
$points_center_url = function_exists('aksrt_get_user_center_tab_url')
|
||
? aksrt_get_user_center_tab_url('credits')
|
||
: home_url('/');
|
||
|
||
// 会员卡片(参考主题 data-for/data-value 选中模型;data-value 为本主题的套餐 ID)。
|
||
$cards = '';
|
||
$min_points = 0;
|
||
$first_package_id = 0;
|
||
foreach ($packages as $index => $package) {
|
||
if (!is_array($package)) {
|
||
continue;
|
||
}
|
||
$package_id = max(0, (int) ($package['package_id'] ?? 0));
|
||
if ($package_id <= 0) {
|
||
continue;
|
||
}
|
||
if ($first_package_id === 0) {
|
||
$first_package_id = $package_id;
|
||
}
|
||
$level = max(1, min(2, (int) ($package['level'] ?? 1)));
|
||
$points = max(0, (int) ($package['points'] ?? 0));
|
||
// 参考主题时长行:30<span class="px12">天</span> / 永久;time 为 Permanent 时按永久渲染。
|
||
$is_permanent = !empty($package['is_permanent']);
|
||
$pkg_time = max(0, (int) ($package['time'] ?? 0));
|
||
$pkg_unit = ((string) ($package['unit'] ?? 'day')) === 'month' ? 'month' : 'day';
|
||
$duration = ($is_permanent || $pkg_time <= 0)
|
||
? esc_html__('永久', 'aksrt')
|
||
: (string) $pkg_time . '<span class="px12">' . esc_html($pkg_unit === 'month' ? __('个月', 'aksrt') : __('天', 'aksrt')) . '</span>';
|
||
$level_name = (string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level));
|
||
|
||
$cards .= '<div class="vip-card vip-theme' . $level . ($index === 0 ? ' active' : '') . ' " data-for="product_id" data-value="' . $package_id . '">'
|
||
. '<span class=" ">' . $this->points_exchange_card_icon($levels[$level] ?? []) . wp_kses_post($level_name) . '</span>'
|
||
. '<div class="flex jsb ac hh em16 mt10">'
|
||
. '<span class="shrink0 mr10">' . $duration . '</span>'
|
||
. '<span class="shrink0">' . $points . '<span class="px12">' . esc_html($currency) . '</span></span>'
|
||
. '</div></div>';
|
||
|
||
if ($min_points === 0 || $points < $min_points) {
|
||
$min_points = $points;
|
||
}
|
||
}
|
||
if ($cards === '') {
|
||
return '';
|
||
}
|
||
|
||
$desc_text = trim((string) ($vip['points_exchange']['desc'] ?? ''));
|
||
$desc = $desc_text !== ''
|
||
// 管理员编辑的富文本说明:保存端已 wp_kses_post,这里同级别输出
|
||
// (参考主题 zibpay_get_vip_points_exchange_modal 原样拼接)。
|
||
? '<div class="muted-box muted-2-color padding-10 em09 mt20 aksrt-vip-points-desc">' . wp_kses_post($desc_text) . '</div>'
|
||
: '';
|
||
|
||
// 最低价套餐仍超出用户积分 → 参考主题的 badg c-red btn-block 提示。
|
||
// (参考主题源码写作 `!$min_val > $user_points`,因运算符优先级恒假,
|
||
// 此处按其原意取真实比较,保证提示分支可达。)
|
||
if ($min_points > $user_points) {
|
||
$form = '<div class="mt20"><div class="badg c-red btn-block">' . esc_html__('积分不足,暂时无法兑换', 'aksrt') . '</div></div>';
|
||
} else {
|
||
$form = '<div class="mt20">'
|
||
. '<input type="hidden" name="product_id" value="' . $first_package_id . '">'
|
||
. '<input type="hidden" name="order_type" value="4">'
|
||
. '<input type="hidden" name="action" value="points_initiate_pay">'
|
||
. '<button type="submit" class="mt6 but jb-yellow btn-block radius padding-lg" data-aksrt-vip-points-submit>'
|
||
. '<i class="fa fas fa-check" aria-hidden="true"></i>' . esc_html__('立即兑换', 'aksrt')
|
||
. '</button></div>';
|
||
}
|
||
|
||
$header = '<div class="mb10 touch">'
|
||
. '<button type="button" class="close" data-dismiss="modal" aria-label="' . esc_attr__('关闭', '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-vip_1"></use></svg></span>'
|
||
. esc_html__('积分兑换会员', 'aksrt') . '</b></div>';
|
||
|
||
$user_box = '<div class="muted-box padding-h10 mb20">'
|
||
. '<a rel="nofollow" class="flex jsb ac" href="' . esc_url($points_center_url) . '">'
|
||
. '<span class="muted-2-color">'
|
||
. '<svg class="icon" aria-hidden="true"><use xlink:href="#icon-points"></use></svg> '
|
||
. esc_html__('我的积分', 'aksrt') . '</span>'
|
||
. '<span class="font-bold ml6 em14"><span class="c-green mr10">' . $user_points . '</span>'
|
||
. '<i class="fa fas fa-angle-right" aria-hidden="true"></i></span></a></div>';
|
||
|
||
$lists = '<div class="muted-2-color mb6 em09">' . esc_html__('请选择需兑换的会员', 'aksrt') . '</div>'
|
||
. '<div class="flex hh exchange-card-box">' . $cards . '</div>';
|
||
|
||
return '<div class="modal flex jc fade" id="aksrt-vip-points-modal" tabindex="-1" role="dialog" aria-hidden="false" data-aksrt-vip-points-modal>'
|
||
. '<div class="modal-dialog modal-mini full-sm" role="document">'
|
||
. '<div class="modal-content"><div class="modal-body">'
|
||
. $header
|
||
. '<form class="" data-aksrt-vip-points-form>' . $user_box . $lists . $desc . $form . '</form>'
|
||
. '</div></div></div></div>';
|
||
}
|
||
|
||
/**
|
||
* 积分兑换卡片等级图标(参考主题 zibpay_get_vip_card_icon)。
|
||
*
|
||
* @param array<string,mixed> $level_settings 等级设置(含 name / img_icon)。
|
||
*/
|
||
private function points_exchange_card_icon(array $level_settings): string {
|
||
$name_text = (string) ($level_settings['name'] ?? '');
|
||
$icon_url = (string) ($level_settings['img_icon'] ?? '');
|
||
if ($icon_url === '') {
|
||
return '<i class="fa fas fa-certificate mr3" aria-hidden="true"></i>';
|
||
}
|
||
|
||
return '<img' . aksrt_get_lazy_attr('lazy_other', $icon_url, 'img-icon mr3')
|
||
. ' title="' . esc_attr($name_text) . '" alt="' . esc_attr($name_text) . '">';
|
||
}
|
||
|
||
/**
|
||
* 支付方式 logo 映射(assets/img/pay-*-logo.svg,与充值/积分弹窗一致)。
|
||
*/
|
||
private function payment_method_logo_url(string $id): string {
|
||
$logos = [
|
||
'balance' => 'pay-balance-logo.svg',
|
||
'alipay' => 'pay-alipay-logo.svg',
|
||
'epay_alipay' => 'pay-alipay-logo.svg',
|
||
'wechatpay' => 'pay-wechat-logo.svg',
|
||
'epay_wxpay' => 'pay-wechat-logo.svg',
|
||
'paypal' => 'pay-paypal-logo.svg',
|
||
'stripe' => 'pay-stripe-logo.svg',
|
||
'card_pass' => 'pay-card-pass-logo.svg',
|
||
];
|
||
$file = $logos[$id] ?? '';
|
||
return $file === '' ? '' : esc_url_raw(get_template_directory_uri() . '/assets/img/' . $file);
|
||
}
|
||
|
||
/**
|
||
* Mini hover card for header (legacy aksrtpay_get_vip_card).
|
||
*/
|
||
public function render_vip_card(int $level, int $user_id = 0): string {
|
||
$level = max(1, min(2, $level));
|
||
$vip = $this->get_vip_settings_context();
|
||
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
|
||
if (empty($levels[$level]['enabled'])) {
|
||
return '';
|
||
}
|
||
$name = wp_kses_post((string) ($levels[$level]['name_html'] ?? $levels[$level]['name'] ?? ('VIP' . $level)));
|
||
$name_text = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
|
||
$equity = (string) ($levels[$level]['equity'] ?? '');
|
||
$cls = $user_id > 0 ? ' pay-vip' : ' signin-loader';
|
||
|
||
$equity_html = $equity !== '' && function_exists('aksrt_format_vip_equity_html')
|
||
? aksrt_format_vip_equity_html($equity, 'mb10 relative')
|
||
: ($equity !== '' ? '<div class="v-equity mb10 relative">' . wp_kses_post($equity) . '</div>' : '');
|
||
|
||
$icon_url = esc_url((string) ($levels[$level]['img_icon'] ?? ''));
|
||
$icon = $icon_url !== ''
|
||
? '<img' . aksrt_get_lazy_attr('lazy_other', $icon_url, 'img-icon') . ' title="' . esc_attr($name_text) . '" alt="' . esc_attr($name_text) . '" decoding="async">'
|
||
: '<i class="fa fas fa-gem" aria-hidden="true"></i>';
|
||
$html = '<div class="vip-card pointer level-' . $level . ' vip-theme' . $level . $cls . '" vip-level="' . $level . '">';
|
||
$html .= '<div class="abs-center vip-baicon" aria-hidden="true">' . $icon . '</div>';
|
||
$html .= '<div class="vip-img abs-right">' . $icon . '</div>';
|
||
$html .= '<div class="relative">';
|
||
$html .= '<div class="vip-name mb10"><span class="mr6">' . $icon . '</span>' . wp_kses_post($name) . '</div>';
|
||
$html .= $equity_html;
|
||
$html .= '<a class="but jb-blue radius payvip-button" href="javascript:;">'
|
||
. wp_kses_post(sprintf(__('开通%s', 'aksrt'), $name))
|
||
. '<span class="ml6"><i class="fa fas fa-chevron-circle-right" aria-hidden="true"></i></span></a>';
|
||
$html .= '</div></div>';
|
||
return $html;
|
||
}
|
||
}
|