'GET', 'callback' => [$this, 'rest_catalog'], 'permission_callback' => [$this, 'check_rest_nonce'], ], [ 'methods' => 'POST', 'callback' => [$this, 'rest_purchase'], 'permission_callback' => [$this, 'check_rest_nonce'], ], ]); } /** * @param WP_REST_Request $request * @return true|WP_Error */ public function check_rest_nonce(WP_REST_Request $request) { $nonce = $request->get_header('x-wp-nonce'); if (!$nonce) { return new WP_Error('aksrt_rest_no_nonce', __('缺少安全令牌', 'aksrt'), ['status' => 403]); } if (!wp_verify_nonce($nonce, 'wp_rest')) { return new WP_Error('aksrt_rest_bad_nonce', __('安全令牌已过期,请刷新页面', 'aksrt'), ['status' => 403]); } return true; } /** * GET /pay/vip */ public function rest_catalog(WP_REST_Request $request) { $uid = get_current_user_id(); return new WP_REST_Response($this->get_catalog($uid), 200); } /** * POST /pay/vip * body: { package_id: int, payment_method?: string } */ public function rest_purchase(WP_REST_Request $request) { $uid = get_current_user_id(); if ($uid <= 0) { return new WP_Error('aksrt_need_login', __('请先登录后再开通会员。', 'aksrt'), [ 'status' => 401, 'login_url' => function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(), ]); } $params = $request->get_json_params(); if (!is_array($params)) { $params = $request->get_params(); } $package_id = (int) ($params['package_id'] ?? 0); $payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance')); if ($payment_method === '' || $payment_method === 'cash') { $payment_method = 'balance'; } $result = $this->purchase($uid, $package_id, $payment_method); if (is_wp_error($result)) { return $result; } if (empty($result['success'])) { return new WP_Error( sanitize_key((string) ($result['code'] ?? 'aksrt_vip_pay_failed')), (string) ($result['message'] ?? __('开通失败', 'aksrt')), ['status' => (int) ($result['status'] ?? 400)] ); } return new WP_REST_Response($result, 200); } /** * @return array */ public function get_catalog(int $user_id = 0): array { $vip = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_payment_vip_settings() : ['enabled' => false, 'packages_raw' => '', 'levels' => [], 'renew' => [], 'upgrade' => []]; $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); $packages = $this->parse_packages((string) ($vip['packages_raw'] ?? '')); $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : []; $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']); $level_packages = []; foreach ([1, 2] as $lv) { if (empty($levels[$lv]['enabled'])) { continue; } // Permanent VIP at same/higher level: only show higher-level upgrade packages. if ($is_permanent && $lv <= $level) { continue; } // Existing VIP without renew: hide same-level packages. if ($level > 0 && $lv === $level && !$renew_enabled) { continue; } // Existing VIP without upgrade: hide higher-level packages. if ($level > 0 && $lv > $level && !$upgrade_enabled) { continue; } $name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv)); $items = []; foreach ($packages as $idx => $pkg) { $base_price = (float) $pkg['price']; $days = (int) $pkg['days']; // Permanent members don't renew timed packages at same level (already filtered). // Non-permanent same-level: renew pricing. $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; } $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' => (float) ($priced['show_price'] ?? 0), 'discount_tag' => (string) ($priced['tag'] ?? ''), 'days' => $days, 'title' => (string) $pkg['title'], 'price_label' => $this->format_cash_label((float) $priced['price']), 'show_price_label' => !empty($priced['show_price']) ? $this->format_cash_label((float) $priced['show_price']) : '', 'days_label' => ($days <= 0) ? __('永久', 'aksrt') : sprintf( /* translators: %d: days */ __('%d 天', 'aksrt'), $days ), 'cta_label' => $action === 'renew' ? __('立即续费', 'aksrt') : ($action === 'upgrade' ? __('立即升级', 'aksrt') : __('立即开通', 'aksrt')), ]; } // Optional dedicated upgrade permanent product line from settings.upgrade.product // Format: price|days|title (single package for VIP2 upgrade). if ($level === 1 && $lv === 2 && $upgrade_enabled) { $upgrade_pkg = $this->parse_upgrade_product_line((string) ($upgrade['product'] ?? '')); if ($upgrade_pkg) { $items[] = [ 'package_id' => $this->encode_package_id(2, 901), 'level' => 2, 'index' => 901, 'action' => 'upgrade', 'price' => (float) $upgrade_pkg['price'], 'base_price' => (float) $upgrade_pkg['price'], 'show_price' => 0.0, 'discount_tag' => __('升级专享', 'aksrt'), 'days' => (int) $upgrade_pkg['days'], 'title' => (string) $upgrade_pkg['title'], 'price_label' => $this->format_cash_label((float) $upgrade_pkg['price']), 'show_price_label' => '', 'days_label' => ((int) $upgrade_pkg['days'] <= 0) ? __('永久', 'aksrt') : sprintf(__('%d 天', 'aksrt'), (int) $upgrade_pkg['days']), 'cta_label' => __('立即升级', 'aksrt'), 'is_upgrade_product' => true, ]; } } if ($items === []) { continue; } $level_packages[] = [ 'level' => $lv, 'name' => $name, 'action' => ($level > 0 && $lv === $level) ? 'renew' : (($level > 0 && $lv > $level) ? 'upgrade' : 'pay'), 'items' => $items, ]; } $methods = $this->available_payment_methods(); return [ '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), 'renew_enabled' => $renew_enabled, 'upgrade_enabled' => $upgrade_enabled, 'renew_desc' => (string) ($renew['desc'] ?? ''), 'upgrade_desc' => (string) ($upgrade['desc'] ?? ''), 'level_packages' => $level_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, ]; } /** * @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]); } $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') ? AKSRT_Settings::get_payment_vip_settings() : ['enabled' => false, 'packages_raw' => '', 'levels' => [], 'renew' => [], 'upgrade' => []]; if (empty($vip['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]); } $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 (index 901). if ($index === 901) { $upgrade_pkg = $this->parse_upgrade_product_line((string) ($upgrade['product'] ?? '')); if (!$upgrade_pkg || $level !== 2) { return new WP_Error('aksrt_invalid_package', __('升级套餐不存在', 'aksrt'), ['status' => 400]); } $pkg = $upgrade_pkg; $amount = round((float) $pkg['price'], 2); $days = (int) $pkg['days']; $base_price = $amount; $discount_tag = __('升级专享', 'aksrt'); } else { $packages = $this->parse_packages((string) ($vip['packages_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']; $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'] ?? ''); } if ($amount <= 0) { return new WP_Error('aksrt_invalid_price', __('套餐价格无效', 'aksrt'), ['status' => 400]); } if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) { 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, 'package_index' => $index, 'package_title' => (string) ($pkg['title'] ?? ''), 'package_id' => $package_id, 'action' => $action, 'base_price' => $base_price, 'discount_tag' => $discount_tag, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $order_number = AKSRT_Order_Repository::get_instance()->create([ 'user_id' => $user_id, 'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_VIP, 'product_id' => $package_id, 'amount' => $amount, 'currency' => 'CNY', 'status' => AKSRT_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), ]); if ($order_number === '') { return new WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]); } $order = AKSRT_Order_Repository::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)); $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; if (!class_exists('AKSRT_Payment_Gateway')) { return new WP_Error('aksrt_pay_unavailable', __('支付网关不可用', 'aksrt'), ['status' => 500]); } $gateway = AKSRT_Payment_Gateway::get_instance()->get_gateway($payment_method); if (!$gateway) { return new WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用', 'aksrt'), [ 'status' => 400, 'order_number' => $order_number, ]); } if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) { return new WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), [ 'status' => 400, 'order_number' => $order_number, ]); } $payment_result = $gateway->create_payment($order); if (empty($payment_result['success'])) { $msg = (string) ($payment_result['message'] ?? __('支付失败', 'aksrt')); $code = ($payment_method === 'balance' && (str_contains($msg, '余额') || str_contains($msg, '不足'))) ? 'aksrt_balance_insufficient' : 'aksrt_pay_failed'; return new WP_Error($code, $msg, [ 'status' => 402, 'order_number' => $order_number, ]); } $code_url = (string) ($payment_result['payment_data']['code_url'] ?? ''); $payment_url = (string) ($payment_result['payment_url'] ?? ''); if ($payment_url !== '' || $code_url !== '') { return [ 'success' => true, 'data' => [ 'redirect' => true, 'payment_url' => $payment_url, 'qr_content' => $code_url, 'order_number' => $order_number, 'amount' => $amount, 'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'), 'reload' => false, 'poll' => true, ], ]; } $paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [ 'payment_method' => $payment_method, 'payment_transaction_id' => sanitize_text_field((string) ($payment_result['transaction_id'] ?? '')), 'note' => __('用户完成会员开通支付。', 'aksrt'), ]); if (!$paid) { return new WP_Error('aksrt_order_confirm_failed', __('订单确认失败', 'aksrt'), ['status' => 500]); } 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_Domain_Model::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); 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; } $this->activate_vip($user_id, $level, $days); } /** * Apply membership meta. */ public function activate_vip(int $user_id, int $level, int $days = 0): 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); 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)); } /** * Parse packages_raw lines: price|days|title * * @return array */ public function parse_packages(string $raw): array { $lines = preg_split('/\r\n|\r|\n/', $raw) ?: []; $out = []; foreach ($lines as $line) { $line = trim((string) $line); if ($line === '') { continue; } $parts = array_map('trim', explode('|', $line)); $price = isset($parts[0]) ? (float) $parts[0] : 0.0; $days = isset($parts[1]) ? (int) $parts[1] : 0; $title = isset($parts[2]) ? sanitize_text_field($parts[2]) : ''; if ($price <= 0) { continue; } if ($title === '') { $title = $days > 0 ? sprintf(__('%d天会员', 'aksrt'), $days) : __('永久会员', 'aksrt'); } $out[] = [ 'price' => round($price, 2), 'days' => max(0, $days), 'title' => $title, ]; } return $out; } /** * Single upgrade product line: price|days|title * * @return array{price:float,days:int,title:string}|null */ public function parse_upgrade_product_line(string $raw): ?array { $raw = trim($raw); if ($raw === '') { return null; } // Allow multiline; take first valid. $lines = preg_split('/\r\n|\r|\n/', $raw) ?: []; foreach ($lines as $line) { $parsed = $this->parse_packages((string) $line); if (!empty($parsed[0])) { return $parsed[0]; } } return null; } /** * Apply renew discount/reduce to same-level packages. * * @param array $renew * @param array $upgrade * @return array{price:float,show_price:float,tag:string}|null */ private function apply_action_price( float $base_price, string $action, array $renew, array $upgrade, int $target_level, int $user_level, int $expires, bool $is_permanent, int $days ): ?array { if ($base_price <= 0) { return null; } // Permanent same-level packages never make sense for renew. if ($action === 'renew' && $is_permanent) { return null; } if ($action !== 'renew') { return [ 'price' => round($base_price, 2), 'show_price' => 0.0, 'tag' => '', ]; } $type = sanitize_key((string) ($renew['price_type'] ?? 'discount')); $price = $base_price; $show = $base_price; $tag = ''; if ($type === 'discount') { // Settings store "8" meaning 8折 (80%) when <= 10; or percent when > 10. $discount = (float) ($renew['discount'] ?? 8); if ($discount <= 0) { $discount = 8; } if ($discount <= 10) { $price = round($base_price * ($discount / 10), 2); $tag = sprintf( /* translators: %s: discount like 8 */ __('%s折续费', 'aksrt'), rtrim(rtrim(number_format($discount, 1, '.', ''), '0'), '.') ); } else { $price = round($base_price * (min(100, $discount) / 100), 2); $tag = sprintf(__('%s%% 续费价', 'aksrt'), (string) (int) $discount); } } elseif ($type === 'reduce') { $reduce = max(0, (float) ($renew['reduce'] ?? 20)); $price = round(max(0.01, $base_price - $reduce), 2); $tag = sprintf( /* translators: %s: amount */ __('续费立减%s', 'aksrt'), $this->format_cash_label($reduce) ); } else { // same $show = 0.0; $tag = __('续费同价', 'aksrt'); } if ($price < 0.01) { $price = 0.01; } if ($show > 0 && $price >= $show) { $show = 0.0; $tag = __('续费', 'aksrt'); } return [ 'price' => $price, 'show_price' => $show > $price ? $show : 0.0, 'tag' => $tag, ]; } public function encode_package_id(int $level, int $index): int { return (max(1, min(2, $level)) * 1000) + max(1, $index); } /** * @return array{level:int,index:int}|null */ public function decode_package_id(int $package_id): ?array { if ($package_id < 1001) { return null; } $level = intdiv($package_id, 1000); $index = $package_id % 1000; if ($level < 1 || $level > 2 || $index < 1) { return null; } return ['level' => $level, 'index' => $index]; } /** * @return array */ private function available_payment_methods(): array { $methods = []; if (!class_exists('AKSRT_Payment_Gateway')) { return [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]]; } $gw = AKSRT_Payment_Gateway::get_instance(); foreach (['balance', 'alipay', 'wechatpay', 'paypal', 'stripe'] as $id) { $g = $gw->get_gateway($id); if (!$g) { continue; } if (method_exists($g, 'is_configured') && !$g->is_configured()) { continue; } $methods[] = [ 'id' => $id, 'name' => method_exists($g, 'get_name') ? (string) $g->get_name() : $id, ]; } if ($methods === []) { $methods[] = ['id' => 'balance', 'name' => __('站内余额', 'aksrt')]; } return $methods; } private function format_cash_label(float $amount): string { $symbol = '¥'; if (class_exists('AKSRT_Settings')) { $paid = AKSRT_Settings::get_paid_content_settings(); $symbol = (string) ($paid['display']['currency_symbol'] ?? '¥'); } $n = function_exists('aksrt_format_price') ? aksrt_format_price($amount) : (string) $amount; return $symbol . $n; } private function format_expires_label(int $expires, int $level): string { if ($level <= 0) { return __('未开通', 'aksrt'); } if ($expires <= 0) { return __('永久有效', 'aksrt'); } if ($expires < time()) { return __('已过期', 'aksrt'); } return sprintf( /* translators: %s: date */ __('到期时间:%s', 'aksrt'), wp_date('Y-m-d H:i', $expires) ); } }