Files
wordpress-theme-aksrt/inc/helpers/aksrt-resource.php
T
kate522 abe2f57417 feat(pay): wire VIP-tier pricing into cost, gates, and download box
Resolve user-facing pay amounts via aksrt_get_user_pay_price (cash/points +
VIP1/2 tiers), surface pay_modo/pay_limit through download meta and REST policy,
and tighten content-visibility / download-box UX for member-only and purchase
gates while improving local stream throttle math.
2026-07-14 11:13:17 +08:00

609 lines
24 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);
// 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_Domain_Model::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_Domain_Model::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_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,
'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') ? 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);
}
$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);
}
}
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 {
$p = get_post($post);
$uid = function_exists('get_current_user_id') ? (int) get_current_user_id() : 0;
// Paid order → no further credit deduct. Avoid has_paid_access() here (recursion via access state).
if ($p instanceof WP_Post && $uid > 0 && class_exists('AKSRT_Order')) {
if (AKSRT_Order::get_instance()->has_paid_access(
$uid,
AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
(int) $p->ID
)) {
return 0;
}
}
$paid_settings = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_paid_content_settings()
: [
'download_credit' => true,
'default_cost' => 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/checkout path unless legacy download_credit is on.
if ($pay_modo !== 'points' && empty($paid_settings['download_credit'])) {
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);
if ($price > 0) {
return (int) ceil($price);
}
return 0;
}
// Fallback without helper.
$meta = function_exists('aksrt_get_pay_meta') ? aksrt_get_pay_meta($post) : aksrt_get_resource_meta($post);
if ($pay_modo === 'points') {
$price = (float) ($meta['points_price'] ?? $meta['price'] ?? 0);
} else {
$price = (float) ($meta['price'] ?? 0);
}
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 片段: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' => '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';
}
}