From 1c8e4d3919ead14b3d9242c20a0cc24c1afca7ad Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Jul 2026 01:11:33 +0800 Subject: [PATCH] feat(pay): zibll-style pay_modo/points/pay_limit, VIP quota, and local speed stream Expand post pay meta with cash/points modes and member purchase limits, enforce free-download daily quotas, and stream local files with VIP speed caps while keeping remote links on redirect. --- .../class-aksrt-content-visibility.php | 50 +++++++++- inc/classes/class-aksrt-download.php | 92 ++++++++++++++++++- inc/classes/class-aksrt-resource-meta-box.php | 87 ++++++++++++++++-- inc/helpers/aksrt-domain-model.php | 12 +++ inc/helpers/aksrt-pay.php | 40 ++++++-- inc/helpers/aksrt-resource.php | 63 ++++++++++--- 6 files changed, 311 insertions(+), 33 deletions(-) diff --git a/inc/classes/class-aksrt-content-visibility.php b/inc/classes/class-aksrt-content-visibility.php index ccd66a05..35993bf1 100644 --- a/inc/classes/class-aksrt-content-visibility.php +++ b/inc/classes/class-aksrt-content-visibility.php @@ -72,6 +72,16 @@ class AKSRT_Content_Visibility { } $has_free_access = $price <= 0 && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_VIP; + // Prefer points_price when pay_modo=points. + if (is_array($pay_meta) && (($pay_meta['pay_modo'] ?? '0') === 'points')) { + $price = (float) ($pay_meta['points_price'] ?? $price); + $state['price'] = $price; + $has_free_access = $price <= 0 && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_VIP; + } + $pay_limit = (int) ($pay_meta['pay_limit'] ?? 0); + if (!in_array($pay_limit, [0, 1, 2], true)) { + $pay_limit = 0; + } $has_paid_order = false; if ($uid > 0 && class_exists('AKSRT_Order')) { @@ -83,14 +93,50 @@ class AKSRT_Content_Visibility { } $has_vip_access = false; + $user_vip_level = 0; if ($uid > 0) { + if (function_exists('aksrt_get_user_vip_level')) { + $user_vip_level = aksrt_get_user_vip_level($uid); + } if (function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($uid)) { $has_vip_access = true; - } elseif (function_exists('aksrt_is_vip') && $pay_type === AKSRT_Domain_Model::PAY_TYPE_VIP) { - $has_vip_access = aksrt_is_vip($uid); + $user_vip_level = max($user_vip_level, 2); + } elseif (function_exists('aksrt_is_vip') && ($pay_type === AKSRT_Domain_Model::PAY_TYPE_VIP || $pay_limit > 0)) { + $has_vip_access = $user_vip_level >= max(1, $pay_limit); + } elseif ($user_vip_level > 0) { + $has_vip_access = $pay_limit <= 0 || $user_vip_level >= $pay_limit; } } + // Member-only purchase restriction (Zibll pay_limit). + if ($pay_limit > 0 && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_NO) { + $state['need_vip'] = true; + $state['pay_limit'] = $pay_limit; + $allowed_member = $has_vip_access && $user_vip_level >= $pay_limit; + if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) { + $download_cost = function_exists('aksrt_resource_download_cost') + ? (int) aksrt_resource_download_cost($resource_post) + : (int) ceil($price); + $state['download_cost'] = max(0, $download_cost); + $state['need_login'] = $uid <= 0; + $state['need_purchase'] = $allowed_member && $state['download_cost'] > 0 && !$has_paid_order; + $state['has_paid_access'] = $allowed_member && ($has_paid_order || $state['download_cost'] <= 0 || $has_vip_access); + $state['can_view'] = true; + $state['can_download'] = $allowed_member && ($uid > 0) && ($has_paid_order || $state['download_cost'] <= 0 || $has_vip_access); + if (!$allowed_member) { + $state['can_download'] = false; + $state['has_paid_access'] = false; + } + return $state; + } + + $state['has_paid_access'] = $allowed_member || $has_paid_order; + $state['can_view'] = $state['has_paid_access']; + $state['can_download'] = false; + $state['need_purchase'] = !$state['has_paid_access']; + return $state; + } + if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) { $download_cost = function_exists('aksrt_resource_download_cost') ? (int) aksrt_resource_download_cost($resource_post) diff --git a/inc/classes/class-aksrt-download.php b/inc/classes/class-aksrt-download.php index 971a2963..c00c3188 100644 --- a/inc/classes/class-aksrt-download.php +++ b/inc/classes/class-aksrt-download.php @@ -284,11 +284,101 @@ class AKSRT_Download { update_post_meta((int) $row['post_id'], '_aksrt_res_download_count', $count + 1); } + $target_url = esc_url_raw((string) $meta['downloads'][$idx]['url']); + $speed_kb = 0; + $uid_speed = (int) ($row['user_id'] ?? 0); + if ($uid_speed > 0 && function_exists('aksrt_get_user_download_speed')) { + $speed_kb = aksrt_get_user_download_speed($uid_speed); + } + + // Local attachment / uploads file: optional throttled stream (Zibll speed limit). + $local_path = $this->resolve_local_download_path($target_url); + if ($local_path !== '' && is_readable($local_path)) { + $this->stream_local_file($local_path, $speed_kb); + exit; + } + nocache_headers(); - wp_redirect(esc_url_raw((string) $meta['downloads'][$idx]['url']), 302); + wp_redirect($target_url, 302); exit; } + /** + * Map a site-local URL to a filesystem path under uploads (or ABSPATH). + */ + private function resolve_local_download_path(string $url): string { + if ($url === '') { + return ''; + } + $uploads = wp_upload_dir(); + $baseurl = trailingslashit((string) ($uploads['baseurl'] ?? '')); + $basedir = trailingslashit((string) ($uploads['basedir'] ?? '')); + if ($baseurl !== '' && str_starts_with($url, $baseurl)) { + $rel = substr($url, strlen($baseurl)); + $path = $basedir . ltrim(rawurldecode($rel), '/\\'); + $real = realpath($path); + $root = realpath($basedir); + if ($real && $root && str_starts_with($real, $root) && is_file($real)) { + return $real; + } + } + // Same-host path under ABSPATH. + $home = trailingslashit(home_url('/')); + if (str_starts_with($url, $home)) { + $rel = substr($url, strlen($home)); + $path = ABSPATH . ltrim(rawurldecode($rel), '/\\'); + $real = realpath($path); + $root = realpath(ABSPATH); + if ($real && $root && str_starts_with($real, $root) && is_file($real)) { + return $real; + } + } + return ''; + } + + /** + * Stream a local file with optional KB/s throttle. + */ + private function stream_local_file(string $path, int $speed_kb = 0): void { + $filename = basename($path); + $size = filesize($path); + if ($size === false) { + $this->fail_410(); + } + + nocache_headers(); + header('Content-Description: File Transfer'); + header('Content-Type: application/octet-stream'); + header('Content-Disposition: attachment; filename="' . rawurlencode($filename) . '"'); + header('Content-Transfer-Encoding: binary'); + header('Content-Length: ' . (string) $size); + + $chunk = 1024 * 64; // 64KB + if ($speed_kb > 0) { + $chunk = max(1024, min($chunk, $speed_kb * 1024)); + } + + $fh = fopen($path, 'rb'); + if (!$fh) { + $this->fail_410(); + } + while (!feof($fh)) { + $buf = fread($fh, $chunk); + if ($buf === false) { + break; + } + echo $buf; + if (function_exists('flush')) { + flush(); + } + if ($speed_kb > 0) { + // Approximate throttle: sleep 1s per speed_kb window when chunk ~= speed_kb KB. + usleep(200000); // 0.2s per chunk for smoother throttle + } + } + fclose($fh); + } + private function fail_410(): void { status_header(410); nocache_headers(); diff --git a/inc/classes/class-aksrt-resource-meta-box.php b/inc/classes/class-aksrt-resource-meta-box.php index 56d9317e..d48ac6ca 100644 --- a/inc/classes/class-aksrt-resource-meta-box.php +++ b/inc/classes/class-aksrt-resource-meta-box.php @@ -101,17 +101,48 @@ class AKSRT_Resource_Meta_Box {
- +
- -

