ID, \AKSRT\Core\DomainModel::RESOURCE_DOWNLOAD_COUNT_KEY, true); $legacy_download_count = (int) get_post_meta($p->ID, 'download_count', true); $resource_view_count = (int) get_post_meta($p->ID, \AKSRT\Core\DomainModel::RESOURCE_VIEW_COUNT_KEY, true); $common_view_count = (int) get_post_meta($p->ID, \AKSRT\Core\DomainModel::POST_VIEW_COUNT_KEY, true); // Normalize downloads extract_code aliases for legacy rows still stored on pay meta. $downloads = array_values(is_array($pay_meta['downloads'] ?? null) ? $pay_meta['downloads'] : []); $downloads = array_values(array_filter(array_map(static function ($row) { if (!is_array($row)) { return null; } $extract_keys = [ 'extract_code', 'extractCode', 'extract', 'extract_key', 'extraction_code', 'extractionCode', 'code', 'password', 'pwd', 'pan_code', 'panCode', 'pan_password', 'panPassword', 'access_code', 'accessCode', ]; if (!array_key_exists('extract_code', $row)) { foreach ($extract_keys as $alias) { if (isset($row[$alias]) && !is_array($row[$alias]) && trim((string) $row[$alias]) !== '') { $row['extract_code'] = (string) $row[$alias]; break; } } } $row['extract_code'] = isset($row['extract_code']) ? trim((string) $row['extract_code']) : ''; return $row; }, $downloads))); $virtual_sales = max(0, (int) ($pay_meta['virtual_sales'] ?? 0)); $show_sales = !isset($pay_meta['show_sales']) || !empty($pay_meta['show_sales']); return [ 'version' => (string) ($pay_meta['version'] ?? ''), 'size' => (string) ($pay_meta['size'] ?? ''), 'price' => (float) ($pay_meta['price'] ?? 0), 'points_price' => (int) ($pay_meta['points_price'] ?? 0), 'pay_modo' => (($pay_meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0', 'pay_limit' => in_array((int) ($pay_meta['pay_limit'] ?? 0), [0, 1, 2], true) ? (int) $pay_meta['pay_limit'] : 0, 'original_price' => (float) ($pay_meta['original_price'] ?? 0), 'vip1_price' => (float) ($pay_meta['vip1_price'] ?? 0), 'vip2_price' => (float) ($pay_meta['vip2_price'] ?? 0), 'vip1_points' => (int) ($pay_meta['vip1_points'] ?? 0), 'vip2_points' => (int) ($pay_meta['vip2_points'] ?? 0), 'pay_type' => (string) ($pay_meta['pay_type'] ?? \AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD), 'license' => (string) ($pay_meta['license'] ?? ''), 'compat' => (string) ($pay_meta['compat'] ?? ''), 'demo_url' => (string) ($pay_meta['demo_url'] ?? ''), 'downloads' => $downloads, 'download_count' => max(0, max($download_count, $legacy_download_count)), 'view_count' => max(0, max($resource_view_count, $common_view_count)), 'virtual_sales' => $virtual_sales, 'sales_count' => $show_sales ? ($virtual_sales + max(0, max($download_count, $legacy_download_count))) : 0, 'show_sales' => $show_sales, 'require_login' => !empty($pay_meta['require_login']), 'require_captcha' => !empty($pay_meta['require_captcha']), ]; } } if (!function_exists('aksrt_default_resource_meta')) { function aksrt_default_resource_meta(): array { $meta = \AKSRT\Core\DomainModel::default_pay_meta(); return [ 'version' => (string) ($meta['version'] ?? ''), 'size' => (string) ($meta['size'] ?? ''), 'price' => (float) ($meta['price'] ?? 0), 'points_price' => (int) ($meta['points_price'] ?? 0), 'pay_modo' => (($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0', 'pay_limit' => in_array((int) ($meta['pay_limit'] ?? 0), [0, 1, 2], true) ? (int) $meta['pay_limit'] : 0, 'original_price' => (float) ($meta['original_price'] ?? 0), 'vip1_price' => (float) ($meta['vip1_price'] ?? 0), 'vip2_price' => (float) ($meta['vip2_price'] ?? 0), 'vip1_points' => (int) ($meta['vip1_points'] ?? 0), 'vip2_points' => (int) ($meta['vip2_points'] ?? 0), 'pay_type' => \AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD, 'license' => (string) ($meta['license'] ?? ''), 'compat' => (string) ($meta['compat'] ?? ''), 'demo_url' => (string) ($meta['demo_url'] ?? ''), 'downloads' => [], 'download_count' => 0, 'view_count' => 0, 'virtual_sales' => 0, 'sales_count' => 0, 'show_sales' => true, 'require_login' => !empty($meta['require_login']), 'require_captcha' => !empty($meta['require_captcha']), ]; } } if (!function_exists('aksrt_resource_price_label')) { /** * 把价格数字渲染成展示徽章文案。 * * @param float $price * @return array { text:string, free:bool } */ function aksrt_resource_price_label(float $price): array { if ($price <= 0) { return [ 'text' => __('免费', 'aksrt'), 'free' => true, ]; } $n = (fmod($price, 1.0) === 0.0) ? (string) (int) $price : rtrim(rtrim(number_format($price, 2, '.', ''), '0'), '.'); return [ 'text' => sprintf(__('%s 积分', 'aksrt'), $n), 'free' => false, ]; } } if (!function_exists('aksrt_resource_price_amount')) { /** * Format the raw resource price number for compact UI labels. * * @param float $price * @return string */ function aksrt_resource_price_amount(float $price): string { if (fmod($price, 1.0) === 0.0) { return (string) (int) $price; } return rtrim(rtrim(number_format($price, 2, '.', ''), '0'), '.'); } } if (!function_exists('aksrt_resource_card_badge_text')) { /** * List-card pay badge (legacy equivalent). * * Renders for any paid post type, not only download resources. * Returns plain text; card template wraps with link + class. * * @param int|WP_Post|null $post * @return string */ function aksrt_resource_card_badge_text($post = null): string { $p = get_post($post); if (!($p instanceof WP_Post)) { return ''; } $meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($p) : null; if (!is_array($meta)) { return ''; } $pay_type = (string) ($meta['pay_type'] ?? 'no'); if ($pay_type === '' || $pay_type === 'no') { return ''; } $pay_modo = (($meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0'; $pay_limit = (int) ($meta['pay_limit'] ?? 0); $type_labels = function_exists('aksrt_get_pay_types') ? aksrt_get_pay_types() : []; $type_name = $type_labels[$pay_type] ?? __('付费内容', 'aksrt'); // VIP-only / purchase limit (legacy pay_limit). if ($pay_type === 'vip' || $pay_limit > 0) { return __('会员专属', 'aksrt'); } $priced = function_exists('aksrt_get_user_pay_price') ? aksrt_get_user_pay_price($p) : [ 'amount' => $pay_modo === 'points' ? (float) ($meta['points_price'] ?? 0) : (float) ($meta['price'] ?? 0), 'modo' => $pay_modo, ]; $price = (float) ($priced['amount'] ?? 0); $pay_modo = (($priced['modo'] ?? $pay_modo) === 'points') ? 'points' : '0'; // Free paid-type (price 0). if ($price <= 0) { if ($pay_type === 'download') { return __('免费资源', 'aksrt'); } return str_replace(__('付费', 'aksrt'), __('免费', 'aksrt'), (string) $type_name); } if ($pay_modo === 'points') { $currency = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\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); } $symbol = '¥'; if (class_exists('\AKSRT\Settings\Settings')) { $paid = \AKSRT\Settings\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); } } if (!function_exists('aksrt_resource_pay_type_badge')) { /** * 根据付费类型返回徽章 HTML。 * * @param string $pay_type * @return string HTML 或空字符串 */ function aksrt_resource_pay_type_badge(string $pay_type): string { $badges = [ 'reading' => ['label' => __('付费阅读', 'aksrt'), 'color' => '#EF4444', 'icon' => 'fa-lock'], 'download'=> ['label' => __('付费下载', 'aksrt'), 'color' => '#F97316', 'icon' => 'fa-download'], 'gallery' => ['label' => __('付费图库', 'aksrt'), 'color' => '#10B981', 'icon' => 'fa-image'], 'video' => ['label' => __('付费视频', 'aksrt'), 'color' => '#8B5CF6', 'icon' => 'fa-play-circle'], 'vip' => ['label' => __('VIP 专属', 'aksrt'), 'color' => '#3B82F6', 'icon' => 'fa-certificate'], ]; if ($pay_type === 'download' || !isset($badges[$pay_type])) { return ''; } $b = $badges[$pay_type]; return sprintf( ' %s', esc_attr($b['color']), esc_attr($b['icon']), esc_html($b['label']) ); } } if (!function_exists('aksrt_resource_preview_images')) { /** * Collect preview images for resource cards and archive thumbnails. * * Priority: curated cover_images meta -> featured image -> gallery/content images. * * @param int|WP_Post|null $post * @param int $limit * @return string[] */ function aksrt_resource_preview_images($post = null, int $limit = 4): array { $p = get_post($post); if (!($p instanceof WP_Post) || !function_exists('aksrt_is_resource_post') || !aksrt_is_resource_post($p)) { return []; } $limit = max(1, min(8, $limit)); $images = []; if (class_exists('\AKSRT\Core\PostMetaBox')) { $cover_images = (string) \AKSRT\Core\PostMetaBox::get_meta_value($p->ID, 'cover_images', ''); if ($cover_images !== '') { $cover_ids = array_filter(array_map('absint', preg_split('/[\s,]+/', $cover_images) ?: [])); foreach ($cover_ids as $attachment_id) { $url = (string) wp_get_attachment_image_url($attachment_id, 'medium_large'); if ($url !== '') { $images[] = esc_url_raw($url); } } } } if (has_post_thumbnail($p)) { $featured = (string) get_the_post_thumbnail_url($p, 'medium_large'); if ($featured !== '') { $images[] = esc_url_raw($featured); } } if (function_exists('aksrt_get_post_gallery_images')) { $images = array_merge($images, aksrt_get_post_gallery_images($p->ID, $limit)); } if (function_exists('aksrt_get_first_content_image_url')) { $content_image = (string) aksrt_get_first_content_image_url($p->ID); if ($content_image !== '') { $images[] = esc_url_raw($content_image); } } $images = array_values(array_unique(array_filter($images, static function ($url): bool { return is_string($url) && $url !== ''; }))); return array_slice($images, 0, $limit); } } if (!function_exists('aksrt_resource_primary_image_url')) { /** * Primary preview image for a resource post. * * @param int|WP_Post|null $post * @return string */ function aksrt_resource_primary_image_url($post = null): string { $images = aksrt_resource_preview_images($post, 1); return $images[0] ?? ''; } } if (!function_exists('aksrt_resource_meta_strip')) { /** * 卡片用的紧凑元数据条 [v1.0 · 210MB · ↓1234]。 * * @param array $meta aksrt_get_resource_meta 返回值 * @return array 字符串数组 */ function aksrt_resource_meta_strip(array $meta): array { $parts = []; if ($meta['version'] !== '') { $parts[] = 'v' . $meta['version']; } if ($meta['size'] !== '') { $parts[] = $meta['size']; } if ($meta['download_count'] > 0) { $parts[] = '↓ ' . number_format_i18n($meta['download_count']); } return $parts; } } /** * 是否命中下载积分免扣策略。 */ if (!function_exists('aksrt_is_download_credit_exempt_user')) { function aksrt_is_download_credit_exempt_user(int $user_id = 0): bool { if ($user_id <= 0 || !class_exists('\AKSRT\Settings\Settings')) { return false; } $paid_settings = \AKSRT\Settings\Settings::get_paid_content_settings(); if (empty($paid_settings['vip_free_download']) || empty($paid_settings['vip_exempt_roles'])) { return false; } $user = get_userdata($user_id); if (!$user || empty($user->roles) || !is_array($user->roles)) { return false; } return !empty(array_intersect($user->roles, (array) $paid_settings['vip_exempt_roles'])); } } if (!function_exists('aksrt_can_download')) { /** * 是否可下载。 * * 单一合同:委托 \AKSRT\Content\Visibility(收银台订单解锁 / VIP / 登录 / 日限额)。 * 不再用 cms_features.download_credit 做「按次扣积分」门禁。 * * @param int|WP_Post|null $post * @param int $user_id 0 表示当前用户 * @return true|WP_Error */ function aksrt_can_download($post = null, int $user_id = 0) { if (class_exists('\AKSRT\Content\Visibility')) { return \AKSRT\Content\Visibility::can_download_resource($post, $user_id); } $p = get_post($post); if (!($p instanceof WP_Post)) { return new WP_Error('aksrt_not_resource', __('该内容不是可下载资源。', 'aksrt')); } $uid = $user_id > 0 ? $user_id : get_current_user_id(); if (!function_exists('aksrt_user_can') || !aksrt_user_can('resource_download', $uid, (int) $p->ID)) { return new WP_Error('aksrt_need_login', __('请先登录后再下载。', 'aksrt'), [ 'status' => 401, 'login_url' => wp_login_url(get_permalink($p)), ]); } // Without Content_Visibility, only login gate remains. return true; } } if (!function_exists('aksrt_resource_download_cost')) { /** * 下载侧展示用「未解锁积分价」。 * * 现金 pay_modo:永远 0(走订单解锁,不按次扣积分)。 * 积分 pay_modo:未购时返回积分价(用于 UI);已购订单后 0。 * 实际扣费只在收银台创建 resource 订单时发生,下载 token 不再扣积分。 */ function aksrt_resource_download_cost($post = null): int { $p = get_post($post); $uid = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0; // Paid order → unlocked. if ($p instanceof WP_Post && $uid > 0 && class_exists('\AKSRT\Payment\Order')) { if (\AKSRT\Payment\Order::get_instance()->has_paid_access( $uid, \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE, (int) $p->ID )) { return 0; } } if ($p instanceof WP_Post && $uid <= 0 && function_exists('aksrt_guest_has_paid_access') && aksrt_guest_has_paid_access((int) $p->ID)) { return 0; } $priced = function_exists('aksrt_get_user_pay_price') ? aksrt_get_user_pay_price($post, $uid) : null; $pay_modo = is_array($priced) && (($priced['modo'] ?? '0') === 'points') ? 'points' : '0'; if (!is_array($priced) && function_exists('aksrt_get_pay_meta')) { $pay = aksrt_get_pay_meta($post); $pay_modo = (($pay['pay_modo'] ?? '0') === 'points') ? 'points' : '0'; } // Cash mode: order unlock only. if ($pay_modo !== 'points') { return 0; } if ($uid > 0 && function_exists('aksrt_is_download_credit_exempt_user') && aksrt_is_download_credit_exempt_user($uid)) { return 0; } if (is_array($priced)) { $price = (float) ($priced['amount'] ?? 0); return $price > 0 ? (int) ceil($price) : 0; } $meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post) : []; $price = (float) ($meta['points_price'] ?? $meta['price'] ?? 0); return $price > 0 ? (int) ceil($price) : 0; } } if (!function_exists('aksrt_resource_meta_query')) { /** * 「是资源」WP_Query meta_query 片段:post + _aksrt_is_resource = 1。 * * @return array */ function aksrt_resource_meta_query(): array { return [ 'key' => '_aksrt_is_resource', 'value' => '1', 'compare' => '=', ]; } } if (!function_exists('aksrt_resource_archive_url')) { /** * 资源列表页 URL(/resources/)。 */ function aksrt_resource_archive_url(): string { return home_url('/resources/'); } } if (!function_exists('aksrt_is_resource_archive')) { /** * 当前请求是否为资源列表(/resources 虚拟归档)。 */ function aksrt_is_resource_archive(): bool { return (bool) get_query_var('aksrt_is_resource_archive'); } } if (!function_exists('aksrt_detect_cloud_service')) { /** * 从下载链接自动识别云盘服务商。 * * @param string $url 下载链接 * @return array { key:string, name:string, color:string } */ function aksrt_detect_cloud_service(string $url): array { $url_lower = strtolower($url); $services = [ 'baidu' => ['name' => '百度网盘', 'color' => '#1578f8', 'pattern' => ['pan.baidu.com', 'yun.baidu.com']], 'lanzou' => ['name' => '蓝奏云', 'color' => '#f37526', 'pattern' => ['lanzouf.com', 'lanzoui.com', 'lanzoux.com', 'lanzouw.com', 'lanzoup.com', 'lanzout.com', 'lanzou']], 'aliyun' => ['name' => '阿里云盘', 'color' => '#6666ff', 'pattern' => ['alipan.com', 'aliyundrive.com']], 'weiyun' => ['name' => '微云', 'color' => '#4facfe', 'pattern' => ['share.weiyun.com', 'weiyun.com']], 'onedrive' => ['name' => 'OneDrive', 'color' => '#0078d4', 'pattern' => ['1drv.ms', 'onedrive.live.com']], 'tianyi' => ['name' => '天翼云盘', 'color' => '#0099ff', 'pattern' => ['cloud.189.cn']], '123pan' => ['name' => '123云盘', 'color' => '#2b7bff', 'pattern' => ['123pan.com', '123684.com']], 'quark' => ['name' => '夸克网盘', 'color' => '#6a5acd', 'pattern' => ['pan.quark.cn']], 'xunlei' => ['name' => '迅雷云盘', 'color' => '#00a4ff', 'pattern' => ['pan.xunlei.com']], 'mega' => ['name' => 'MEGA', 'color' => '#d92323', 'pattern' => ['mega.nz', 'mega.co.nz']], 'gofile' => ['name' => 'GoFile', 'color' => '#4caf50', 'pattern' => ['gofile.io']], 'mediafire' => ['name' => 'MediaFire', 'color' => '#1294f2', 'pattern' => ['mediafire.com']], 'github' => ['name' => 'GitHub', 'color' => '#333333', 'pattern' => ['github.com', 'raw.githubusercontent.com']], ]; foreach ($services as $key => $svc) { foreach ($svc['pattern'] as $p) { if (strpos($url_lower, $p) !== false) { return ['key' => $key, 'name' => $svc['name'], 'color' => $svc['color']]; } } } return ['key' => 'other', 'name' => __('下载', 'aksrt'), 'color' => '#6b7280']; } } if (!function_exists('aksrt_resource_meta_icon')) { /** * 根据元信息字段返回对应的 Font Awesome 图标类。 */ function aksrt_resource_meta_icon(string $field): string { $icons = [ 'version' => 'fa fas fa-code-branch', 'size' => 'fa fas fa-hdd', 'compat' => 'fa fas fa-laptop', 'license' => 'fa fas fa-shield-alt', 'demo_url' => 'fa fas fa-external-link-alt', 'download' => 'fa fas fa-download', 'views' => 'fa fas fa-eye', ]; return $icons[$field] ?? 'fa fas fa-info-circle'; } }