diff --git a/assets/js/cms-download.js b/assets/js/cms-download.js index aab2de9d..19a9d577 100644 --- a/assets/js/cms-download.js +++ b/assets/js/cms-download.js @@ -413,7 +413,7 @@ function renderCashier(preview, returnFocusEl) { if (!preview) return; - if (preview.need_login) { + if (preview.need_login && !preview.allow_guest) { renderError('', { login_url: preview.login_url }, returnFocusEl, 'aksrt_need_login'); return; } diff --git a/inc/classes/class-aksrt-content-pay.php b/inc/classes/class-aksrt-content-pay.php index dbbfcec1..f72252e1 100644 --- a/inc/classes/class-aksrt-content-pay.php +++ b/inc/classes/class-aksrt-content-pay.php @@ -84,13 +84,6 @@ class AKSRT_Content_Pay { public function rest_purchase(WP_REST_Request $request) { $post_id = (int) $request['id']; $uid = get_current_user_id(); - if ($uid <= 0) { - $post = get_post($post_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)) { @@ -102,6 +95,18 @@ class AKSRT_Content_Pay { $price_type = ''; } + // Guest allowed only for cash + pay_limit=0 + setting on. + if ($uid <= 0) { + $post = get_post($post_id); + $allow_guest = function_exists('aksrt_allow_guest_content_pay') && aksrt_allow_guest_content_pay($post); + if (!$allow_guest) { + return new WP_Error('aksrt_need_login', __('请先登录后再购买。', 'aksrt'), [ + 'status' => 401, + 'login_url' => wp_login_url($post ? get_permalink($post) : home_url('/')), + ]); + } + } + $gate = $this->assert_purchasable($post_id, $uid, $price_type); if (is_wp_error($gate)) { return $gate; @@ -115,6 +120,9 @@ class AKSRT_Content_Pay { $mode = $pay_modo === 'points' ? 'points' : 'cash'; } if ($mode === 'points' || $pay_modo === 'points') { + if ($uid <= 0) { + return new WP_Error('aksrt_need_login', __('积分支付请先登录。', 'aksrt'), ['status' => 401]); + } return $this->rest_wrap_result($this->create_and_pay_points($post_id, $uid, $price_type)); } @@ -122,6 +130,10 @@ class AKSRT_Content_Pay { if ($payment_method === '' || $payment_method === 'cash') { $payment_method = 'balance'; } + // Guests cannot use balance/points gateways. + if ($uid <= 0 && in_array($payment_method, ['balance', 'points'], true)) { + $payment_method = 'alipay'; + } return $this->rest_wrap_result($this->create_cash_order($post_id, $uid, $payment_method, $price_type)); } @@ -130,19 +142,30 @@ class AKSRT_Content_Pay { */ public function rest_order_status(WP_REST_Request $request) { $uid = get_current_user_id(); - if ($uid <= 0) { - return new WP_Error('aksrt_need_login', __('请先登录。', 'aksrt'), ['status' => 401]); - } $order_number = sanitize_text_field((string) $request['order_number']); if ($order_number === '' || !class_exists('AKSRT_Order_Repository')) { return new WP_Error('aksrt_invalid', __('订单不存在', 'aksrt'), ['status' => 404]); } - $order = AKSRT_Order_Repository::get_instance()->get_user_order($uid, $order_number); + $repo = AKSRT_Order_Repository::get_instance(); + if ($uid > 0) { + $order = $repo->get_user_order($uid, $order_number); + } else { + $order = $repo->get($order_number); + // Guest may only poll own guest content orders (user_id=0 + resource). + if ($order && ((int) ($order['user_id'] ?? -1) !== 0 + || (string) ($order['product_type'] ?? '') !== AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE)) { + $order = null; + } + } if (!$order) { return new WP_Error('aksrt_invalid', __('订单不存在', 'aksrt'), ['status' => 404]); } $status = (string) ($order['status'] ?? ''); $paid = class_exists('AKSRT_Domain_Model') && AKSRT_Domain_Model::is_paid_order_status($status); + if ($paid && $uid <= 0 && function_exists('aksrt_set_guest_content_pay_cookies')) { + // Ensure cookie set even if notify raced ahead of browser cookie. + aksrt_set_guest_content_pay_cookies($order); + } return new WP_REST_Response([ 'order_number' => $order_number, 'status' => $status, @@ -182,6 +205,19 @@ class AKSRT_Content_Pay { $cur = (int) get_post_meta($post_id, $key, true); update_post_meta($post_id, $key, $cur + 1); } + + // Guest cookie unlock (user_id=0). + $uid = (int) ($paid_order['user_id'] ?? 0); + if ($uid <= 0 && function_exists('aksrt_set_guest_content_pay_cookies')) { + // Ensure full order row for token material. + if (class_exists('AKSRT_Order_Repository')) { + $full = AKSRT_Order_Repository::get_instance()->get($order_number); + if (is_array($full)) { + $paid_order = array_merge($full, $paid_order); + } + } + aksrt_set_guest_content_pay_cookies($paid_order); + } } /** @@ -244,6 +280,13 @@ class AKSRT_Content_Pay { $paid_order = aksrt_get_user_content_paid_order($post_id, $uid); } } + if (!$has_paid && $uid <= 0 && function_exists('aksrt_get_guest_content_paid_order')) { + $guest_order = aksrt_get_guest_content_paid_order($post_id); + if ($guest_order) { + $has_paid = true; + $paid_order = $guest_order; + } + } $pending_order = null; if ($uid > 0 && !$has_paid && class_exists('AKSRT_Order_Repository')) { @@ -251,12 +294,13 @@ class AKSRT_Content_Pay { } $need_vip = $pay_limit > 0 && $vip_level < $pay_limit; + $allow_guest = function_exists('aksrt_allow_guest_content_pay') && aksrt_allow_guest_content_pay($post); $currency = (string) ($priced['currency'] ?? ($pay_modo === 'points' ? (class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt')) : '¥')); $balance = ($uid > 0 && class_exists('AKSRT_Credits')) ? AKSRT_Credits::balance($uid) : 0; - $methods = $this->available_payment_methods($pay_modo); + $methods = $this->available_payment_methods($pay_modo, $uid > 0); $amount_label = $this->format_amount_label($amount, $pay_modo, $currency); $base_label = $this->format_amount_label($base, $pay_modo, $currency); @@ -304,7 +348,8 @@ class AKSRT_Content_Pay { 'payment_method' => (string) ($pending_order['payment_method'] ?? ''), 'created_at' => (string) ($pending_order['created_at'] ?? ''), ] : null, - 'need_login' => $uid <= 0, + 'need_login' => $uid <= 0 && !$allow_guest, + 'allow_guest' => $allow_guest, 'need_vip' => $need_vip, 'need_purchase' => !$has_paid && !$need_vip && $amount > 0, 'logged_in' => $uid > 0, @@ -468,17 +513,20 @@ class AKSRT_Content_Pay { return new WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]); } - // Resume existing pending content order for this post when amount matches. - $pending = $this->find_pending_content_order($user_id, $post_id); + $payment_method = sanitize_key($payment_method); + if ($payment_method === '') { + $payment_method = $user_id > 0 ? 'balance' : 'alipay'; + } + if ($user_id <= 0 && in_array($payment_method, ['balance', 'points'], true)) { + $payment_method = 'alipay'; + } + + // Resume existing pending content order for this post when amount matches (logged-in only). + $pending = $user_id > 0 ? $this->find_pending_content_order($user_id, $post_id) : null; if ($pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001) { $order_number = (string) ($pending['order_number'] ?? ''); $order = $pending; } else { - $payment_method = sanitize_key($payment_method); - if ($payment_method === '') { - $payment_method = 'balance'; - } - $note = wp_json_encode([ 'content_pay' => 1, 'pay_modo' => '0', @@ -487,10 +535,12 @@ class AKSRT_Content_Pay { 'vip_level' => (int) ($priced['vip_level'] ?? 0), 'base' => (float) ($priced['base'] ?? $amount), 'amount' => $amount, + 'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''), + 'guest' => $user_id <= 0 ? 1 : 0, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $order_number = AKSRT_Order_Repository::get_instance()->create([ - 'user_id' => $user_id, + 'user_id' => max(0, $user_id), 'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE, 'product_id' => $post_id, 'amount' => $amount, @@ -513,7 +563,7 @@ class AKSRT_Content_Pay { $payment_method = sanitize_key($payment_method ?: (string) ($order['payment_method'] ?? 'balance')); if ($payment_method === '') { - $payment_method = 'balance'; + $payment_method = $user_id > 0 ? 'balance' : 'alipay'; } $order['subject'] = sprintf( @@ -603,12 +653,6 @@ class AKSRT_Content_Pay { if (!($post instanceof WP_Post) || $post->post_status !== 'publish') { return new WP_Error('aksrt_invalid', __('内容不存在', 'aksrt'), ['status' => 404]); } - if ($user_id <= 0) { - return new WP_Error('aksrt_need_login', __('请先登录后再购买。', 'aksrt'), [ - 'status' => 401, - 'login_url' => wp_login_url(get_permalink($post)), - ]); - } $meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post) : []; $pay_type = AKSRT_Domain_Model::normalize_pay_type((string) ($meta['pay_type'] ?? AKSRT_Domain_Model::PAY_TYPE_NO)); @@ -619,6 +663,17 @@ class AKSRT_Content_Pay { return new WP_Error('aksrt_need_vip', __('该内容为 VIP 专属,请开通会员。', 'aksrt'), ['status' => 403]); } + $pay_modo_meta = (($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0'; + $allow_guest = function_exists('aksrt_allow_guest_content_pay') && aksrt_allow_guest_content_pay($post); + if ($user_id <= 0) { + if ($pay_modo_meta === 'points' || !$allow_guest) { + return new WP_Error('aksrt_need_login', __('请先登录后再购买。', 'aksrt'), [ + 'status' => 401, + 'login_url' => wp_login_url(get_permalink($post)), + ]); + } + } + $priced = function_exists('aksrt_get_user_pay_price') ? aksrt_get_user_pay_price($post, $user_id) : ['amount' => 0.0, 'modo' => '0', 'pay_limit' => 0, 'vip_level' => 0, 'is_free' => true, 'base' => 0.0]; @@ -648,7 +703,7 @@ class AKSRT_Content_Pay { ]); } - if (class_exists('AKSRT_Order') && AKSRT_Order::get_instance()->has_paid_access( + if ($user_id > 0 && class_exists('AKSRT_Order') && AKSRT_Order::get_instance()->has_paid_access( $user_id, AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE, $post_id @@ -659,6 +714,13 @@ class AKSRT_Content_Pay { 'reload' => true, ]); } + if ($user_id <= 0 && function_exists('aksrt_guest_has_paid_access') && aksrt_guest_has_paid_access($post_id)) { + return new WP_Error('aksrt_already_paid', __('您已购买该内容。', 'aksrt'), [ + 'status' => 200, + 'has_paid_access' => true, + 'reload' => true, + ]); + } return [ 'post' => $post, @@ -691,7 +753,7 @@ class AKSRT_Content_Pay { /** * @return array */ - private function available_payment_methods(string $pay_modo): array { + private function available_payment_methods(string $pay_modo, bool $logged_in = true): array { if ($pay_modo === 'points') { return [ ['id' => 'points', 'name' => __('积分支付', 'aksrt')], @@ -699,11 +761,16 @@ class AKSRT_Content_Pay { } $methods = []; + $ids = $logged_in + ? ['balance', 'alipay', 'wechatpay', 'paypal', 'stripe'] + : ['alipay', 'wechatpay', 'paypal', 'stripe']; if (!class_exists('AKSRT_Payment_Gateway')) { - return [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]]; + return $logged_in + ? [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]] + : [['id' => 'alipay', 'name' => __('支付宝', 'aksrt')]]; } $gw = AKSRT_Payment_Gateway::get_instance(); - foreach (['balance', 'alipay', 'wechatpay', 'paypal', 'stripe'] as $id) { + foreach ($ids as $id) { $g = $gw->get_gateway($id); if (!$g) { continue; @@ -717,7 +784,9 @@ class AKSRT_Content_Pay { ]; } if ($methods === []) { - $methods[] = ['id' => 'balance', 'name' => __('站内余额', 'aksrt')]; + $methods[] = $logged_in + ? ['id' => 'balance', 'name' => __('站内余额', 'aksrt')] + : ['id' => 'alipay', 'name' => __('支付宝', 'aksrt')]; } return $methods; } diff --git a/inc/classes/class-aksrt-content-visibility.php b/inc/classes/class-aksrt-content-visibility.php index 02b1fae2..c00a2c6e 100644 --- a/inc/classes/class-aksrt-content-visibility.php +++ b/inc/classes/class-aksrt-content-visibility.php @@ -100,6 +100,10 @@ class AKSRT_Content_Visibility { (int) $resource_post->ID ); } + // Guest cookie paid unlock (Zibll pay_no_logged_in). + if (!$has_paid_order && $uid <= 0 && function_exists('aksrt_guest_has_paid_access')) { + $has_paid_order = aksrt_guest_has_paid_access((int) $resource_post->ID); + } $has_vip_access = false; $user_vip_level = is_array($priced) ? (int) ($priced['vip_level'] ?? 0) : 0; diff --git a/inc/classes/class-aksrt-order.php b/inc/classes/class-aksrt-order.php index 134af951..5345105c 100644 --- a/inc/classes/class-aksrt-order.php +++ b/inc/classes/class-aksrt-order.php @@ -48,7 +48,8 @@ class AKSRT_Order { } public function create(int $user_id, string $product_type, int $product_id, float $amount, string $currency = 'CNY'): string { - if ($user_id <= 0 || $product_id < 0 || $amount < 0) { + // Guest content orders may use user_id=0 (Zibll pay_no_logged_in). + if ($user_id < 0 || $product_id < 0 || $amount < 0) { return ''; } diff --git a/inc/helpers/aksrt-pay.php b/inc/helpers/aksrt-pay.php index 3ae9a031..199dba38 100644 --- a/inc/helpers/aksrt-pay.php +++ b/inc/helpers/aksrt-pay.php @@ -237,6 +237,177 @@ if (!function_exists('aksrt_bump_content_sales_volume')) { } } +/** + * Whether guest cash purchase is allowed (Zibll pay_no_logged_in + pay_limit=0). + */ +if (!function_exists('aksrt_allow_guest_content_pay')) { + function aksrt_allow_guest_content_pay($post = null): bool { + $p = get_post($post); + if (!($p instanceof WP_Post)) { + return false; + } + $meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($p) : []; + $pay_limit = (int) ($meta['pay_limit'] ?? 0); + if ($pay_limit > 0) { + return false; + } + $pay_modo = (($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0'; + if ($pay_modo === 'points') { + return false; // points require login + } + $order = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_order_settings() : []; + $paid = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_paid_content_settings() : []; + $allow = !empty($order['no_logged_in']) || !empty($paid['no_logged_in']); + /** + * Filter guest content pay allow. + * + * @param bool $allow + * @param WP_Post $p + */ + return (bool) apply_filters('aksrt_allow_guest_content_pay', $allow, $p); + } +} + +/** + * Cookie lifetime days for guest paid content (Zibll pay_cookie_day). + */ +if (!function_exists('aksrt_guest_pay_cookie_days')) { + function aksrt_guest_pay_cookie_days(int $post_id = 0): int { + $days = 15; + if (class_exists('AKSRT_Settings')) { + $order = AKSRT_Settings::get_order_settings(); + $days = max(1, min(31, (int) ($order['cookie_day'] ?? 15))); + } + /** + * @param int $days + * @param int $post_id + */ + return max(1, (int) apply_filters('aksrt_guest_pay_cookie_days', $days, $post_id)); + } +} + +/** + * Client IP helper for cookie token binding. + */ +if (!function_exists('aksrt_pay_client_ip')) { + function aksrt_pay_client_ip(): string { + if (function_exists('aksrt_get_client_ip')) { + return (string) aksrt_get_client_ip(); + } + return (string) ($_SERVER['REMOTE_ADDR'] ?? ''); + } +} + +/** + * Build token material for guest order cookie (zibll-style). + * + * @param array $order + */ +if (!function_exists('aksrt_content_order_cookie_token_key')) { + function aksrt_content_order_cookie_token_key(array $order): string { + $meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : []; + $ip = (string) ($meta['ip_address'] ?? $order['ip_address'] ?? ''); + $id = (string) ($order['id'] ?? $order['order_number'] ?? ''); + $post_id = (int) ($order['product_id'] ?? $meta['post_id'] ?? 0); + $num = (string) ($order['order_number'] ?? ''); + $amount = function_exists('aksrt_format_price') + ? aksrt_format_price((float) ($order['amount'] ?? 0)) + : (string) round((float) ($order['amount'] ?? 0), 2); + return $id . '_' . $post_id . '_' . trim($num) . '_' . $amount . '_' . trim($ip); + } +} + +/** + * Set guest paid cookies after content order paid (Zibll zibpay_posts_order_created_set_cookie). + * + * @param array $order + */ +if (!function_exists('aksrt_set_guest_content_pay_cookies')) { + function aksrt_set_guest_content_pay_cookies(array $order): void { + $post_id = (int) ($order['product_id'] ?? 0); + $order_number = (string) ($order['order_number'] ?? ''); + if ($post_id <= 0 || $order_number === '') { + return; + } + $days = aksrt_guest_pay_cookie_days($post_id); + $expire = time() + (DAY_IN_SECONDS * $days); + $path = defined('COOKIEPATH') && COOKIEPATH ? COOKIEPATH : '/'; + $domain = defined('COOKIE_DOMAIN') ? COOKIE_DOMAIN : ''; + $secure = is_ssl(); + $httponly = true; + + setcookie('aksrt_pay_' . $post_id, $order_number, $expire, $path, $domain, $secure, $httponly); + $_COOKIE['aksrt_pay_' . $post_id] = $order_number; + + $token = md5(aksrt_content_order_cookie_token_key($order)); + $token_name = 'aksrt_pay_token_' . md5($order_number); + setcookie($token_name, $token, $expire, $path, $domain, $secure, $httponly); + $_COOKIE[$token_name] = $token; + } +} + +/** + * Resolve guest paid order from browser cookie for a post. + * + * @return array|null + */ +if (!function_exists('aksrt_get_guest_content_paid_order')) { + function aksrt_get_guest_content_paid_order(int $post_id): ?array { + if ($post_id <= 0 || empty($_COOKIE['aksrt_pay_' . $post_id])) { + return null; + } + if (!class_exists('AKSRT_Order_Repository')) { + return null; + } + $order_number = sanitize_text_field((string) $_COOKIE['aksrt_pay_' . $post_id]); + if ($order_number === '') { + return null; + } + $order = AKSRT_Order_Repository::get_instance()->get($order_number); + if (!$order) { + return null; + } + if ((string) ($order['product_type'] ?? '') !== AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE) { + return null; + } + if ((int) ($order['product_id'] ?? 0) !== $post_id) { + return null; + } + $status = (string) ($order['status'] ?? ''); + if (!AKSRT_Domain_Model::is_paid_order_status($status)) { + return null; + } + + // Cookie day window from paid_at. + $paid_at = (string) ($order['paid_at'] ?? $order['created_at'] ?? ''); + if ($paid_at !== '') { + $ts = strtotime($paid_at); + $days = aksrt_guest_pay_cookie_days($post_id); + if ($ts !== false && ($ts + DAY_IN_SECONDS * $days) < time()) { + return null; + } + } + + $token_name = 'aksrt_pay_token_' . md5($order_number); + $cookie_token = isset($_COOKIE[$token_name]) ? (string) $_COOKIE[$token_name] : ''; + $expect = md5(aksrt_content_order_cookie_token_key($order)); + if ($cookie_token === '' || !hash_equals($expect, $cookie_token)) { + return null; + } + + return $order; + } +} + +/** + * Whether current request has paid access via guest cookie. + */ +if (!function_exists('aksrt_guest_has_paid_access')) { + function aksrt_guest_has_paid_access(int $post_id): bool { + return null !== aksrt_get_guest_content_paid_order($post_id); + } +} + /** * Resolve the amount a given user should pay for a post (Zibll VIP tier min). * @@ -813,9 +984,18 @@ if (!function_exists('aksrt_get_paywall_html')) { + 0 && $pay_modo !== 'points' && function_exists('aksrt_allow_guest_content_pay') && aksrt_allow_guest_content_pay($post)) : ?> + +

+ +

+ + diff --git a/templates/parts/resource-download-box.php b/templates/parts/resource-download-box.php index c8e0b768..ab4fd717 100644 --- a/templates/parts/resource-download-box.php +++ b/templates/parts/resource-download-box.php @@ -69,6 +69,18 @@ $is_need_vip = $gate_blocked && $gate_code === 'aksrt_need_vip'; $is_need_purchase = $gate_blocked && $gate_code === 'aksrt_need_purchase'; // Points unpaid may also surface as need_purchase; treat low balance only after cashier. $is_low_credit = false; +$allow_guest_buy = !is_user_logged_in() + && function_exists('aksrt_allow_guest_content_pay') + && aksrt_allow_guest_content_pay($post_id) + && $pay_modo !== 'points' + && $user_amount > 0 + && empty($access['need_vip']); +// If gate only says login but guest buy allowed for cash posts, convert to purchase CTA. +if ($is_need_login && $allow_guest_buy) { + $is_need_login = false; + $is_need_purchase = true; + $gate_blocked = true; +} // Zibll pay_free_logged_show: free downloads still require login. $free_logged_show = !empty($paid_settings['free_logged_show']);