\\d+)', [ [ 'methods' => 'GET', 'callback' => [$this, 'rest_preview'], 'permission_callback' => [$this, 'check_rest_nonce'], ], [ 'methods' => 'POST', 'callback' => [$this, 'rest_purchase'], 'permission_callback' => [$this, 'check_rest_nonce'], ], ]); } /** * @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; } public function rest_preview(WP_REST_Request $request) { $plate_id = (int) $request['id']; $uid = get_current_user_id(); $preview = $this->preview($plate_id, $uid); if (is_wp_error($preview)) { return $preview; } return new WP_REST_Response($preview, 200); } public function rest_purchase(WP_REST_Request $request) { $plate_id = (int) $request['id']; $uid = get_current_user_id(); if ($uid <= 0) { $post = get_post($plate_id); return new WP_Error('aksrt_need_login', __('请先登录后再关注。', 'aksrt'), [ 'status' => 401, 'login_url' => wp_login_url($post ? get_permalink($post) : home_url('/')), ]); } $params = $request->get_json_params(); if (!is_array($params)) { $params = $request->get_params(); } $payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance')); if ($payment_method === '' || $payment_method === 'cash') { $payment_method = 'balance'; } return $this->rest_wrap_result( $this->create_cash_order($plate_id, $uid, $payment_method) ); } /** * @param mixed $result * @return WP_REST_Response|WP_Error */ private function rest_wrap_result($result) { if (is_wp_error($result)) { return $result; } if (empty($result['success'])) { $code = sanitize_key((string) ($result['code'] ?? 'aksrt_pay_failed')); $status = (int) ($result['status'] ?? 400); return new WP_Error( $code !== '' ? $code : 'aksrt_pay_failed', (string) ($result['message'] ?? __('支付失败', 'aksrt')), array_merge( ['status' => $status > 0 ? $status : 400], is_array($result['data'] ?? null) ? $result['data'] : [] ) ); } return new WP_REST_Response($result, 200); } /** * @return array|WP_Error */ public function preview(int $plate_id, int $user_id = 0) { $assert = $this->assert_plate($plate_id); if (is_wp_error($assert)) { return $assert; } /** @var WP_Post $post */ $post = $assert['post']; $uid = $user_id > 0 ? $user_id : get_current_user_id(); if ($uid <= 0) { return [ 'post_id' => $plate_id, 'title' => get_the_title($post), 'permalink' => get_permalink($post), 'pay_modo' => '0', 'pay_type' => 'plate_follow', 'pay_type_label' => __('付费关注', 'aksrt'), 'amount' => 0, 'base' => 0, 'list_amount' => 0, 'payable' => 0, 'currency' => '¥', 'amount_label' => '', 'base_label' => '', 'list_amount_label' => '', 'is_free' => false, 'has_paid_access' => false, 'need_login' => true, 'allow_guest' => false, 'need_vip' => false, 'need_purchase' => true, 'logged_in' => false, 'balance' => 0, 'balance_enough' => false, 'payment_methods' => [], 'login_url' => wp_login_url(get_permalink($post)), 'vip_url' => home_url('/'), 'credits_url' => home_url('/'), 'coupon_allowed' => false, 'coupon_desc' => '', 'coupon_code' => '', 'coupon' => null, 'coupon_error' => '', 'coupon_discount' => 0, 'coupon_discount_label' => '', 'rebate_discount' => 0, 'rebate_discount_label' => '', 'points_deduction_allowed' => false, 'card_pass_allowed' => false, 'price_type' => '', ]; } $priced = $this->resolve_price($plate_id, $uid); if (($priced['modo'] ?? 'money') === 'points') { return new WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]); } $amount = round((float) ($priced['amount'] ?? 0), 2); $base = round((float) ($priced['base'] ?? $amount), 2); $is_free = !empty($priced['is_free']) || $amount <= 0; $has_followed = $this->user_has_followed($uid, $plate_id); $balance = 0; if (class_exists('AKSRT_Credits')) { $balance = (int) AKSRT_Credits::balance($uid); } $methods = function_exists('aksrt_list_cash_payment_methods') ? aksrt_list_cash_payment_methods(true) : [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]]; $amount_label = $is_free ? __('免费', 'aksrt') : sprintf('¥%s', number_format($amount, 2, '.', '')); $base_label = sprintf('¥%s', number_format($base, 2, '.', '')); return [ 'post_id' => $plate_id, 'title' => get_the_title($post), 'permalink' => get_permalink($post), 'pay_modo' => '0', 'pay_type' => 'plate_follow', 'pay_type_label' => __('付费关注', 'aksrt'), 'amount' => $amount, 'base' => $base, 'list_amount' => $amount, 'list_amount_label' => $amount_label, 'payable' => $amount, 'currency' => '¥', 'amount_label' => $amount_label, 'base_label' => $base_label, 'is_free' => $is_free, 'has_paid_access' => $has_followed, 'paid_order' => null, 'pending_order' => null, 'need_login' => false, 'allow_guest' => false, 'need_vip' => false, 'need_purchase' => !$has_followed && !$is_free, 'logged_in' => true, 'balance' => $balance, 'balance_enough' => true, 'payment_methods' => $methods, 'login_url' => wp_login_url(get_permalink($post)), 'vip_url' => home_url('/'), 'credits_url' => home_url('/'), 'coupon_allowed' => false, 'coupon_desc' => '', 'coupon_code' => '', 'coupon' => null, 'coupon_error' => '', 'coupon_discount' => 0, 'coupon_discount_label' => '', 'rebate_discount' => 0, 'rebate_discount_label' => '', 'points_deduction_allowed' => false, 'card_pass_allowed' => false, 'price_type' => '', 'vip_level' => (int) ($priced['vip_level'] ?? 0), 'is_vip_price' => !empty($priced['is_vip_price']), ]; } /** * @return array|WP_Error */ public function create_cash_order(int $plate_id, int $user_id, string $payment_method = 'balance') { if ($user_id <= 0) { return new WP_Error('aksrt_need_login', __('请先登录后再关注。', 'aksrt'), ['status' => 401]); } $assert = $this->assert_plate($plate_id); if (is_wp_error($assert)) { return $assert; } /** @var WP_Post $post */ $post = $assert['post']; if ($this->user_has_followed($user_id, $plate_id)) { return new WP_Error('aksrt_already_followed', __('已关注该版块。', 'aksrt'), ['status' => 400]); } $priced = $this->resolve_price($plate_id, $user_id); if (($priced['modo'] ?? 'money') === 'points') { return new WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]); } $amount = round((float) ($priced['amount'] ?? 0), 2); $base = round((float) ($priced['base'] ?? $amount), 2); $is_free = !empty($priced['is_free']) || $amount <= 0; if ($is_free) { return $this->create_free_paid_order($plate_id, $user_id, $post, $priced); } if ($amount <= 0) { return new WP_Error('aksrt_price_unset', __('价格未设置', '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'; } $pending = $this->find_pending_order($user_id, $plate_id); $can_resume = $pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001; if ($can_resume) { $order_number = (string) ($pending['order_number'] ?? ''); $order = $pending; } else { $note = wp_json_encode([ 'plate_follow_pay' => 1, 'plate_id' => $plate_id, 'pay_modo' => '0', 'vip_level' => (int) ($priced['vip_level'] ?? 0), 'base' => $base, 'list_amount' => $amount, 'amount' => $amount, 'payable' => $amount, 'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''), ], 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_PLATE_FOLLOW, 'product_id' => $plate_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]); } } $order['subject'] = sprintf( /* translators: %s: plate title */ __('付费关注版块「%s」', 'aksrt'), get_the_title($post) ); $order['payment_method'] = $payment_method; $order['amount'] = $amount; if (!class_exists('AKSRT_Payment_Gateway')) { return new WP_Error('aksrt_pay_unavailable', __('支付网关不可用。', 'aksrt'), ['status' => 500]); } $gateway_manager = AKSRT_Payment_Gateway::get_instance(); $gateway = $gateway_manager->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, 'pay_modo' => '0', '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]); } $this->mark_followed($user_id, $plate_id); return [ 'success' => true, 'data' => [ 'order_number' => $order_number, 'amount' => $amount, 'pay_modo' => '0', 'message' => $payment_result['message'] ?? __('关注成功', 'aksrt'), 'reload' => true, ], ]; } public function on_order_paid(string $order_number, array $paid_order): void { $product_type = sanitize_key((string) ($paid_order['product_type'] ?? '')); if ($product_type !== AKSRT_Domain_Model::ORDER_PRODUCT_PLATE_FOLLOW) { return; } $plate_id = (int) ($paid_order['product_id'] ?? 0); $user_id = (int) ($paid_order['user_id'] ?? 0); if ($plate_id <= 0 || $user_id <= 0) { return; } $meta = $this->decode_order_note_meta($paid_order); if (empty($meta['plate_follow_pay'])) { $post = get_post($plate_id); if (!($post instanceof WP_Post) || $post->post_type !== 'aksrt_forum_plate') { return; } } $this->mark_followed($user_id, $plate_id); } /** * @return array{post:WP_Post}|WP_Error */ private function assert_plate(int $plate_id) { $post = get_post($plate_id); if (!($post instanceof WP_Post) || $post->post_type !== 'aksrt_forum_plate') { return new WP_Error('aksrt_invalid', __('版块不存在', 'aksrt'), ['status' => 404]); } if ($post->post_status !== 'publish') { return new WP_Error('aksrt_invalid', __('版块不可关注', 'aksrt'), ['status' => 404]); } if (!$this->is_follow_pay_enabled($plate_id)) { return new WP_Error('aksrt_not_paid', __('该版块未开启付费关注', 'aksrt'), ['status' => 400]); } $modo = sanitize_key((string) (get_post_meta($plate_id, '_aksrt_follow_pay_modo', true) ?: 'money')); if ($modo === 'points') { return new WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]); } return ['post' => $post]; } private function is_follow_pay_enabled(int $plate_id): bool { if (class_exists('AKSRT_Forum') && method_exists('AKSRT_Forum', 'is_plate_follow_pay_enabled')) { return (bool) AKSRT_Forum::get_instance()->is_plate_follow_pay_enabled($plate_id); } return (int) get_post_meta($plate_id, '_aksrt_follow_pay', true) === 1; } /** * @return array{amount:float,modo:string,is_vip_price:bool,is_free:bool,label:string,base:float,vip_level:int} */ private function resolve_price(int $plate_id, int $user_id): array { if (class_exists('AKSRT_Forum') && method_exists('AKSRT_Forum', 'resolve_plate_follow_pay_price')) { return AKSRT_Forum::get_instance()->resolve_plate_follow_pay_price($plate_id, $user_id); } return [ 'amount' => max(0.0, (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true)), 'modo' => 'money', 'is_vip_price' => false, 'is_free' => false, 'label' => '', 'base' => max(0.0, (float) get_post_meta($plate_id, '_aksrt_follow_pay_price', true)), 'vip_level' => 0, ]; } private function user_has_followed(int $user_id, int $plate_id): bool { if ($user_id <= 0 || $plate_id <= 0) { return false; } if (class_exists('AKSRT_Forum') && method_exists('AKSRT_Forum', 'is_user_following_plate')) { return (bool) AKSRT_Forum::get_instance()->is_user_following_plate($user_id, $plate_id); } $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); if (!is_array($follow_list)) { return false; } return in_array($plate_id, array_map('intval', $follow_list), true); } private function mark_followed(int $user_id, int $plate_id): void { if (class_exists('AKSRT_Forum') && method_exists('AKSRT_Forum', 'mark_plate_followed')) { AKSRT_Forum::get_instance()->mark_plate_followed($user_id, $plate_id); return; } if ($user_id <= 0 || $plate_id <= 0) { return; } $follow_list = get_user_meta($user_id, 'aksrt_follow_plate', true); if (!is_array($follow_list)) { $follow_list = []; } $follow_list[] = $plate_id; update_user_meta($user_id, 'aksrt_follow_plate', array_values(array_unique(array_map('intval', $follow_list)))); } /** * @return array|WP_Error */ private function create_free_paid_order(int $plate_id, int $user_id, WP_Post $post, array $priced) { if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) { $this->mark_followed($user_id, $plate_id); return [ 'success' => true, 'data' => [ 'order_number' => '', 'amount' => 0, 'pay_modo' => '0', 'free' => true, 'message' => __('VIP 免费关注成功', 'aksrt'), 'reload' => true, ], ]; } $note = wp_json_encode([ 'plate_follow_pay' => 1, 'plate_id' => $plate_id, 'pay_modo' => '0', 'vip_level' => (int) ($priced['vip_level'] ?? 0), 'base' => (float) ($priced['base'] ?? 0), 'list_amount' => 0, 'amount' => 0, 'payable' => 0, 'free' => 1, 'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''), ], 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_PLATE_FOLLOW, 'product_id' => $plate_id, 'amount' => 0.0, 'currency' => 'CNY', 'status' => AKSRT_Order::STATUS_PENDING, 'payment_method' => 'free', 'note' => is_string($note) ? $note : '', 'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS), ]); if ($order_number === '') { $this->mark_followed($user_id, $plate_id); return [ 'success' => true, 'data' => [ 'order_number' => '', 'amount' => 0, 'free' => true, 'message' => __('VIP 免费关注成功', 'aksrt'), 'reload' => true, ], ]; } AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [ 'payment_method' => 'free', 'payment_transaction_id' => 'free_' . $order_number, 'note' => __('VIP 免费关注版块。', 'aksrt'), ]); $this->mark_followed($user_id, $plate_id); return [ 'success' => true, 'data' => [ 'order_number' => $order_number, 'amount' => 0, 'pay_modo' => '0', 'free' => true, 'message' => __('VIP 免费关注成功', 'aksrt'), 'reload' => true, ], ]; } private function find_pending_order(int $user_id, int $plate_id): ?array { if ($user_id <= 0 || $plate_id <= 0 || !class_exists('AKSRT_Order_Repository')) { return null; } $orders = AKSRT_Order_Repository::get_instance()->get_user_orders($user_id, [ 'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_PLATE_FOLLOW, 'status' => AKSRT_Domain_Model::ORDER_STATUS_PENDING, 'limit' => 20, 'offset' => 0, ]); foreach ($orders as $order) { if ((int) ($order['product_id'] ?? 0) !== $plate_id) { continue; } $meta = $this->decode_order_note_meta($order); if (!empty($meta['plate_follow_pay'])) { return $order; } return $order; } return null; } private function decode_order_note_meta(array $order): array { if (isset($order['meta_payload']) && is_array($order['meta_payload']) && $order['meta_payload'] !== []) { return $order['meta_payload']; } $note = (string) ($order['note'] ?? ''); if ($note === '') { return []; } $decoded = json_decode($note, true); return is_array($decoded) ? $decoded : []; } }