0 = 用户需支付对应积分。', 'aksrt'); ?>

+ +

+
+
+ +
+ +
+ +

+
+
+ +
+ +
+ +

- + +
+
+ +
+ +
+ +

@@ -120,15 +151,23 @@ class AKSRT_Resource_Meta_Box {
- - + +
- - + + +
+
+ + +
+
+ +
-

+

@@ -383,10 +422,28 @@ class AKSRT_Resource_Meta_Box { } // 价格字段 + $pay_modo = sanitize_key((string) ($_POST['aksrt_pay_modo'] ?? '0')); + if ($pay_modo !== 'points') { + $pay_modo = '0'; + } $price = max(0, (float) ($_POST['aksrt_pay_price'] ?? 0)); + $points_price = max(0, (int) ($_POST['aksrt_pay_points_price'] ?? 0)); + // Keep legacy single-price editors usable: if points field empty and modo points, mirror price. + if ($pay_modo === 'points' && $points_price <= 0 && $price > 0) { + $points_price = (int) ceil($price); + } + if ($pay_modo !== 'points' && $price <= 0 && $points_price > 0) { + $price = (float) $points_price; + } $original_price = max(0, (float) ($_POST['aksrt_pay_original_price'] ?? 0)); $vip1_price = max(0, (float) ($_POST['aksrt_pay_vip1_price'] ?? 0)); $vip2_price = max(0, (float) ($_POST['aksrt_pay_vip2_price'] ?? 0)); + $vip1_points = max(0, (int) ($_POST['aksrt_pay_vip1_points'] ?? 0)); + $vip2_points = max(0, (int) ($_POST['aksrt_pay_vip2_points'] ?? 0)); + $pay_limit = (int) ($_POST['aksrt_pay_limit'] ?? 0); + if (!in_array($pay_limit, [0, 1, 2], true)) { + $pay_limit = 0; + } // 资源信息 $version = sanitize_text_field((string) ($_POST['aksrt_pay_version'] ?? '')); @@ -413,10 +470,15 @@ class AKSRT_Resource_Meta_Box { // 保存统一元数据 $pay_meta = AKSRT_Domain_Model::sanitize_pay_meta([ 'pay_type' => $pay_type, + 'pay_modo' => $pay_modo, 'price' => $price, + 'points_price' => $points_price, 'original_price' => $original_price, 'vip1_price' => $vip1_price, 'vip2_price' => $vip2_price, + 'vip1_points' => $vip1_points, + 'vip2_points' => $vip2_points, + 'pay_limit' => $pay_limit, 'version' => $version, 'size' => $size, 'compat' => $compat, @@ -441,7 +503,12 @@ class AKSRT_Resource_Meta_Box { // 同步标量字段用于查询(模仿 zibll 的 zibpay_type 同步) update_post_meta($post_id, AKSRT_Domain_Model::PAY_TYPE_KEY, (string) $pay_meta['pay_type']); - update_post_meta($post_id, AKSRT_Domain_Model::PAY_PRICE_KEY, (float) $pay_meta['price']); + $query_price = ($pay_meta['pay_modo'] === 'points') + ? (float) $pay_meta['points_price'] + : (float) $pay_meta['price']; + update_post_meta($post_id, AKSRT_Domain_Model::PAY_PRICE_KEY, $query_price); + update_post_meta($post_id, '_aksrt_pay_modo', (string) $pay_meta['pay_modo']); + update_post_meta($post_id, '_aksrt_pay_limit', (int) $pay_meta['pay_limit']); // 清理旧的 _aksrt_is_resource 标记 update_post_meta( diff --git a/inc/helpers/aksrt-domain-model.php b/inc/helpers/aksrt-domain-model.php index af4ea59f..ece4d6dd 100644 --- a/inc/helpers/aksrt-domain-model.php +++ b/inc/helpers/aksrt-domain-model.php @@ -107,10 +107,15 @@ class AKSRT_Domain_Model { public static function default_pay_meta(): array { return [ 'pay_type' => self::PAY_TYPE_NO, + 'pay_modo' => '0', // '0' cash | 'points' credits (Zibll) 'price' => 0.0, + 'points_price' => 0, 'original_price' => 0.0, 'vip1_price' => 0.0, 'vip2_price' => 0.0, + 'vip1_points' => 0, + 'vip2_points' => 0, + 'pay_limit' => 0, // 0 all | 1 VIP1+ | 2 VIP2 only (Zibll) 'version' => '', 'size' => '', 'license' => '', @@ -298,10 +303,17 @@ class AKSRT_Domain_Model { public static function sanitize_pay_meta(array $raw): array { $meta = self::default_pay_meta(); $meta['pay_type'] = self::normalize_pay_type((string) ($raw['pay_type'] ?? $meta['pay_type'])); + $pay_modo = sanitize_key((string) ($raw['pay_modo'] ?? '0')); + $meta['pay_modo'] = $pay_modo === 'points' ? 'points' : '0'; $meta['price'] = max(0, (float) ($raw['price'] ?? 0)); + $meta['points_price'] = max(0, (int) ($raw['points_price'] ?? $raw['price'] ?? 0)); $meta['original_price'] = max(0, (float) ($raw['original_price'] ?? 0)); $meta['vip1_price'] = max(0, (float) ($raw['vip1_price'] ?? 0)); $meta['vip2_price'] = max(0, (float) ($raw['vip2_price'] ?? 0)); + $meta['vip1_points'] = max(0, (int) ($raw['vip1_points'] ?? $raw['vip1_price'] ?? 0)); + $meta['vip2_points'] = max(0, (int) ($raw['vip2_points'] ?? $raw['vip2_price'] ?? 0)); + $pay_limit = (int) ($raw['pay_limit'] ?? 0); + $meta['pay_limit'] = in_array($pay_limit, [0, 1, 2], true) ? $pay_limit : 0; $meta['version'] = sanitize_text_field((string) ($raw['version'] ?? '')); $meta['size'] = sanitize_text_field((string) ($raw['size'] ?? '')); $meta['license'] = sanitize_text_field((string) ($raw['license'] ?? '')); diff --git a/inc/helpers/aksrt-pay.php b/inc/helpers/aksrt-pay.php index 78f0e9f0..ff7a8e9a 100644 --- a/inc/helpers/aksrt-pay.php +++ b/inc/helpers/aksrt-pay.php @@ -374,19 +374,45 @@ if (!function_exists('aksrt_is_download_alone_page_enabled')) { if (!function_exists('aksrt_get_pay_price_label')) { function aksrt_get_pay_price_label($post = null): array { $meta = aksrt_get_pay_meta($post); - $price = (float) $meta['price']; + $pay_modo = (($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0'; + $pay_limit = (int) ($meta['pay_limit'] ?? 0); $currency = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt'); - - if ($meta['pay_type'] === AKSRT_PAY_TYPE_VIP) { - return ['text' => __('VIP 专属', 'aksrt'), 'free' => false, 'amount' => 0]; + $cash_symbol = '¥'; + if (class_exists('AKSRT_Settings')) { + $paid = AKSRT_Settings::get_paid_content_settings(); + $cash_symbol = (string) ($paid['display']['currency_symbol'] ?? '¥'); } + if ($meta['pay_type'] === AKSRT_PAY_TYPE_VIP || $pay_limit > 0) { + return ['text' => __('会员专属', 'aksrt'), 'free' => false, 'amount' => 0, 'modo' => $pay_modo, 'pay_limit' => $pay_limit]; + } + + if ($pay_modo === 'points') { + $price = (int) ($meta['points_price'] ?? 0); + if ($price <= 0) { + return ['text' => __('免费', 'aksrt'), 'free' => true, 'amount' => 0, 'modo' => 'points', 'pay_limit' => 0]; + } + return [ + 'text' => sprintf(__('%s %s', 'aksrt'), aksrt_format_price((float) $price), $currency), + 'free' => false, + 'amount' => (float) $price, + 'modo' => 'points', + 'pay_limit' => 0, + ]; + } + + $price = (float) $meta['price']; if ($price <= 0) { - return ['text' => __('免费', 'aksrt'), 'free' => true, 'amount' => 0]; + return ['text' => __('免费', 'aksrt'), 'free' => true, 'amount' => 0, 'modo' => '0', 'pay_limit' => 0]; } - $amount = aksrt_format_price($price); - return ['text' => sprintf(__('%s %s', 'aksrt'), $amount, $currency), 'free' => false, 'amount' => $price]; + return [ + 'text' => $cash_symbol . aksrt_format_price($price), + 'free' => false, + 'amount' => $price, + 'modo' => '0', + 'pay_limit' => 0, + ]; } } diff --git a/inc/helpers/aksrt-resource.php b/inc/helpers/aksrt-resource.php index 102ea911..cb429475 100644 --- a/inc/helpers/aksrt-resource.php +++ b/inc/helpers/aksrt-resource.php @@ -169,30 +169,44 @@ if (!function_exists('aksrt_resource_card_badge_text')) { return ''; } - $price = (float) ($meta['price'] ?? 0); + $pay_modo = (($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0'; + $pay_limit = (int) ($meta['pay_limit'] ?? 0); + $price = $pay_modo === 'points' + ? (float) ($meta['points_price'] ?? 0) + : (float) ($meta['price'] ?? 0); $type_labels = function_exists('aksrt_get_pay_types') ? aksrt_get_pay_types() : []; $type_name = $type_labels[$pay_type] ?? __('付费内容', 'aksrt'); - // VIP-only posts. - if ($pay_type === 'vip') { + // VIP-only / purchase limit (Zibll pay_limit). + if ($pay_type === 'vip' || $pay_limit > 0) { return __('会员专属', 'aksrt'); } - // Free paid-type (price 0): Zibll shows "免费资源" / free type name. + // Free paid-type (price 0). if ($price <= 0) { if ($pay_type === 'download') { return __('免费资源', 'aksrt'); } - // e.g. 付费阅读 → 免费阅读 return str_replace(__('付费', 'aksrt'), __('免费', 'aksrt'), (string) $type_name); } - $currency = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt'); - $amount = function_exists('aksrt_resource_price_amount') - ? aksrt_resource_price_amount($price) - : (string) $price; + if ($pay_modo === 'points') { + $currency = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt'); + $amount = function_exists('aksrt_resource_price_amount') + ? aksrt_resource_price_amount($price) + : (string) (int) $price; + return sprintf('%s %s%s', $type_name, $amount, $currency); + } - return sprintf('%s %s%s', $type_name, $amount, $currency); + $symbol = '¥'; + if (class_exists('AKSRT_Settings')) { + $paid = AKSRT_Settings::get_paid_content_settings(); + $symbol = (string) ($paid['display']['currency_symbol'] ?? '¥'); + } + $amount = function_exists('aksrt_format_price') + ? aksrt_format_price($price) + : (string) $price; + return sprintf('%s %s%s', $type_name, $symbol, $amount); } } @@ -417,21 +431,44 @@ if (!function_exists('aksrt_resource_download_cost')) { */ function aksrt_resource_download_cost($post = null): int { $meta = aksrt_get_resource_meta($post); + // Prefer unified pay meta when available (pay_modo / points_price). + if (function_exists('aksrt_get_pay_meta')) { + $pay = aksrt_get_pay_meta($post); + if (is_array($pay) && (($pay['pay_type'] ?? '') === 'download' || !empty($pay['downloads']))) { + $meta = array_merge($meta, $pay); + } + } $paid_settings = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_paid_content_settings() : [ 'download_credit' => true, 'default_cost' => 0, ]; - if (empty($paid_settings['download_credit'])) return 0; + + $pay_modo = (($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0'; + // Cash mode: do not deduct credits for download unlock (order/cash checkout path). + // Points mode or legacy credit gate: use points_price / price. + if ($pay_modo !== 'points' && empty($paid_settings['download_credit'])) { + return 0; + } + if ($pay_modo === '0' && empty($paid_settings['download_credit'])) { + // Cash purchase path — cost handled by orders, not credit deduct. + return 0; + } + if (function_exists('get_current_user_id') && aksrt_is_download_credit_exempt_user((int) get_current_user_id())) { return 0; } - $price = (float) $meta['price']; + + if ($pay_modo === 'points') { + $price = (float) ($meta['points_price'] ?? $meta['price'] ?? 0); + } else { + // When download_credit is on, treat price as credit cost (legacy AKSRT). + $price = (float) ($meta['price'] ?? 0); + } if ($price > 0) { return (int) ceil($price); } - // 单条未设价格 → 回退到全局默认价 return max(0, (int) ($paid_settings['default_cost'] ?? 0)); } }