Files
wordpress-theme-aksrt/inc/helpers/aksrt-resource.php
T

580 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* AKSRT Resource Helper
*
* 资源(CMS)相关的顶层辅助函数。
*
* 提供给模板层调用,将原始 post meta 转成视图友好的结构,
* 同时承担"显示策略"判定(是否显示价格/版本/下载按钮等)。
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!function_exists('aksrt_get_resource_meta')) {
/**
* 一次性取齐资源的全部元数据,已带默认值。
*
* 单次 get_post_meta($id, '', true) 调用 + WordPress 内部 meta 缓存命中,
* 避免 11 次单独查询。
*
* @param int|WP_Post|null $post
* @return array
*/
function aksrt_get_resource_meta($post = null): array {
$p = get_post($post);
if (!($p instanceof WP_Post)) {
return aksrt_default_resource_meta();
}
$pay_meta = function_exists('aksrt_get_pay_meta')
? aksrt_get_pay_meta($p)
: AKSRT_Domain_Model::default_pay_meta();
$download_count = (int) get_post_meta($p->ID, AKSRT_Domain_Model::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_Domain_Model::RESOURCE_VIEW_COUNT_KEY, true);
$common_view_count = (int) get_post_meta($p->ID, AKSRT_Domain_Model::POST_VIEW_COUNT_KEY, true);
return [
'version' => (string) ($pay_meta['version'] ?? ''),
'size' => (string) ($pay_meta['size'] ?? ''),
'price' => (float) ($pay_meta['price'] ?? 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),
'pay_type' => (string) ($pay_meta['pay_type'] ?? AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD),
'license' => (string) ($pay_meta['license'] ?? ''),
'compat' => (string) ($pay_meta['compat'] ?? ''),
'demo_url' => (string) ($pay_meta['demo_url'] ?? ''),
'downloads' => array_values(is_array($pay_meta['downloads'] ?? null) ? $pay_meta['downloads'] : []),
'download_count' => max(0, max($download_count, $legacy_download_count)),
'view_count' => max(0, max($resource_view_count, $common_view_count)),
'require_login' => !empty($pay_meta['require_login']),
'require_captcha' => !empty($pay_meta['require_captcha']),
];
// 一次取出全部 post meta(WP 已缓存到 object cache)
$all = get_post_meta($p->ID);
if (!is_array($all)) $all = [];
$get = static function ($key) use ($all) {
if (!isset($all[$key]) || !is_array($all[$key]) || empty($all[$key])) return null;
return maybe_unserialize($all[$key][0]);
};
$downloads = $get('_aksrt_res_downloads');
if (!is_array($downloads)) $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)));
$resource_download_count = (int) ($get('_aksrt_res_download_count') ?? 0);
$legacy_download_count = (int) ($get('download_count') ?? 0);
$resource_view_count = (int) ($get('_aksrt_res_view_count') ?? 0);
$common_view_count = (int) ($get('aksrt_post_views') ?? 0);
$allowed_pay_types = ['download', 'reading', 'gallery', 'video', 'vip'];
$pay_type = (string) ($get('_aksrt_res_pay_type') ?? 'download');
if (!in_array($pay_type, $allowed_pay_types, true)) {
$pay_type = 'download';
}
return [
'version' => (string) ($get('_aksrt_res_version') ?? ''),
'size' => (string) ($get('_aksrt_res_size') ?? ''),
'price' => (float) ($get('_aksrt_res_price') ?? 0),
'original_price' => (float) ($get('_aksrt_res_original_price') ?? 0),
'vip1_price' => (float) ($get('_aksrt_res_vip1_price') ?? 0),
'vip2_price' => (float) ($get('_aksrt_res_vip2_price') ?? 0),
'pay_type' => $pay_type,
'license' => (string) ($get('_aksrt_res_license') ?? ''),
'compat' => (string) ($get('_aksrt_res_compat') ?? ''),
'demo_url' => (string) ($get('_aksrt_res_demo_url') ?? ''),
'downloads' => array_values($downloads),
'download_count' => max(0, max($resource_download_count, $legacy_download_count)),
'view_count' => max(0, max($resource_view_count, $common_view_count)),
'require_login' => (bool) ($get('_aksrt_res_require_login') ?? false),
'require_captcha' => (bool) ($get('_aksrt_res_require_captcha') ?? false),
];
}
}
if (!function_exists('aksrt_default_resource_meta')) {
function aksrt_default_resource_meta(): array {
$meta = AKSRT_Domain_Model::default_pay_meta();
return [
'version' => (string) ($meta['version'] ?? ''),
'size' => (string) ($meta['size'] ?? ''),
'price' => (float) ($meta['price'] ?? 0),
'original_price' => (float) ($meta['original_price'] ?? 0),
'vip1_price' => (float) ($meta['vip1_price'] ?? 0),
'vip2_price' => (float) ($meta['vip2_price'] ?? 0),
'pay_type' => AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD,
'license' => (string) ($meta['license'] ?? ''),
'compat' => (string) ($meta['compat'] ?? ''),
'demo_url' => (string) ($meta['demo_url'] ?? ''),
'downloads' => [],
'download_count' => 0,
'view_count' => 0,
'require_login' => !empty($meta['require_login']),
'require_captcha' => !empty($meta['require_captcha']),
];
return [
'version' => '',
'size' => '',
'price' => 0.0,
'original_price' => 0.0,
'vip1_price' => 0.0,
'vip2_price' => 0.0,
'pay_type' => 'download',
'license' => '',
'compat' => '',
'demo_url' => '',
'downloads' => [],
'download_count' => 0,
'view_count' => 0,
'require_login' => false,
'require_captcha' => false,
];
}
}
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')) {
/**
* Resource badge text used in mixed post cards.
*
* Only resource-type posts should render this badge.
*
* @param int|WP_Post|null $post
* @return string
*/
function aksrt_resource_card_badge_text($post = null): string {
if (!function_exists('aksrt_is_resource_post') || !aksrt_is_resource_post($post)) {
return '';
}
$meta = aksrt_get_resource_meta($post);
$price = (float) $meta['price'];
if ($price <= 0) {
// 免费资源不在卡片上显示价格 chip。
return '';
}
$currency = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
return sprintf(__('付费 %s %s', 'aksrt'), aksrt_resource_price_amount($price), $currency);
}
}
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-images'],
'video' => ['label' => __('付费视频', 'aksrt'), 'color' => '#8B5CF6', 'icon' => 'fa-play-circle'],
'vip' => ['label' => __('VIP 专属', 'aksrt'), 'color' => '#3B82F6', 'icon' => 'fa-crown'],
];
if ($pay_type === 'download' || !isset($badges[$pay_type])) {
return '';
}
$b = $badges[$pay_type];
return sprintf(
'<span class="resource-pay-type-badge" style="background:%s"><i class="fas %s"></i> %s</span>',
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_Post_Meta_Box')) {
$cover_images = (string) AKSRT_Post_Meta_Box::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')) {
return false;
}
$paid_settings = AKSRT_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')) {
/**
* 是否可下载(P3 阶段:登录 + 积分校验已接入;P5 接入验证码)。
*
* 返回 WP_Error 表示拒绝;返回 true 表示放行。
*
* @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) || !aksrt_is_resource_post($p)) {
return new WP_Error('aksrt_not_resource', __('该内容不是可下载资源。', 'aksrt'));
}
$meta = aksrt_get_resource_meta($p);
$paid_settings = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_paid_content_settings()
: [
'download_login' => true,
'download_credit' => true,
];
// 登录门槛:全局开关 OR 单条覆盖
$need_login = !empty($paid_settings['download_login']) || !empty($meta['require_login']);
$uid = $user_id > 0 ? $user_id : get_current_user_id();
if ($need_login && $uid <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录后再下载。', 'aksrt'), [
'status' => 401,
'login_url' => wp_login_url(get_permalink($p)),
]);
}
// 积分门槛:仅在 cms_features.download_credit 开启 + 实际成本 > 0 + 登录态时才校验
$credit_enabled = !empty($paid_settings['download_credit']);
if ($credit_enabled) {
$cost = (int) aksrt_resource_download_cost($p);
if ($cost > 0) {
if ($uid <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录后再下载。', 'aksrt'), [
'status' => 401,
'login_url' => wp_login_url(get_permalink($p)),
]);
}
$balance = class_exists('AKSRT_Credits') ? AKSRT_Credits::balance($uid) : 0;
if ($balance < $cost) {
return new WP_Error('aksrt_credits_insufficient', __('积分不足,无法下载。', 'aksrt'), [
'status' => 402,
'balance' => $balance,
'cost' => $cost,
]);
}
}
}
// P5 阶段:验证码校验(在请求时校验,不在 can_download 静态判定中拦截)
return true;
}
}
if (!function_exists('aksrt_resource_download_cost')) {
/**
* 取实际需要扣除的积分(向上取整),未启用积分门槛时返回 0。
*
* 优先级:单条 _aksrt_res_price > 全局 download_default_cost。
*/
function aksrt_resource_download_cost($post = null): int {
$meta = aksrt_get_resource_meta($post);
$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;
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 ($price > 0) {
return (int) ceil($price);
}
// 单条未设价格 → 回退到全局默认价
return max(0, (int) ($paid_settings['default_cost'] ?? 0));
}
}
if (!function_exists('aksrt_resource_meta_query')) {
/**
* 「是资源」WP_Query meta_query 片段。
*
* v3.4: 资源 = post + _aksrt_is_resource = 1。
* 兼容历史 aksrt_resource CPT 时由调用方决定是否额外加 post_type。
*
* @return array
*/
function aksrt_resource_meta_query(): array {
return [
'key' => '_aksrt_is_resource',
'value' => '1',
'compare' => '=',
];
}
}
if (!function_exists('aksrt_resource_post_types')) {
/**
* 资源查询使用的 post_type 数组。
*
* 当历史 aksrt_resource CPT 仍在站点中存在(未迁移完成)时,会自动包含。
*
* @return array
*/
function aksrt_resource_post_types(): array {
$types = ['post'];
if (post_type_exists('aksrt_resource')) {
$types[] = 'aksrt_resource';
}
return $types;
}
}
if (!function_exists('aksrt_resource_archive_url')) {
/**
* 取资源归档页 URL(v3.4 起统一为 /resources/)。
*/
function aksrt_resource_archive_url(): string {
return home_url('/resources/');
}
}
if (!function_exists('aksrt_is_resource_archive')) {
/**
* 当前请求是否为资源归档(/resources)。
*
* 兼容历史 aksrt_resource CPT 归档。
*/
function aksrt_is_resource_archive(): bool {
if (function_exists('post_type_exists') && post_type_exists('aksrt_resource')
&& function_exists('is_post_type_archive') && is_post_type_archive('aksrt_resource')) {
return true;
}
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' => 'fas fa-code-branch',
'size' => 'fas fa-hdd',
'compat' => 'fas fa-laptop',
'license' => 'fas fa-shield-alt',
'demo_url' => 'fas fa-external-link-alt',
'download' => 'fas fa-download',
'views' => 'fas fa-eye',
];
return $icons[$field] ?? 'fas fa-info-circle';
}
}