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

563 lines
22 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\Core\DomainModel::default_pay_meta();
$download_count = (int) get_post_meta($p->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 (Zibll zib_get_posts_list_pay_tags 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 (Zibll 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(
'<span class="resource-pay-type-badge" style="background:%s"><i class="fa %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\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';
}
}