perf(modal): unify cancellable frontend modal flows

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 02:04:55 +08:00
co-authored by Claude Fable 5
parent 25ce1d8f43
commit 5375ad6429
36 changed files with 2623 additions and 1806 deletions
+186 -132
View File
@@ -9,27 +9,114 @@ defined('ABSPATH') || exit;
trait VipServicePurchase {
/** @var array<string,mixed>|null */
private $vip_settings_cache = null;
/** @var array<int,array<string,mixed>> */
private $vip_member_cache = [];
/** @var array<int,array<string,mixed>> */
private $vip_catalog_cache = [];
/** @var array<string,mixed>|null */
private $vip_paid_display_cache = null;
/**
* Read payment settings once per request. The cache is kept on the
* singleton instance and is never shared between users or requests.
*
* @return array<string,mixed>
*/
private function get_vip_settings_context(): array {
if (is_array($this->vip_settings_cache)) {
return $this->vip_settings_cache;
}
$settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_payment_vip_settings()
: [];
$this->vip_settings_cache = is_array($settings)
? $settings
: ['enabled' => false, 'levels' => [], 'renew' => [], 'upgrade' => [], 'points_exchange' => []];
return $this->vip_settings_cache;
}
/**
* Read the current member state once per request and user.
*
* @return array{level:int,expires:int,is_permanent:bool,state:string,expires_label:string}
*/
private function get_vip_member_context(int $user_id): array {
$user_id = max(0, $user_id);
if (array_key_exists($user_id, $this->vip_member_cache)) {
return $this->vip_member_cache[$user_id];
}
$level = $user_id > 0 && function_exists('aksrt_get_user_vip_level')
? (int) aksrt_get_user_vip_level($user_id)
: 0;
$expires = $user_id > 0 ? (int) get_user_meta($user_id, 'aksrt_vip_expires', true) : 0;
$member_state = 'unopened';
if ($level > 0) {
$member_state = $expires <= 0
? 'permanent'
: ($expires < time() ? 'expired' : 'active');
} elseif ($expires > 0 && $expires < time()) {
// aksrt_get_user_vip_level() lazily clears expired levels; retain
// the observable expired state for this response without reviving it.
$member_state = 'expired';
}
$state = [
'level' => max(0, $level),
'expires' => $expires,
'is_permanent' => $level > 0 && $expires <= 0,
'state' => $member_state,
'expires_label' => $member_state === 'expired'
? __('已过期', 'aksrt')
: $this->format_expires_label($expires, $level),
];
$this->vip_member_cache[$user_id] = $state;
return $state;
}
/**
* Cache paid-content display settings alongside the VIP request context.
*
* @return array<string,mixed>
*/
private function get_paid_display_context(): array {
if (is_array($this->vip_paid_display_cache)) {
return $this->vip_paid_display_cache;
}
$settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_paid_content_settings()
: [];
$this->vip_paid_display_cache = is_array($settings) ? $settings : [];
return $this->vip_paid_display_cache;
}
/**
* @return array<string,mixed>
*/
public function get_catalog(int $user_id = 0): array {
$vip = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_payment_vip_settings()
: ['enabled' => false, 'levels' => [], 'renew' => [], 'upgrade' => [], 'points_exchange' => []];
$user_id = max(0, $user_id);
if (array_key_exists($user_id, $this->vip_catalog_cache)) {
return $this->vip_catalog_cache[$user_id];
}
$level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0;
$expires = $user_id > 0 ? (int) get_user_meta($user_id, 'aksrt_vip_expires', true) : 0;
$is_permanent = ($level > 0 && $expires <= 0);
$vip = $this->get_vip_settings_context();
$member = $this->get_vip_member_context($user_id);
$level = (int) $member['level'];
$expires = (int) $member['expires'];
$is_permanent = !empty($member['is_permanent']);
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
$renew = is_array($vip['renew'] ?? null) ? $vip['renew'] : [];
$upgrade = is_array($vip['upgrade'] ?? null) ? $vip['upgrade'] : [];
$points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : [];
$renew_enabled = !isset($renew['enabled']) || !empty($renew['enabled']);
$upgrade_enabled = !isset($upgrade['enabled']) || !empty($upgrade['enabled']);
$points_enabled = !empty($points_ex['enabled'])
&& (!class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::credits_enabled());
$credits_enabled = !class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::credits_enabled();
$points_enabled = !empty($points_ex['enabled']) && $credits_enabled;
$level_packages = [];
foreach ([1, 2] as $lv) {
@@ -141,19 +228,20 @@ trait VipServicePurchase {
$methods = $this->available_payment_methods();
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
return [
$catalog = [
'enabled' => !empty($vip['enabled']),
'desc' => (string) ($vip['desc'] ?? ''),
'help_text' => (string) ($vip['help_text'] ?? ''),
'user_level' => $level,
'user_expires' => $expires,
'user_is_permanent' => $is_permanent,
'user_expires_label' => $this->format_expires_label($expires, $level),
'user_expires_label' => (string) ($member['expires_label'] ?? ''),
'user_points' => $points_balance,
'wallet_balance' => $wallet_balance,
'currency_name' => $currency_name,
'renew_enabled' => $renew_enabled,
'upgrade_enabled' => $upgrade_enabled,
'credits_enabled' => $credits_enabled,
'renew_desc' => (string) ($renew['desc'] ?? ''),
'upgrade_desc' => (string) ($upgrade['desc'] ?? ''),
'level_packages' => $level_packages,
@@ -168,6 +256,36 @@ trait VipServicePurchase {
: (function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/')),
'logged_in' => $user_id > 0,
];
$this->vip_catalog_cache[$user_id] = $catalog;
return $catalog;
}
/**
* Find a package in the server-derived catalog.
*
* The returned group name is request-local metadata used only for the
* order subject; price and entitlement data remain server-derived.
*
* @param array<string,mixed> $catalog
* @return array<string,mixed>|null
*/
private function find_catalog_package(array $catalog, int $package_id): ?array {
$groups = is_array($catalog['level_packages'] ?? null) ? $catalog['level_packages'] : [];
foreach ($groups as $group) {
if (!is_array($group)) {
continue;
}
$group_name = (string) ($group['name'] ?? '');
$items = is_array($group['items'] ?? null) ? $group['items'] : [];
foreach ($items as $item) {
if (!is_array($item) || (int) ($item['package_id'] ?? 0) !== $package_id) {
continue;
}
$item['_level_name'] = $group_name !== '' ? $group_name : ('VIP' . (int) ($item['level'] ?? 1));
return $item;
}
}
return null;
}
@@ -180,106 +298,36 @@ trait VipServicePurchase {
return new \WP_Error('aksrt_need_login', __('请先登录后再开通会员。', 'aksrt'), ['status' => 401]);
}
$decoded = $this->decode_package_id($package_id);
if (!$decoded) {
return new \WP_Error('aksrt_invalid_package', __('套餐无效', 'aksrt'), ['status' => 400]);
}
$level = (int) $decoded['level'];
$index = (int) $decoded['index'];
$vip = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_payment_vip_settings()
: ['enabled' => false, 'levels' => [], 'renew' => [], 'upgrade' => []];
if (empty($vip['enabled'])) {
$catalog = $this->get_catalog($user_id);
if (empty($catalog['enabled'])) {
return new \WP_Error('aksrt_vip_disabled', __('会员功能未启用', 'aksrt'), ['status' => 400]);
}
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
if (empty($levels[$level]['enabled'])) {
return new \WP_Error('aksrt_vip_level_disabled', __('该会员等级未启用', 'aksrt'), ['status' => 400]);
$pkg = $this->find_catalog_package($catalog, $package_id);
if (!$pkg) {
return new \WP_Error('aksrt_invalid_package', __('套餐不存在或当前不可购买', 'aksrt'), ['status' => 400]);
}
$user_level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0;
$expires = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
$is_permanent = ($user_level > 0 && $expires <= 0);
$renew = is_array($vip['renew'] ?? null) ? $vip['renew'] : [];
$upgrade = is_array($vip['upgrade'] ?? null) ? $vip['upgrade'] : [];
$renew_enabled = !isset($renew['enabled']) || !empty($renew['enabled']);
$upgrade_enabled = !isset($upgrade['enabled']) || !empty($upgrade['enabled']);
if ($is_permanent && $level <= $user_level) {
return new \WP_Error('aksrt_vip_permanent', __('您已是永久会员,无需续费。', 'aksrt'), ['status' => 400]);
}
if ($user_level > 0 && $level === $user_level && !$renew_enabled) {
return new \WP_Error('aksrt_vip_renew_disabled', __('续费功能未开启。', 'aksrt'), ['status' => 400]);
}
if ($user_level > 0 && $level > $user_level && !$upgrade_enabled) {
return new \WP_Error('aksrt_vip_upgrade_disabled', __('升级功能未开启。', 'aksrt'), ['status' => 400]);
}
if ($user_level > 0 && $level < $user_level) {
return new \WP_Error('aksrt_vip_downgrade', __('不支持降级购买。', 'aksrt'), ['status' => 400]);
}
$action = 'pay';
if ($user_level > 0 && $level === $user_level) {
$action = 'renew';
} elseif ($user_level > 0 && $level > $user_level) {
$action = 'upgrade';
}
// Dedicated upgrade product:
// 901 permanent/fixed, 902 unit remaining days, 903 jump permanent.
$upgrade_mode = '';
$pkg_time = 0;
$pkg_unit = 'day';
if (in_array($index, [901, 902, 903], true)) {
$upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? ''));
$match = null;
foreach ($upgrade_items as $ui) {
if ((int) ($ui['package_id'] ?? 0) === $package_id) {
$match = $ui;
break;
}
}
if (!$match || $level !== 2) {
return new \WP_Error('aksrt_invalid_package', __('升级套餐不存在', 'aksrt'), ['status' => 400]);
}
$pkg = [
'price' => (float) $match['price'],
'days' => (int) $match['days'],
'title' => (string) $match['title'],
];
$amount = round((float) $match['price'], 2);
$days = (int) $match['days'];
$pkg_time = $days; // unit upgrade keeps days as remaining; jump/permanent use 0
$level = (int) ($pkg['level'] ?? 0);
$index = (int) ($pkg['index'] ?? 0);
$action = sanitize_key((string) ($pkg['action'] ?? 'pay'));
$amount = round((float) ($pkg['price'] ?? 0), 2);
$base_price = (float) ($pkg['base_price'] ?? $amount);
$days = (int) ($pkg['days'] ?? 0);
$pkg_time = array_key_exists('time', $pkg) ? (int) $pkg['time'] : $days;
$pkg_unit = sanitize_key((string) ($pkg['unit'] ?? 'day'));
if (!in_array($pkg_unit, ['day', 'month'], true)) {
$pkg_unit = 'day';
$base_price = (float) ($match['base_price'] ?? $amount);
$discount_tag = (string) ($match['discount_tag'] ?? __('升级专享', 'aksrt'));
$upgrade_mode = sanitize_key((string) ($match['upgrade_mode'] ?? ''));
$action = 'upgrade';
if ($upgrade_mode === 'jump' || $upgrade_mode === 'permanent') {
$pkg_time = 0;
$days = 0;
}
} else {
$level_cfg = is_array($levels[$level] ?? null) ? $levels[$level] : [];
if (isset($level_cfg['product_s']) && empty($level_cfg['product_s'])) {
return new \WP_Error('aksrt_vip_product_disabled', __('该等级暂不支持购买', 'aksrt'), ['status' => 400]);
}
$packages = $this->parse_level_products((string) ($level_cfg['products_raw'] ?? ''));
$pkg = $packages[$index - 1] ?? null;
if (!$pkg) {
return new \WP_Error('aksrt_invalid_package', __('套餐不存在', 'aksrt'), ['status' => 400]);
}
$base_price = (float) $pkg['price'];
$days = (int) $pkg['days'];
$pkg_time = (int) ($pkg['time'] ?? 0);
$pkg_unit = (string) ($pkg['unit'] ?? 'day');
$priced = $this->apply_action_price($base_price, $action, $renew, $upgrade, $level, $user_level, $expires, $is_permanent, $days);
if ($priced === null) {
return new \WP_Error('aksrt_invalid_package', __('当前无法购买该套餐', 'aksrt'), ['status' => 400]);
}
$amount = round((float) $priced['price'], 2);
$discount_tag = (string) ($priced['tag'] ?? '');
}
$discount_tag = (string) ($pkg['discount_tag'] ?? '');
$upgrade_mode = sanitize_key((string) ($pkg['upgrade_mode'] ?? ''));
if (in_array($upgrade_mode, ['jump', 'permanent'], true)) {
$pkg_time = 0;
$days = 0;
}
if ($level < 1 || $level > 2 || $index < 1 || $action === '') {
return new \WP_Error('aksrt_invalid_package', __('套餐无效', 'aksrt'), ['status' => 400]);
}
if ($amount <= 0) {
@@ -310,6 +358,7 @@ trait VipServicePurchase {
'discount_tag' => $discount_tag,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$expires_at = gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS);
$order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([
'user_id' => $user_id,
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP,
@@ -319,27 +368,36 @@ trait VipServicePurchase {
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
'payment_method' => $payment_method,
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
'expires_at' => $expires_at,
]);
if ($order_number === '') {
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
$order = \AKSRT\Payment\OrderRepository::get_instance()->get($order_number);
if (!$order) {
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
$level_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level));
// Build the request-local order context from the same server-derived
// package used for validation. PaymentOrchestrator still reloads the
// persisted row under its attempt lock before touching the gateway.
$level_name = (string) ($pkg['_level_name'] ?? ('VIP' . $level));
$action_label = $action === 'renew' ? __('续费', 'aksrt') : ($action === 'upgrade' ? __('升级', 'aksrt') : __('开通', 'aksrt'));
$order['subject'] = sprintf(
/* translators: 1: action 2: vip name 3: package title */
__('%1$s%2$s · %3$s', 'aksrt'),
$action_label,
$level_name,
(string) ($pkg['title'] ?? '')
);
$order['payment_method'] = $payment_method;
$order = [
'order_number' => $order_number,
'user_id' => $user_id,
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP,
'product_id' => $package_id,
'amount' => $amount,
'currency' => 'CNY',
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
'payment_method' => $payment_method,
'note' => is_string($note) ? $note : '',
'expires_at' => $expires_at,
'subject' => sprintf(
/* translators: 1: action 2: vip name 3: package title */
__('%1$s%2$s · %3$s', 'aksrt'),
$action_label,
$level_name,
(string) ($pkg['title'] ?? '')
),
];
if (!class_exists('\AKSRT\Payment\Gateway')) {
return new \WP_Error('aksrt_pay_unavailable', __('支付网关不可用', 'aksrt'), ['status' => 500]);
@@ -355,6 +413,7 @@ trait VipServicePurchase {
$payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [
'paid_note' => __('用户完成会员开通支付。', 'aksrt'),
'order_context' => $order,
]);
if (empty($payment_result['success'])) {
return new \WP_Error(
@@ -568,28 +627,23 @@ trait VipServicePurchase {
if (!class_exists('\AKSRT\Credits\Credits')) {
return new \WP_Error('aksrt_credits_unavailable', __('积分系统不可用', 'aksrt'), ['status' => 500]);
}
if (class_exists('\AKSRT\Settings\Settings') && !\AKSRT\Settings\Settings::credits_enabled()) {
$catalog = $this->get_catalog($user_id);
if (array_key_exists('credits_enabled', $catalog) && empty($catalog['credits_enabled'])) {
return new \WP_Error('aksrt_credits_disabled', __('积分功能未启用', 'aksrt'), ['status' => 400]);
}
$vip = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_payment_vip_settings()
: [];
$points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : [];
$points_ex = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : [];
if (empty($points_ex['enabled'])) {
return new \WP_Error('aksrt_points_exchange_disabled', __('积分兑换会员未启用', 'aksrt'), ['status' => 400]);
}
$user_level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0;
if ($user_level > 0) {
if ((int) ($catalog['user_level'] ?? 0) > 0) {
return new \WP_Error('aksrt_already_vip', __('您已是会员,无法使用积分兑换。', 'aksrt'), ['status' => 400]);
}
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
$packages = $this->get_points_exchange_packages($vip, $levels);
$match = null;
$packages = is_array($points_ex['packages'] ?? null) ? $points_ex['packages'] : [];
foreach ($packages as $pkg) {
if ((int) ($pkg['package_id'] ?? 0) === $package_id) {
if (is_array($pkg) && (int) ($pkg['package_id'] ?? 0) === $package_id) {
$match = $pkg;
break;
}
@@ -605,7 +659,7 @@ trait VipServicePurchase {
return new \WP_Error('aksrt_invalid_price', __('兑换积分无效', 'aksrt'), ['status' => 400]);
}
$balance = \AKSRT\Credits\Credits::balance($user_id);
$balance = (int) ($catalog['user_points'] ?? 0);
if ($balance < $cost) {
return new \WP_Error('aksrt_credits_insufficient', __('积分不足,暂时无法兑换', 'aksrt'), [
'status' => 402,