Files
wordpress-theme-aksrt/src/Content/VipServicePurchase.php
T
2026-08-09 16:40:27 +08:00

774 lines
33 KiB
PHP

<?php
/**
* @package AKSRT
*/
namespace AKSRT\Content;
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 {
$user_id = max(0, $user_id);
if (array_key_exists($user_id, $this->vip_catalog_cache)) {
return $this->vip_catalog_cache[$user_id];
}
$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']);
$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) {
if (empty($levels[$lv]['enabled'])) {
continue;
}
// Cash products can be disabled per level.
$product_s = !isset($levels[$lv]['product_s']) || !empty($levels[$lv]['product_s']);
if ($is_permanent && $lv <= $level) {
continue;
}
if ($level > 0 && $lv === $level && !$renew_enabled) {
continue;
}
if ($level > 0 && $lv > $level && !$upgrade_enabled) {
continue;
}
$name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
$items = [];
$packages = $product_s
? $this->parse_level_products((string) ($levels[$lv]['products_raw'] ?? ''))
: [];
foreach ($packages as $idx => $pkg) {
$base_price = (float) $pkg['price'];
$days = (int) $pkg['days'];
$action = 'pay';
if ($level > 0 && $lv === $level) {
$action = 'renew';
} elseif ($level > 0 && $lv > $level) {
$action = 'upgrade';
}
$priced = $this->apply_action_price($base_price, $action, $renew, $upgrade, $lv, $level, $expires, $is_permanent, $days);
if ($priced === null) {
continue;
}
$show_price = (float) ($priced['show_price'] ?? 0);
if ($show_price <= 0 && !empty($pkg['show_price']) && $action === 'pay') {
$show_price = (float) $pkg['show_price'];
}
$discount_tag = (string) ($priced['tag'] ?? '');
if ($discount_tag === '' && $action === 'pay' && !empty($pkg['tag'])) {
$discount_tag = (string) $pkg['tag'];
}
$days_label = __('永久', 'aksrt');
if ((int) $pkg['time'] > 0) {
$days_label = ((string) ($pkg['unit'] ?? 'day') === 'month')
? sprintf(__('%d 个月', 'aksrt'), (int) $pkg['time'])
: sprintf(__('%d 天', 'aksrt'), (int) $pkg['time']);
}
$items[] = [
'package_id' => $this->encode_package_id($lv, $idx + 1),
'level' => $lv,
'index' => $idx + 1,
'action' => $action,
'price' => (float) $priced['price'],
'base_price' => $base_price,
'show_price' => $show_price,
'discount_tag' => $discount_tag,
'days' => $days,
'time' => (int) ($pkg['time'] ?? 0),
'unit' => (string) ($pkg['unit'] ?? 'day'),
'title' => (string) $pkg['title'],
'price_label' => $this->format_cash_label((float) $priced['price']),
'show_price_label' => $show_price > 0
? $this->format_cash_label($show_price)
: '',
'days_label' => $days_label,
'cta_label' => $action === 'renew'
? __('立即续费', 'aksrt')
: ($action === 'upgrade' ? __('立即升级', 'aksrt') : __('立即开通', 'aksrt')),
];
}
// Dedicated upgrade product: fixed or remaining-days unit price.
if ($level === 1 && $lv === 2 && $upgrade_enabled) {
$upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? ''));
foreach ($upgrade_items as $ui) {
$items[] = $ui;
}
}
if ($items === []) {
continue;
}
$level_packages[] = [
'level' => $lv,
'name' => $name,
'action' => ($level > 0 && $lv === $level) ? 'renew' : (($level > 0 && $lv > $level) ? 'upgrade' : 'pay'),
'items' => $items,
];
}
$points_packages = [];
if ($points_enabled) {
$points_packages = $this->get_points_exchange_packages($vip, $levels);
if ($level > 0) {
$points_packages = array_values(array_filter(
$points_packages,
static function (array $package) use ($level, $is_permanent): bool {
$target_level = (int) ($package['level'] ?? 0);
return $target_level > $level || (!$is_permanent && $target_level === $level);
}
));
}
}
$points_balance = ($user_id > 0 && class_exists('\AKSRT\Credits\Credits'))
? (int) \AKSRT\Credits\Credits::balance($user_id)
: 0;
$wallet_balance = ($user_id > 0 && class_exists('\AKSRT\Credits\Balance') && \AKSRT\Credits\Balance::enabled())
? (float) \AKSRT\Credits\Balance::balance($user_id)
: 0.0;
$methods = $this->available_payment_methods();
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
$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' => (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,
'points_exchange' => [
'enabled' => $points_enabled,
'desc' => (string) ($points_ex['desc'] ?? ''),
'packages' => $points_packages,
],
'payment_methods' => $methods,
'vip_url' => function_exists('aksrt_get_user_center_tab_url')
? aksrt_get_user_center_tab_url('credits')
: (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;
}
/**
* @return array|\WP_Error
*/
public function purchase(int $user_id, int $package_id, string $payment_method = 'balance') {
if ($user_id <= 0) {
return new \WP_Error('aksrt_need_login', __('请先登录后再开通会员。', 'aksrt'), ['status' => 401]);
}
$catalog = $this->get_catalog($user_id);
if (empty($catalog['enabled'])) {
return new \WP_Error('aksrt_vip_disabled', __('会员功能未启用', 'aksrt'), ['status' => 400]);
}
$pkg = $this->find_catalog_package($catalog, $package_id);
if (!$pkg) {
return new \WP_Error('aksrt_invalid_package', __('套餐不存在或当前不可购买', 'aksrt'), ['status' => 400]);
}
$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';
}
$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) {
return new \WP_Error('aksrt_invalid_price', __('套餐价格无效', 'aksrt'), ['status' => 400]);
}
if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) {
return new \WP_Error('aksrt_pay_unavailable', __('支付系统不可用', 'aksrt'), ['status' => 500]);
}
$payment_method = sanitize_key($payment_method);
if ($payment_method === '') {
$payment_method = 'balance';
}
$note = wp_json_encode([
'vip_pay' => 1,
'level' => $level,
'days' => $days,
'time' => $pkg_time,
'unit' => $pkg_unit,
'package_index' => $index,
'package_title' => (string) ($pkg['title'] ?? ''),
'package_id' => $package_id,
'action' => $action,
'upgrade_mode' => $upgrade_mode,
'base_price' => $base_price,
'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,
'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,
]);
if ($order_number === '') {
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
// 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 = [
'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]);
}
$resolved = \AKSRT\Payment\PaymentOrchestrator::resolve_gateway($payment_method);
if (empty($resolved['gateway'])) {
return new \WP_Error(
(string) ($resolved['error_code'] ?? 'aksrt_gateway_unavailable'),
(string) ($resolved['message'] ?? __('当前支付方式不可用', 'aksrt')),
['status' => 400, 'order_number' => $order_number]
);
}
$payment_result = \AKSRT\Payment\PaymentOrchestrator::pay($resolved['gateway'], $order, [
'paid_note' => __('用户完成会员开通支付。', 'aksrt'),
'order_context' => $order,
]);
if (empty($payment_result['success'])) {
return new \WP_Error(
(string) ($payment_result['code'] ?? 'aksrt_pay_failed'),
(string) ($payment_result['message'] ?? __('支付失败', 'aksrt')),
['status' => 402, 'order_number' => $order_number]
);
}
if (!empty($payment_result['redirect'])) {
return [
'success' => true,
'data' => [
'redirect' => true,
'payment_url' => (string) ($payment_result['payment_url'] ?? ''),
'qr_content' => (string) ($payment_result['qr_content'] ?? ''),
'order_number' => $order_number,
'amount' => $amount,
'message' => $payment_result['message'],
'reload' => false,
'poll' => true,
],
];
}
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'amount' => $amount,
'level' => $level,
'days' => $days,
'action' => $action,
'message' => $action === 'renew'
? __('会员续费成功', 'aksrt')
: ($action === 'upgrade' ? __('会员升级成功', 'aksrt') : __('会员开通成功', 'aksrt')),
'reload' => true,
],
];
}
/**
* Activate VIP on paid order.
*
* @param string $order_number
* @param array $paid_order
*/
public function on_order_paid(string $order_number, array $paid_order): void {
if (sanitize_key((string) ($paid_order['product_type'] ?? '')) !== \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP) {
return;
}
$user_id = (int) ($paid_order['user_id'] ?? 0);
if ($user_id <= 0) {
return;
}
$meta = [];
if (!empty($paid_order['note']) && is_string($paid_order['note'])) {
$decoded = json_decode($paid_order['note'], true);
if (is_array($decoded)) {
$meta = $decoded;
}
}
if (empty($meta) && is_array($paid_order['meta_payload'] ?? null)) {
$meta = $paid_order['meta_payload'];
}
$level = (int) ($meta['level'] ?? 0);
$days = (int) ($meta['days'] ?? 0);
$time = (int) ($meta['time'] ?? 0);
$unit = sanitize_key((string) ($meta['unit'] ?? 'day'));
if (!in_array($unit, ['day', 'month'], true)) {
$unit = 'day';
}
$upgrade_mode = sanitize_key((string) ($meta['upgrade_mode'] ?? ''));
if ($level <= 0) {
$decoded = $this->decode_package_id((int) ($paid_order['product_id'] ?? 0));
if ($decoded) {
$level = (int) $decoded['level'];
}
}
if ($level < 1) {
$level = 1;
}
if ($level > 2) {
$level = 2;
}
$previous_level = (int) get_user_meta($user_id, 'aksrt_vip_level', true);
$previous_expiry = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
$fulfilled = false;
// unit upgrade: raise level only, keep current expiry (zibll).
if ($upgrade_mode === 'unit') {
$this->activate_vip($user_id, $level, 0, true);
$fulfilled = (int) get_user_meta($user_id, 'aksrt_vip_level', true) >= $level;
} elseif ($time > 0 || (isset($meta['time']) && (int) $meta['time'] === 0 && $days === 0)) {
// Prefer calendar time+unit when present (including permanent time=0).
$this->activate_vip_duration($user_id, $level, $time, $unit);
$new_expiry = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
$expected_base = max($previous_expiry, time());
$expected_expiry = $time <= 0
? 0
: strtotime('+' . $time . ' ' . ($unit === 'month' ? 'months' : 'days'), $expected_base);
if ($expected_expiry === false) {
$expected_expiry = $expected_base + ($this->duration_to_days($time, $unit) * DAY_IN_SECONDS);
}
$fulfilled = (int) get_user_meta($user_id, 'aksrt_vip_level', true) >= max($previous_level, $level)
&& ($new_expiry === 0 || ($time <= 0 ? $new_expiry === 0 : $new_expiry >= $expected_expiry));
} else {
$this->activate_vip($user_id, $level, $days, false);
$new_expiry = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
$fulfilled = (int) get_user_meta($user_id, 'aksrt_vip_level', true) >= max($previous_level, $level)
&& ($new_expiry === 0 || $days <= 0 || $new_expiry >= max($previous_expiry, time()) + ($days * DAY_IN_SECONDS));
}
if ($fulfilled && class_exists('\AKSRT\Payment\OrderRepository')) {
\AKSRT\Payment\OrderRepository::get_instance()->update_status(
$order_number,
\AKSRT\Payment\Order::STATUS_COMPLETED,
['note' => __('会员权益已发放。', 'aksrt')]
);
}
}
/**
* Apply membership meta.
*
* @param bool $keep_expiry When true (unit upgrade), only raise level and do not change expiry.
* When days<=0 and keep_expiry=false, set permanent (expires=0).
*/
public function activate_vip(int $user_id, int $level, int $days = 0, bool $keep_expiry = false): void {
if ($user_id <= 0) {
return;
}
$level = max(1, min(2, $level));
$current_level = (int) get_user_meta($user_id, 'aksrt_vip_level', true);
$new_level = max($current_level, $level);
update_user_meta($user_id, 'aksrt_vip_level', $new_level);
// Clear expired memory when re-granting.
delete_user_meta($user_id, 'aksrt_vip_level_expired');
// Unit upgrade (zibll): keep vip_exp_date, only raise level.
if ($keep_expiry) {
return;
}
if ($days <= 0) {
// Permanent.
update_user_meta($user_id, 'aksrt_vip_expires', 0);
return;
}
$current_exp = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
if ($current_exp === 0 && $current_level > 0) {
// Already permanent — keep permanent.
return;
}
$base = max($current_exp, time());
update_user_meta($user_id, 'aksrt_vip_expires', $base + ($days * DAY_IN_SECONDS));
}
/**
* Activate VIP using calendar duration (zibll month/day).
* time=0 → permanent. Extends from max(now, current_exp).
*/
public function activate_vip_duration(int $user_id, int $level, int $time, string $unit = 'day'): void {
if ($user_id <= 0) {
return;
}
$level = max(1, min(2, $level));
$unit = $unit === 'month' ? 'month' : 'day';
$current_level = (int) get_user_meta($user_id, 'aksrt_vip_level', true);
$new_level = max($current_level, $level);
update_user_meta($user_id, 'aksrt_vip_level', $new_level);
delete_user_meta($user_id, 'aksrt_vip_level_expired');
if ($time <= 0) {
update_user_meta($user_id, 'aksrt_vip_expires', 0);
return;
}
$current_exp = (int) get_user_meta($user_id, 'aksrt_vip_expires', true);
if ($current_exp === 0 && $current_level > 0) {
return; // keep permanent
}
$base = max($current_exp, time());
$modifier = $unit === 'month'
? ('+' . $time . ' months')
: ('+' . $time . ' days');
$new_exp = strtotime($modifier, $base);
if ($new_exp === false) {
$new_exp = $base + ($this->duration_to_days($time, $unit) * DAY_IN_SECONDS);
}
update_user_meta($user_id, 'aksrt_vip_expires', (int) $new_exp);
}
/**
* Points exchange. Ordinary users can open a VIP; existing VIP users can
* renew the same level or upgrade to a higher level with the exchanged term.
*
* @return array|\WP_Error
*/
public function exchange_with_points(int $user_id, int $package_id) {
if ($user_id <= 0) {
return new \WP_Error('aksrt_need_login', __('请先登录后再兑换。', 'aksrt'), ['status' => 401]);
}
if (!class_exists('\AKSRT\Credits\Credits')) {
return new \WP_Error('aksrt_credits_unavailable', __('积分系统不可用', 'aksrt'), ['status' => 500]);
}
$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]);
}
$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]);
}
$match = null;
$packages = is_array($points_ex['packages'] ?? null) ? $points_ex['packages'] : [];
foreach ($packages as $pkg) {
if (is_array($pkg) && (int) ($pkg['package_id'] ?? 0) === $package_id) {
$match = $pkg;
break;
}
}
if (!$match) {
return new \WP_Error('aksrt_invalid_package', __('兑换套餐不存在', 'aksrt'), ['status' => 400]);
}
if ((int) ($catalog['user_level'] ?? 0) > 0) {
$current_level = (int) ($catalog['user_level'] ?? 0);
$target_level = (int) ($match['level'] ?? 0);
$is_permanent = !empty($catalog['user_is_permanent']);
if ($target_level < $current_level || ($target_level === $current_level && $is_permanent)) {
return new \WP_Error('aksrt_vip_package_unavailable', __('当前会员状态无法兑换该套餐。', 'aksrt'), ['status' => 400]);
}
}
$cost = (int) ($match['points'] ?? 0);
$level = (int) ($match['level'] ?? 1);
$days = (int) ($match['days'] ?? 0);
if ($cost <= 0) {
return new \WP_Error('aksrt_invalid_price', __('兑换积分无效', 'aksrt'), ['status' => 400]);
}
$balance = (int) ($catalog['user_points'] ?? 0);
if ($balance < $cost) {
return new \WP_Error('aksrt_credits_insufficient', __('积分不足,暂时无法兑换', 'aksrt'), [
'status' => 402,
'balance' => $balance,
'cost' => $cost,
]);
}
if (!class_exists('\AKSRT\Payment\Order') || !class_exists('\AKSRT\Payment\OrderRepository')) {
return new \WP_Error('aksrt_pay_unavailable', __('订单系统不可用', 'aksrt'), ['status' => 500]);
}
$note = wp_json_encode([
'vip_pay' => 1,
'vip_pay_type' => 'points_exchange',
'level' => $level,
'days' => $days,
'points' => $cost,
'package_id' => $package_id,
'package_title' => (string) ($match['title'] ?? ''),
'action' => 'points_exchange',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$order_number = \AKSRT\Payment\OrderRepository::get_instance()->create([
'user_id' => $user_id,
'product_type' => \AKSRT\Core\DomainModel::ORDER_PRODUCT_VIP,
'product_id' => $package_id,
'amount' => (float) $cost,
'currency' => 'points',
'status' => \AKSRT\Payment\Order::STATUS_PENDING,
'payment_method' => 'points',
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
]);
if ($order_number === '') {
return new \WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
$lock = class_exists('\AKSRT\Payment\PaymentAttemptLock')
? \AKSRT\Payment\PaymentAttemptLock::acquire($order_number)
: false;
if ($lock === false) {
return new \WP_Error('aksrt_pay_locked', __('该订单正在发起支付,请稍后重试。', 'aksrt'), ['status' => 409]);
}
try {
$deducted = \AKSRT\Credits\Credits::deduct(
$user_id,
$cost,
'vip_points_exchange',
['order' => $order_number, 'level' => $level, 'days' => $days],
sprintf(__('积分兑换会员 · %s', 'aksrt'), (string) ($match['title'] ?? ''))
);
if (is_wp_error($deducted) || $deducted === false) {
\AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_CANCELLED, [
'note' => __('积分扣除失败,自动取消', 'aksrt'),
]);
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt');
return new \WP_Error('aksrt_credits_deduct_failed', $msg ?: __('积分扣除失败', 'aksrt'), ['status' => 400]);
}
$paid = \AKSRT\Payment\Order::get_instance()->update_status($order_number, \AKSRT\Payment\Order::STATUS_PAID, [
'payment_method' => 'points',
'payment_transaction_id' => 'points_' . $order_number,
'note' => sprintf(__('积分兑换会员 %d 积分。', 'aksrt'), $cost),
'require_transition' => true,
]);
if (!$paid) {
\AKSRT\Credits\Credits::add(
$user_id,
$cost,
'vip_points_exchange_refund',
['order' => $order_number],
__('会员兑换失败退款', 'aksrt')
);
return new \WP_Error('aksrt_order_confirm_failed', __('兑换确认失败,已尝试退回积分', 'aksrt'), ['status' => 500]);
}
} finally {
\AKSRT\Payment\PaymentAttemptLock::release($order_number, $lock);
}
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'points' => $cost,
'level' => $level,
'days' => $days,
'balance' => \AKSRT\Credits\Credits::balance($user_id),
'message' => __('兑换成功', 'aksrt'),
'reload' => true,
],
];
}
}