diff --git a/inc/classes/class-aksrt-content-visibility.php b/inc/classes/class-aksrt-content-visibility.php index 40c40755..ccd66a05 100644 --- a/inc/classes/class-aksrt-content-visibility.php +++ b/inc/classes/class-aksrt-content-visibility.php @@ -171,6 +171,14 @@ class AKSRT_Content_Visibility { ]); } + // Zibll daily free-download quota. + if ($cost <= 0 && function_exists('aksrt_check_free_download_quota')) { + $quota = aksrt_check_free_download_quota($uid, $cost); + if (is_wp_error($quota)) { + return $quota; + } + } + if (!empty($paid_settings['download_credit']) && $cost > 0) { $balance = class_exists('AKSRT_Credits') ? AKSRT_Credits::balance($uid) : 0; if ($balance < $cost) { diff --git a/inc/classes/class-aksrt-download.php b/inc/classes/class-aksrt-download.php index 2012a9f1..971a2963 100644 --- a/inc/classes/class-aksrt-download.php +++ b/inc/classes/class-aksrt-download.php @@ -124,10 +124,13 @@ class AKSRT_Download { 'post_id' => $post_id, 'price' => (float) $meta['price'], 'cost' => $cost, - 'require_login' => !empty($paid_settings['download_login']) || !empty($meta['require_login']), + 'require_login' => !empty($paid_settings['download_login']) || !empty($meta['require_login']) || (!empty($paid_settings['free_logged_show']) && $cost <= 0), 'require_captcha' => $need_captcha, 'logged_in' => $uid > 0, 'balance' => $uid > 0 && class_exists('AKSRT_Credits') ? AKSRT_Credits::balance($uid) : 0, + 'free_limit' => $uid > 0 && function_exists('aksrt_get_user_free_download_limit') ? aksrt_get_user_free_download_limit($uid) : 0, + 'free_used' => $uid > 0 && function_exists('aksrt_get_user_free_downloaded_number') ? aksrt_get_user_free_downloaded_number($uid) : 0, + 'free_surplus' => $uid > 0 && function_exists('aksrt_get_free_download_surplus') ? aksrt_get_free_download_surplus($uid) : null, 'sources' => array_map(function ($row, $idx) { $extract_code = trim((string) ($row['extract_code'] ?? '')); return [ @@ -162,6 +165,17 @@ class AKSRT_Download { if (is_wp_error($gate)) return $gate; $paid_settings = AKSRT_Settings::get_paid_content_settings(); + $uid = get_current_user_id(); + $cost = aksrt_resource_download_cost($post); + + // Daily free-download quota (Zibll). + if ($cost <= 0 && $uid > 0 && function_exists('aksrt_check_free_download_quota')) { + $quota = aksrt_check_free_download_quota($uid, $cost); + if (is_wp_error($quota)) { + return $quota; + } + } + $need_captcha = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']); // 下载双开关:cms_features.download_captcha(或单资源强制)且场景未停用。 if ($need_captcha && function_exists('aksrt_require_captcha')) { @@ -176,8 +190,6 @@ class AKSRT_Download { } } - $uid = get_current_user_id(); - $cost = aksrt_resource_download_cost($post); if ($cost > 0 && $uid > 0 && class_exists('AKSRT_Credits')) { $deducted = AKSRT_Credits::deduct($uid, $cost, 'download', ['type' => 'post', 'id' => $post_id]); if (is_wp_error($deducted)) return $deducted; @@ -191,6 +203,11 @@ class AKSRT_Download { return $token; } + // Record free download against daily quota after token issued. + if ($cost <= 0 && $uid > 0 && function_exists('aksrt_record_user_free_download')) { + aksrt_record_user_free_download($uid, $post_id); + } + $dl_url = home_url('/' . self::ENDPOINT . '/' . $token); if ($uid > 0 && function_exists('aksrt_add_user_notification')) { aksrt_add_user_notification($uid, [ diff --git a/inc/helpers/aksrt-vip.php b/inc/helpers/aksrt-vip.php index bfad8164..77161697 100644 --- a/inc/helpers/aksrt-vip.php +++ b/inc/helpers/aksrt-vip.php @@ -149,6 +149,160 @@ function aksrt_get_vip_badge_text(): string { return sanitize_text_field($vip_settings['badge_text'] ?? 'VIP'); } +/** + * Daily free-download limit for a user (0 = unlimited). + * Aligns with zibll pay_download_limit / pay_download_limit_vip_{n}. + */ +if (!function_exists('aksrt_get_user_free_download_limit')) { + function aksrt_get_user_free_download_limit(int $user_id = 0): int { + if ($user_id <= 0) { + $user_id = get_current_user_id(); + } + if ($user_id <= 0 || !class_exists('AKSRT_Settings')) { + return 0; + } + + $vip = AKSRT_Settings::get_payment_vip_settings(); + $level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0; + $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : []; + $benefit = is_array($vip['benefit'] ?? null) ? $vip['benefit'] : []; + + if ($level >= 1 && !empty($levels[$level]['enabled'])) { + return max(0, (int) ($levels[$level]['download_limit'] ?? 0)); + } + + return max(0, (int) ($benefit['download_limit'] ?? 0)); + } +} + +/** + * Download speed cap in KB/s for a user (0 = unlimited). + */ +if (!function_exists('aksrt_get_user_download_speed')) { + function aksrt_get_user_download_speed(int $user_id = 0): int { + if ($user_id <= 0) { + $user_id = get_current_user_id(); + } + if ($user_id <= 0 || !class_exists('AKSRT_Settings')) { + return 0; + } + + $vip = AKSRT_Settings::get_payment_vip_settings(); + $level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0; + $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : []; + $benefit = is_array($vip['benefit'] ?? null) ? $vip['benefit'] : []; + + if ($level >= 1 && !empty($levels[$level]['enabled'])) { + return max(0, (int) ($levels[$level]['download_speed'] ?? 0)); + } + + return max(0, (int) ($benefit['download_speed'] ?? 0)); + } +} + +/** + * Count free downloads used today for a user (zibll pay_down_number). + */ +if (!function_exists('aksrt_get_user_free_downloaded_number')) { + function aksrt_get_user_free_downloaded_number(int $user_id = 0): int { + if ($user_id <= 0) { + $user_id = get_current_user_id(); + } + if ($user_id <= 0) { + return 0; + } + + $day = current_time('Y-m-d'); + $mate = get_user_meta($user_id, 'aksrt_pay_down_number', true); + if (!is_array($mate) || empty($mate[$day]) || !is_array($mate[$day])) { + return 0; + } + return count($mate[$day]); + } +} + +/** + * Record a free download for daily quota (zibll pay_down_number). + */ +if (!function_exists('aksrt_record_user_free_download')) { + function aksrt_record_user_free_download(int $user_id, int $post_id): void { + if ($user_id <= 0 || $post_id <= 0) { + return; + } + $day = current_time('Y-m-d'); + $mate = get_user_meta($user_id, 'aksrt_pay_down_number', true); + if (!is_array($mate)) { + $mate = []; + } + // Drop old days to keep meta small. + foreach (array_keys($mate) as $k) { + if ($k !== $day) { + unset($mate[$k]); + } + } + if (!isset($mate[$day]) || !is_array($mate[$day])) { + $mate[$day] = []; + } + $mate[$day][(string) $post_id] = 1; + update_user_meta($user_id, 'aksrt_pay_down_number', $mate); + } +} + +/** + * Whether a free download is allowed under daily quota. + * Paid downloads (cost > 0) always pass. + * + * @return true|WP_Error + */ +if (!function_exists('aksrt_check_free_download_quota')) { + function aksrt_check_free_download_quota(int $user_id, int $cost = 0) { + if ($cost > 0) { + return true; + } + if ($user_id <= 0) { + return true; // login gate handled elsewhere + } + + $limit = aksrt_get_user_free_download_limit($user_id); + if ($limit <= 0) { + return true; // unlimited + } + + $used = aksrt_get_user_free_downloaded_number($user_id); + if ($used >= $limit) { + return new WP_Error( + 'aksrt_download_limit', + __('您今日下载免费资源个数已超限,请明日再下载。', 'aksrt'), + [ + 'status' => 429, + 'limit' => $limit, + 'used' => $used, + ] + ); + } + + return true; + } +} + +/** + * Remaining free downloads today (null = unlimited). + */ +if (!function_exists('aksrt_get_free_download_surplus')) { + function aksrt_get_free_download_surplus(int $user_id = 0): ?int { + if ($user_id <= 0) { + $user_id = get_current_user_id(); + } + $limit = aksrt_get_user_free_download_limit($user_id); + if ($limit <= 0) { + return null; + } + $used = aksrt_get_user_free_downloaded_number($user_id); + return max(0, $limit - $used); + } +} + + /** * 渲染 VIP 徽标 HTML * diff --git a/templates/parts/resource-download-box.php b/templates/parts/resource-download-box.php index 4bf6d730..7b04838b 100644 --- a/templates/parts/resource-download-box.php +++ b/templates/parts/resource-download-box.php @@ -282,7 +282,23 @@ $currency = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() -