导入 AKSRT 主题开发源码

This commit is contained in:
Claude Code committed 2026-07-03 22:50:34 +08:00
1 parent 9681dd3d48
commit 1268a55dd0
595 files changed
+127705

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
<?php
/**
* AKSRT Credits Helper
*
* 模板层使用的顶层辅助函数:
* aksrt_user_credits($user_id = 0) 读取余额
* aksrt_format_credits(int $n) 格式化展示
* aksrt_credits_label(string $reason) 流水原因 → 友好文案
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!function_exists('aksrt_user_credits')) {
/**
* 取用户当前积分余额。
*
* @param int $user_id 0 表示当前登录用户
* @return int
*/
function aksrt_user_credits(int $user_id = 0): int {
if ($user_id <= 0) $user_id = get_current_user_id();
if ($user_id <= 0) return 0;
if (!class_exists('AKSRT_Credits')) return 0;
return AKSRT_Credits::balance($user_id);
}
}
if (!function_exists('aksrt_format_credits')) {
function aksrt_format_credits(int $n): string {
return number_format_i18n($n);
}
}
if (!function_exists('aksrt_credits_label')) {
/**
* 把 reason 字段翻译成展示文案。
*/
function aksrt_credits_label(string $reason): string {
$map = [
'register' => __('注册赠送', 'aksrt'),
'signin' => __('每日签到', 'aksrt'),
'comment' => __('发表评论', 'aksrt'),
'invite' => __('邀请奖励', 'aksrt'),
'invitee' => __('被邀请奖励', 'aksrt'),
'download' => __('下载消费', 'aksrt'),
'admin' => __('管理员调整', 'aksrt'),
'refund' => __('退款', 'aksrt'),
];
return $map[$reason] ?? $reason;
}
}
+423
View File
@@ -0,0 +1,423 @@
<?php
/**
* AKSRT Domain Model Registry
*
* Canonical keys, defaults, and state machines shared by helpers and services.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Domain_Model {
public const POST_META_KEY = 'aksrt_post_meta';
public const TERM_META_KEY = 'aksrt_term_meta';
public const PAY_META_KEY = '_aksrt_pay';
public const PAY_TYPE_KEY = '_aksrt_pay_type';
public const PAY_PRICE_KEY = '_aksrt_pay_price';
public const RESOURCE_MARKER_KEY = '_aksrt_is_resource';
public const RESOURCE_DOWNLOAD_COUNT_KEY = '_aksrt_res_download_count';
public const RESOURCE_VIEW_COUNT_KEY = '_aksrt_res_view_count';
public const POST_VIEW_COUNT_KEY = 'aksrt_post_views';
public const ORDER_MIGRATED_OPTION = 'aksrt_order_migrated_to_table';
public const RESOURCE_PAY_META_MIGRATED = 'aksrt_resource_pay_meta_migrated';
public const ORDER_PRODUCT_RESOURCE = 'resource';
public const ORDER_PRODUCT_VIP = 'vip';
public const ORDER_PRODUCT_RECHARGE = 'recharge';
public const ORDER_PRODUCT_SHOP = 'shop_product';
public const PAY_TYPE_NO = 'no';
public const PAY_TYPE_READING = 'reading';
public const PAY_TYPE_DOWNLOAD = 'download';
public const PAY_TYPE_GALLERY = 'gallery';
public const PAY_TYPE_VIDEO = 'video';
public const PAY_TYPE_VIP = 'vip';
public const ORDER_STATUS_CREATED = 'created';
public const ORDER_STATUS_PENDING = 'pending';
public const ORDER_STATUS_PAID = 'paid';
public const ORDER_STATUS_DELIVERED = 'delivered';
public const ORDER_STATUS_COMPLETED = 'completed';
public const ORDER_STATUS_CANCELLED = 'cancelled';
public const ORDER_STATUS_EXPIRED = 'expired';
public const ORDER_STATUS_REFUNDING = 'refunding';
public const ORDER_STATUS_REFUNDED = 'refunded';
public const ORDER_STATUS_PENDING_REVIEW = 'pending_review';
public const SHIPPING_STATUS_PENDING = 0;
public const SHIPPING_STATUS_SHIPPED = 1;
public const SHIPPING_STATUS_COMPLETED = 2;
/**
* @return array<string, mixed>
*/
public static function default_post_meta(): array {
return [
'cover_images' => '',
'custom_excerpt' => '',
'layout_override' => '',
'header_image_mode' => '',
'show_toc' => '',
'show_reading_progress' => '',
'enable_lightbox' => '',
'show_related' => '',
'related_count' => 0,
'enable_like' => '',
'enable_favorite' => '',
'enable_share' => '',
'enable_print' => '',
'enable_pdf' => '',
'copyright_text' => '',
'is_featured' => false,
// §3.8 文章扩展字段
'thumbnail_url' => '',
'featured_slide' => '',
'featured_video' => '',
'featured_video_episode' => 0,
'subtitle' => '',
'views' => 0,
'likes' => 0,
'maxheight' => 0,
];
}
/**
* @return array<string, mixed>
*/
public static function default_term_meta(): array {
return [
'cover_url' => '',
'description' => '',
'layout_mode' => '',
'show_sidebar' => '',
'show_header' => true,
'show_featured' => false,
];
}
/**
* @return array<string, mixed>
*/
public static function default_pay_meta(): array {
return [
'pay_type' => self::PAY_TYPE_NO,
'price' => 0.0,
'original_price' => 0.0,
'vip1_price' => 0.0,
'vip2_price' => 0.0,
'version' => '',
'size' => '',
'license' => '',
'compat' => '',
'demo_url' => '',
'video_url' => '',
'downloads' => [],
'attributes' => [],
'gallery' => [],
'require_login' => false,
'require_captcha' => false,
// §3.8 电商增强字段
'virtual_sales' => 0,
'stock' => -1,
'card_keys' => [],
'purchase_limit' => 0,
'free_preview' => '',
'show_sales' => true,
];
}
/**
* @return array<string, string>
*/
public static function pay_type_labels(): array {
return [
self::PAY_TYPE_NO => '不付费',
self::PAY_TYPE_READING => '付费阅读',
self::PAY_TYPE_DOWNLOAD => '付费下载',
self::PAY_TYPE_GALLERY => '付费图库',
self::PAY_TYPE_VIDEO => '付费视频',
self::PAY_TYPE_VIP => 'VIP 专属',
];
}
public static function normalize_pay_type(string $pay_type): string {
$pay_type = sanitize_key($pay_type);
return array_key_exists($pay_type, self::pay_type_labels()) ? $pay_type : self::PAY_TYPE_NO;
}
public static function is_resource_pay_type(string $pay_type): bool {
return self::normalize_pay_type($pay_type) === self::PAY_TYPE_DOWNLOAD;
}
/**
* @param mixed $value
* @return array<int, array<string, string>>
*/
public static function sanitize_downloads($value): array {
if (!is_array($value)) {
return [];
}
$allowed_types = ['direct', 'net_disk', 'external'];
$downloads = [];
foreach ($value as $row) {
if (!is_array($row)) {
continue;
}
$url = isset($row['url']) ? esc_url_raw((string) $row['url']) : '';
if ($url === '') {
continue;
}
$type = isset($row['type']) && in_array($row['type'], $allowed_types, true)
? (string) $row['type']
: 'direct';
$extract_code = '';
foreach ([
'extract_code',
'extractCode',
'extract',
'extract_key',
'extraction_code',
'extractionCode',
'code',
'password',
'pwd',
'pan_code',
'panCode',
'pan_password',
'panPassword',
'access_code',
'accessCode',
] as $extract_key) {
if (isset($row[$extract_key]) && !is_array($row[$extract_key])) {
$candidate = trim((string) $row[$extract_key]);
if ($candidate !== '') {
$extract_code = sanitize_text_field($candidate);
break;
}
}
}
$downloads[] = [
'label' => isset($row['label']) ? sanitize_text_field((string) $row['label']) : '',
'url' => $url,
'type' => $type,
'extract_code' => $extract_code,
];
}
return array_values($downloads);
}
/**
* @param mixed $value
* @return array<int, array<string, string>>
*/
public static function sanitize_attributes($value): array {
if (!is_array($value)) {
return [];
}
$attributes = [];
foreach ($value as $row) {
if (!is_array($row)) {
continue;
}
$key = sanitize_text_field((string) ($row['key'] ?? ''));
$item_value = sanitize_text_field((string) ($row['value'] ?? ''));
if ($key === '' && $item_value === '') {
continue;
}
$attributes[] = [
'key' => $key,
'value' => $item_value,
];
}
return array_values($attributes);
}
/**
* @param mixed $value
* @return string[]
*/
public static function sanitize_gallery($value): array {
if (!is_array($value)) {
return [];
}
$gallery = [];
foreach ($value as $url) {
$url = esc_url_raw((string) $url);
if ($url !== '') {
$gallery[] = $url;
}
}
return array_values(array_unique($gallery));
}
/**
* Sanitize card/key codes for auto-delivery.
* Each entry is a non-empty sanitized string (one code per line).
*/
public static function sanitize_card_keys($value): array {
if (!is_array($value)) {
return [];
}
$keys = [];
foreach ($value as $key) {
$key = sanitize_text_field((string) $key);
if ($key !== '') {
$keys[] = $key;
}
}
return $keys;
}
/**
* @param array<string, mixed> $raw
* @return array<string, mixed>
*/
public static function sanitize_pay_meta(array $raw): array {
$meta = self::default_pay_meta();
$meta['pay_type'] = self::normalize_pay_type((string) ($raw['pay_type'] ?? $meta['pay_type']));
$meta['price'] = max(0, (float) ($raw['price'] ?? 0));
$meta['original_price'] = max(0, (float) ($raw['original_price'] ?? 0));
$meta['vip1_price'] = max(0, (float) ($raw['vip1_price'] ?? 0));
$meta['vip2_price'] = max(0, (float) ($raw['vip2_price'] ?? 0));
$meta['version'] = sanitize_text_field((string) ($raw['version'] ?? ''));
$meta['size'] = sanitize_text_field((string) ($raw['size'] ?? ''));
$meta['license'] = sanitize_text_field((string) ($raw['license'] ?? ''));
$meta['compat'] = sanitize_text_field((string) ($raw['compat'] ?? ''));
$meta['demo_url'] = esc_url_raw((string) ($raw['demo_url'] ?? ''));
$meta['video_url'] = esc_url_raw((string) ($raw['video_url'] ?? ''));
$meta['downloads'] = self::sanitize_downloads($raw['downloads'] ?? []);
$meta['attributes'] = self::sanitize_attributes($raw['attributes'] ?? []);
$meta['gallery'] = self::sanitize_gallery($raw['gallery'] ?? []);
$meta['require_login'] = !empty($raw['require_login']);
$meta['require_captcha'] = !empty($raw['require_captcha']);
// §3.8 电商增强字段
$meta['virtual_sales'] = max(0, (int) ($raw['virtual_sales'] ?? 0));
$meta['stock'] = (int) ($raw['stock'] ?? -1);
$meta['card_keys'] = self::sanitize_card_keys($raw['card_keys'] ?? []);
$meta['purchase_limit'] = max(0, (int) ($raw['purchase_limit'] ?? 0));
$meta['free_preview'] = wp_kses_post((string) ($raw['free_preview'] ?? ''));
$meta['show_sales'] = !isset($raw['show_sales']) || !empty($raw['show_sales']);
return $meta;
}
/**
* @return array<string, string>
*/
public static function order_status_labels(): array {
return [
self::ORDER_STATUS_CREATED => __('已创建', 'aksrt'),
self::ORDER_STATUS_PENDING => __('待支付', 'aksrt'),
self::ORDER_STATUS_PAID => __('已支付', 'aksrt'),
self::ORDER_STATUS_DELIVERED => __('已发货', 'aksrt'),
self::ORDER_STATUS_COMPLETED => __('已完成', 'aksrt'),
self::ORDER_STATUS_CANCELLED => __('已取消', 'aksrt'),
self::ORDER_STATUS_EXPIRED => __('已过期', 'aksrt'),
self::ORDER_STATUS_REFUNDING => __('退款中', 'aksrt'),
self::ORDER_STATUS_REFUNDED => __('已退款', 'aksrt'),
self::ORDER_STATUS_PENDING_REVIEW => __('待评价', 'aksrt'),
];
}
/**
* @return array<string, string[]>
*/
public static function order_transitions(): array {
return [
self::ORDER_STATUS_CREATED => [
self::ORDER_STATUS_PENDING,
self::ORDER_STATUS_CANCELLED,
],
self::ORDER_STATUS_PENDING => [
self::ORDER_STATUS_PAID,
self::ORDER_STATUS_CANCELLED,
self::ORDER_STATUS_EXPIRED,
],
self::ORDER_STATUS_PAID => [
self::ORDER_STATUS_DELIVERED,
self::ORDER_STATUS_COMPLETED,
self::ORDER_STATUS_REFUNDING,
],
self::ORDER_STATUS_DELIVERED => [
self::ORDER_STATUS_COMPLETED,
self::ORDER_STATUS_PENDING_REVIEW,
self::ORDER_STATUS_REFUNDING,
],
self::ORDER_STATUS_COMPLETED => [
self::ORDER_STATUS_PENDING_REVIEW,
self::ORDER_STATUS_REFUNDING,
],
self::ORDER_STATUS_CANCELLED => [],
self::ORDER_STATUS_EXPIRED => [
self::ORDER_STATUS_PENDING,
],
self::ORDER_STATUS_REFUNDING => [
self::ORDER_STATUS_REFUNDED,
self::ORDER_STATUS_PAID,
],
self::ORDER_STATUS_REFUNDED => [],
self::ORDER_STATUS_PENDING_REVIEW => [
self::ORDER_STATUS_COMPLETED,
self::ORDER_STATUS_REFUNDING,
],
];
}
public static function normalize_order_status(string $status, string $default = self::ORDER_STATUS_PENDING): string {
$status = sanitize_key($status);
return array_key_exists($status, self::order_status_labels()) ? $status : $default;
}
public static function can_transition_order_status(string $from, string $to): bool {
$from = self::normalize_order_status($from, self::ORDER_STATUS_CREATED);
$to = self::normalize_order_status($to);
$transitions = self::order_transitions();
return isset($transitions[$from]) && in_array($to, $transitions[$from], true);
}
/**
* @return string[]
*/
public static function paid_order_statuses(): array {
return [
self::ORDER_STATUS_PAID,
self::ORDER_STATUS_DELIVERED,
self::ORDER_STATUS_COMPLETED,
];
}
public static function is_paid_order_status(string $status): bool {
return in_array(self::normalize_order_status($status), self::paid_order_statuses(), true);
}
/**
* @return string[]
*/
public static function vip_active_order_statuses(): array {
return self::paid_order_statuses();
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
/**
* AKSRT Flow Helper
*
* v4 起主题统一为 CMS / 资源站形态,不再提供 blog 形态分流。
* 历史 helper 全部保留签名以保证兼容,但实际行为已收敛:
*
* aksrt_get_site_profile() 始终返回 'cms'
* aksrt_get_term_flow() 始终返回 'inherit'(不再读取 term meta)
* aksrt_is_resource_post() 仍按 _aksrt_is_resource meta 判定
* aksrt_get_post_flow() 始终返回 'cms'
* aksrt_is_cms_mode() 始终返回 true
* aksrt_flow_layout_for() 始终返回 'grid-cms'
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!function_exists('aksrt_get_site_profile')) {
/**
* @deprecated v4 起恒为 'cms',保留仅为向后兼容。
* @return string 'cms'
*/
function aksrt_get_site_profile(): string {
return 'cms';
}
}
if (!function_exists('aksrt_get_term_flow')) {
/**
* @deprecated v4 起恒为 'inherit',term meta 不再生效。
* @param int $term_id
* @return string
*/
function aksrt_get_term_flow(int $term_id): string {
unset($term_id);
return 'inherit';
}
}
if (!function_exists('aksrt_is_resource_post')) {
/**
* 是否为资源(post + _aksrt_is_resource meta,或历史 aksrt_resource CPT)。
*
* @param int|WP_Post|null $post
* @return bool
*/
function aksrt_is_resource_post($post = null): bool {
$p = get_post($post);
if (!($p instanceof WP_Post)) return false;
if ($p->post_type === 'aksrt_resource') return true;
if ($p->post_type !== 'post') return false;
return (bool) get_post_meta($p->ID, '_aksrt_is_resource', true);
}
}
if (!function_exists('aksrt_get_post_flow')) {
/**
* @deprecated v4 起恒为 'cms'。
*
* @param int|WP_Post|null $post
* @return string 'cms'
*/
function aksrt_get_post_flow($post = null): string {
unset($post);
return 'cms';
}
}
if (!function_exists('aksrt_is_cms_mode')) {
/**
* 当前请求是否处在 CMS 形态。v4 起全站统一 CMS。
*
* @return bool
*/
function aksrt_is_cms_mode(): bool {
return true;
}
}
if (!function_exists('aksrt_flow_layout_for')) {
/**
* @deprecated v4 起恒为 'grid-cms'。
*
* @param string $flow
* @return string
*/
function aksrt_flow_layout_for(string $flow): string {
unset($flow);
return 'grid-cms';
}
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+149
View File
@@ -0,0 +1,149 @@
<?php
/**
* AKSRT i18n Switch — 多语言切换辅助函数
*
* 支持 Polylang、WPML 或 WordPress 内置语言切换。
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
/**
* 获取可用语言列表
*
* @return array<array{code: string, name: string, url: string, flag: string, active: bool}>
*/
function aksrt_get_available_languages(): array {
$languages = [];
// Polylang
if (function_exists('pll_the_languages')) {
$pll_langs = pll_the_languages(['raw' => 1]);
if (is_array($pll_langs)) {
foreach ($pll_langs as $lang) {
$languages[] = [
'code' => $lang['slug'] ?? '',
'name' => $lang['name'] ?? '',
'url' => $lang['url'] ?? '#',
'flag' => $lang['flag'] ?? '',
'active' => !empty($lang['current_lang']),
];
}
}
return $languages;
}
// WPML
if (defined('ICL_LANGUAGE_CODE') && function_exists('icl_get_languages')) {
$wpml_langs = icl_get_languages('skip_missing=0');
if (is_array($wpml_langs)) {
foreach ($wpml_langs as $lang) {
$languages[] = [
'code' => $lang['language_code'] ?? '',
'name' => $lang['native_name'] ?? '',
'url' => $lang['url'] ?? '#',
'flag' => $lang['country_flag_url'] ?? '',
'active' => !empty($lang['active']),
];
}
}
return $languages;
}
// WordPress 内置:通过 filter 提供语言列表
$filtered = apply_filters('aksrt_available_languages', []);
if (is_array($filtered) && !empty($filtered)) {
return $filtered;
}
return [];
}
/**
* 检查是否有多语言插件可用
*/
function aksrt_has_multilingual(): bool {
if (function_exists('pll_the_languages')) return true;
if (defined('ICL_LANGUAGE_CODE')) return true;
if (!empty(apply_filters('aksrt_available_languages', []))) return true;
return false;
}
/**
* 获取当前语言代码
*/
function aksrt_get_current_language(): string {
if (function_exists('pll_current_language')) {
return pll_current_language('slug');
}
if (defined('ICL_LANGUAGE_CODE')) {
return ICL_LANGUAGE_CODE;
}
return get_locale();
}
/**
* 渲染语言切换器 HTML
*
* @param string $style dropdown | inline | flags
* @return string
*/
function aksrt_render_language_switcher(string $style = 'dropdown'): string {
$languages = aksrt_get_available_languages();
if (count($languages) < 2) return '';
$current = aksrt_get_current_language();
if ($style === 'flags') {
$html = '<div class="aksrt-lang-switch aksrt-lang-switch--flags">';
foreach ($languages as $lang) {
$active_class = $lang['active'] ? ' is-active' : '';
$html .= '<a href="' . esc_url($lang['url']) . '" class="aksrt-lang-switch__flag' . esc_attr($active_class) . '" title="' . esc_attr($lang['name']) . '">';
if ($lang['flag']) {
$html .= '<img src="' . esc_url($lang['flag']) . '" alt="' . esc_attr($lang['code']) . '" width="20" height="14">';
} else {
$html .= '<span>' . esc_html(strtoupper($lang['code'])) . '</span>';
}
$html .= '</a>';
}
$html .= '</div>';
return $html;
}
// dropdown style
$html = '<div class="aksrt-lang-switch">';
$html .= '<button type="button" class="aksrt-lang-switch__toggle" aria-haspopup="true" aria-expanded="false">';
// 当前语言
foreach ($languages as $lang) {
if ($lang['active'] || $lang['code'] === $current) {
if ($lang['flag']) {
$html .= '<img src="' . esc_url($lang['flag']) . '" alt="" width="18" height="12"> ';
}
$html .= '<span>' . esc_html($lang['name']) . '</span>';
break;
}
}
$html .= ' <i class="fas fa-chevron-down" style="font-size:10px;margin-left:4px;"></i>';
$html .= '</button>';
$html .= '<ul class="aksrt-lang-switch__dropdown" role="menu">';
foreach ($languages as $lang) {
$active_class = $lang['active'] ? ' is-active' : '';
$html .= '<li role="menuitem">';
$html .= '<a href="' . esc_url($lang['url']) . '" class="aksrt-lang-switch__item' . esc_attr($active_class) . '">';
if ($lang['flag']) {
$html .= '<img src="' . esc_url($lang['flag']) . '" alt="" width="18" height="12"> ';
}
$html .= esc_html($lang['name']);
$html .= '</a>';
$html .= '</li>';
}
$html .= '</ul>';
$html .= '</div>';
return $html;
}
+121
View File
@@ -0,0 +1,121 @@
<?php
/**
* AKSRT Theme - SVG Icons Module
*
* 本地 SVG 图标读取/缓存、图标 key 映射显示名称、
* 联系方式图标下拉选项渲染
*
* @package AKSRT
* @version 3.1.0
*/
defined('ABSPATH') || exit;
// ============================================================
// PHP 7.x 兼容辅助函数(替代 PHP 8.0+ 的 match 表达式)
// ============================================================
if (!function_exists('aksrt_parse_country_list')):
/**
* 解析 GeoIP 屏蔽国家列表字符串为数组
*
* @param string $raw 逗号分隔的国家代码
* @return array 大写国家代码数组
*/
function aksrt_parse_country_list($raw) {
if (empty($raw)) return array();
$parts = explode(',', wp_unslash($raw));
$result = array();
foreach ($parts as $p) {
$trimmed = trim($p);
if ($trimmed !== '') {
$result[] = strtoupper($trimmed);
}
}
return array_values(array_filter($result));
}
endif;
// ============================================================
// 本地 SVG 图标:读取 SVGs 目录并缓存
// ============================================================
if (!function_exists('aksrt_inline_svg')) :
function aksrt_inline_svg($name) {
// 兼容 FA5 旧数据:fab fa-github / fabfa-github / fa-github → github
$name = preg_replace('/^(fab|fas|far)[\s-]*/', '', $name);
$name = preg_replace('/^fa[\s-]*/', '', $name);
$name = preg_replace('/^[\s-]+/', '', $name);
$name = trim($name, '- ');
static $cache = array();
if (isset($cache[$name])) return $cache[$name];
$path = AKSRT_DIR . '/assets/img/icons/' . $name . '.svg';
if (file_exists($path)) {
$svg = file_get_contents($path);
// 去除 XML 声明和 DOCTYPE
$svg = preg_replace('/^<\?xml.*?\?>\s*/i', '', $svg);
$svg = preg_replace('/^<!DOCTYPE[^>]*>\s*/i', '', $svg);
// 移除 <svg> 上的固定宽高,让 CSS 控制尺寸
$svg = preg_replace('/\swidth="[^"]*"/', '', $svg);
$svg = preg_replace('/\sheight="[^"]*"/', '', $svg);
// 移除 iconfont 冗余属性
$svg = preg_replace('/\sclass="[^"]*"/', '', $svg);
$svg = preg_replace('/\st="[^"]*"/', '', $svg);
$svg = preg_replace('/\sp-id="[^"]*"/', '', $svg);
$svg = preg_replace('/\sxmlns:xlink="[^"]*"/', '', $svg);
// 移除路径上的 opacity 属性
$svg = preg_replace('/(<(?:path|circle|rect|polygon|line|polyline|ellipse)[^>]*)\sopacity="[^"]*"/', '$1', $svg);
$cache[$name] = $svg;
return $svg;
}
return '';
}
endif;
// ============================================================
// 图标 key → 显示名称
// ============================================================
if (!function_exists('aksrt_icon_key_to_name')) :
function aksrt_icon_key_to_name($key) {
$map = array(
'github' => 'GitHub', 'google' => 'Google', 'x' => 'X',
'email' => '邮箱', 'wechat' => '微信', 'qq' => 'QQ',
'weibo' => '微博', 'telegram' => 'Telegram',
'discord' => 'Discord', 'bilibili' => 'Bilibili',
'linkedin' => 'LinkedIn', 'facebook' => 'Facebook',
'youtube' => 'YouTube', 'instagram' => 'Instagram',
'web' => '网站',
);
return isset($map[$key]) ? $map[$key] : ucwords(str_replace('-', ' ', $key));
}
endif;
// ============================================================
// 联系方式图标选项(仅保留选定的 15 个)
// ============================================================
if (!function_exists('aksrt_contact_icon_options')) :
function aksrt_contact_icon_options($selected = '') {
$icons = array(
'github' => 'GitHub',
'google' => 'Google',
'x' => 'X',
'email' => '邮箱',
'wechat' => '微信',
'qq' => 'QQ',
'weibo' => '微博',
'telegram' => 'Telegram',
'discord' => 'Discord',
'bilibili' => 'Bilibili',
'linkedin' => 'LinkedIn',
'facebook' => 'Facebook',
'youtube' => 'YouTube',
'instagram' => 'Instagram',
'web' => '网站',
);
$html = '<option value="">—</option>';
foreach ($icons as $key => $label) {
$sel = $key === $selected ? ' selected' : '';
$html .= '<option value="' . esc_attr($key) . '"' . $sel . '>' . esc_html($label) . '</option>';
}
return $html;
}
endif;
+629
View File
@@ -0,0 +1,629 @@
<?php
/**
* AKSRT Pay System — 统一付费内容系统(1:1 对齐 zibll)
*
* 核心机制:
* - 任何 post 通过 _aksrt_pay meta 成为付费文章
* - pay_type 驱动行为(no/reading/download/gallery/video/vip)
* - 统一的价格、VIP 价、下载源、属性、图库、视频管理
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
// ============================================================
// 付费类型常量
// ============================================================
if (!defined('AKSRT_PAY_TYPE_NO')) {
define('AKSRT_PAY_TYPE_NO', AKSRT_Domain_Model::PAY_TYPE_NO);
}
if (!defined('AKSRT_PAY_TYPE_READING')) {
define('AKSRT_PAY_TYPE_READING', AKSRT_Domain_Model::PAY_TYPE_READING);
}
if (!defined('AKSRT_PAY_TYPE_DOWNLOAD')) {
define('AKSRT_PAY_TYPE_DOWNLOAD', AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD);
}
if (!defined('AKSRT_PAY_TYPE_GALLERY')) {
define('AKSRT_PAY_TYPE_GALLERY', AKSRT_Domain_Model::PAY_TYPE_GALLERY);
}
if (!defined('AKSRT_PAY_TYPE_VIDEO')) {
define('AKSRT_PAY_TYPE_VIDEO', AKSRT_Domain_Model::PAY_TYPE_VIDEO);
}
if (!defined('AKSRT_PAY_TYPE_VIP')) {
define('AKSRT_PAY_TYPE_VIP', AKSRT_Domain_Model::PAY_TYPE_VIP);
}
/**
* 检查用户是否为 VIP 豁免用户(免扣积分下载)。
*
* @param int $user_id
* @return bool
*/
if (!function_exists('aksrt_is_vip_exempt')) {
function aksrt_is_vip_exempt(int $user_id = 0): bool {
if ($user_id <= 0) {
$user_id = get_current_user_id();
}
if ($user_id <= 0) {
return false;
}
// 优先使用 aksrt_is_download_credit_exempt_user(如果已定义)
if (function_exists('aksrt_is_download_credit_exempt_user')) {
return aksrt_is_download_credit_exempt_user($user_id);
}
// 回退:直接检查 VIP 角色
if (function_exists('aksrt_is_vip') && aksrt_is_vip($user_id)) {
return true;
}
return false;
}
}
function aksrt_get_pay_types(): array {
return AKSRT_Domain_Model::pay_type_labels();
return [
AKSRT_PAY_TYPE_NO => __('不付费', 'aksrt'),
AKSRT_PAY_TYPE_READING => __('付费阅读', 'aksrt'),
AKSRT_PAY_TYPE_DOWNLOAD => __('付费下载', 'aksrt'),
AKSRT_PAY_TYPE_GALLERY => __('付费图库', 'aksrt'),
AKSRT_PAY_TYPE_VIDEO => __('付费视频', 'aksrt'),
AKSRT_PAY_TYPE_VIP => __('VIP 专属', 'aksrt'),
];
}
// ============================================================
// 元数据读取
// ============================================================
if (!function_exists('aksrt_get_pay_meta')) {
function aksrt_get_pay_meta($post = null): array {
$p = get_post($post);
if (!($p instanceof WP_Post)) return aksrt_default_pay_meta();
$meta = get_post_meta($p->ID, AKSRT_Domain_Model::PAY_META_KEY, true);
if (is_array($meta) && isset($meta['pay_type'])) {
return wp_parse_args(AKSRT_Domain_Model::sanitize_pay_meta($meta), aksrt_default_pay_meta());
}
return aksrt_default_pay_meta();
}
}
if (!function_exists('aksrt_default_pay_meta')) {
function aksrt_default_pay_meta(): array {
return AKSRT_Domain_Model::default_pay_meta();
return [
'pay_type' => AKSRT_PAY_TYPE_NO,
'price' => 0.0,
'original_price' => 0.0,
'vip1_price' => 0.0,
'vip2_price' => 0.0,
'version' => '',
'size' => '',
'license' => '',
'compat' => '',
'demo_url' => '',
'downloads' => [],
'require_login' => false,
'require_captcha' => false,
'attributes' => [],
'gallery' => [],
'video_url' => '',
];
}
}
// ============================================================
// 状态判断
// ============================================================
if (!function_exists('aksrt_is_paid_post')) {
function aksrt_is_paid_post($post = null): bool {
return aksrt_get_pay_meta($post)['pay_type'] !== AKSRT_PAY_TYPE_NO;
}
}
if (!function_exists('aksrt_is_paid_reading')) {
function aksrt_is_paid_reading($post = null): bool {
return aksrt_get_pay_meta($post)['pay_type'] === AKSRT_PAY_TYPE_READING;
}
}
if (!function_exists('aksrt_is_paid_download')) {
function aksrt_is_paid_download($post = null): bool {
return aksrt_get_pay_meta($post)['pay_type'] === AKSRT_PAY_TYPE_DOWNLOAD;
}
}
if (!function_exists('aksrt_is_paid_gallery')) {
function aksrt_is_paid_gallery($post = null): bool {
return aksrt_get_pay_meta($post)['pay_type'] === AKSRT_PAY_TYPE_GALLERY;
}
}
if (!function_exists('aksrt_is_paid_video')) {
function aksrt_is_paid_video($post = null): bool {
return aksrt_get_pay_meta($post)['pay_type'] === AKSRT_PAY_TYPE_VIDEO;
}
}
if (!function_exists('aksrt_is_vip_only')) {
function aksrt_is_vip_only($post = null): bool {
return aksrt_get_pay_meta($post)['pay_type'] === AKSRT_PAY_TYPE_VIP;
}
}
// ============================================================
// 付费状态检查
// ============================================================
if (!function_exists('aksrt_has_paid_access')) {
function aksrt_has_paid_access($post = null, int $user_id = 0): bool {
$p = get_post($post);
if (!($p instanceof WP_Post)) return true;
// 单一真相:付费访问由 AKSRT_Content_Visibility(基于订单 + VIP)裁决。
if (class_exists('AKSRT_Content_Visibility')) {
$state = AKSRT_Content_Visibility::get_post_access_state($p, $user_id);
return !empty($state['has_paid_access']);
}
// 兜底:仅在 service 不可用时使用,避免离线环境直接拒绝。
$meta = aksrt_get_pay_meta($p);
if ($meta['pay_type'] === AKSRT_PAY_TYPE_NO) return true;
if ((float) $meta['price'] <= 0 && $meta['pay_type'] !== AKSRT_PAY_TYPE_VIP) return true;
$uid = $user_id > 0 ? $user_id : get_current_user_id();
if ($uid > 0 && function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($uid)) return true;
if ($uid > 0 && get_user_meta($uid, '_aksrt_paid_' . $p->ID, true)) return true;
return false;
}
}
if (!function_exists('aksrt_can_access_content')) {
function aksrt_can_access_content($post = null): bool {
$p = get_post($post);
if (!($p instanceof WP_Post)) return true;
if (class_exists('AKSRT_Content_Visibility')) {
return AKSRT_Content_Visibility::can_view_paid_content($p);
}
$pay_type = aksrt_get_pay_meta($p)['pay_type'];
if ($pay_type === AKSRT_PAY_TYPE_NO) return true;
// 只有 reading/gallery/video 需要隐藏内容
if (!in_array($pay_type, [AKSRT_PAY_TYPE_READING, AKSRT_PAY_TYPE_GALLERY, AKSRT_PAY_TYPE_VIDEO], true)) {
return true;
}
return aksrt_has_paid_access($p);
}
}
if (!function_exists('aksrt_can_download')) {
/**
* 下载授权门禁 —— 单一合同。
*
* 实际判定全部交给 AKSRT_Content_Visibility::can_download_resource()
* (由 inc/helpers/aksrt-resource.php 的同名 helper 在更早加载时定义)。
* 此处仅作为该 helper 不可用时的兜底,避免出现第二套下载真相。
*
* @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_invalid', __('无效内容。', 'aksrt'));
}
$meta = aksrt_get_pay_meta($p);
if ($meta['pay_type'] !== AKSRT_PAY_TYPE_DOWNLOAD) {
return new WP_Error('aksrt_not_downloadable', __('该内容不是可下载资源。', 'aksrt'));
}
$paid_settings = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_paid_content_settings()
: ['download_login' => true, 'download_credit' => true];
$uid = $user_id > 0 ? $user_id : get_current_user_id();
if (!empty($paid_settings['download_login']) && $uid <= 0) {
$login_url = wp_login_url(get_permalink($p));
return new WP_Error('aksrt_need_login', __('请先登录。', 'aksrt'), ['login_url' => $login_url]);
}
if ($uid > 0 && aksrt_has_paid_access($p, $uid)) return true;
if ($uid > 0 && function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($uid)) return true;
$cost = aksrt_get_download_cost($p);
if ($cost > 0 && !empty($paid_settings['download_credit'])) {
$balance = function_exists('aksrt_user_credits') ? aksrt_user_credits($uid) : 0;
if ($balance < $cost) {
return new WP_Error('aksrt_credits_insufficient', __('积分不足。', 'aksrt'), [
'balance' => $balance,
'cost' => $cost,
]);
}
}
return true;
}
}
if (!function_exists('aksrt_get_download_cost')) {
function aksrt_get_download_cost($post = null): int {
if (function_exists('aksrt_resource_download_cost')) {
return (int) aksrt_resource_download_cost($post);
}
$meta = aksrt_get_pay_meta($post);
return (int) ceil((float) $meta['price']);
}
}
// ============================================================
// 价格显示
// ============================================================
if (!function_exists('aksrt_get_pay_price_label')) {
function aksrt_get_pay_price_label($post = null): array {
$meta = aksrt_get_pay_meta($post);
$price = (float) $meta['price'];
$currency = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
if ($meta['pay_type'] === AKSRT_PAY_TYPE_VIP) {
return ['text' => __('VIP 专属', 'aksrt'), 'free' => false, 'amount' => 0];
}
if ($price <= 0) {
return ['text' => __('免费', 'aksrt'), 'free' => true, 'amount' => 0];
}
$amount = aksrt_format_price($price);
return ['text' => sprintf(__('%s %s', 'aksrt'), $amount, $currency), 'free' => false, 'amount' => $price];
}
}
if (!function_exists('aksrt_format_price')) {
function aksrt_format_price(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_get_pay_type_badge')) {
function aksrt_get_pay_type_badge(string $pay_type): string {
$badges = [
AKSRT_PAY_TYPE_READING => ['label' => __('付费阅读', 'aksrt'), 'color' => '#EF4444', 'icon' => 'fa-lock'],
AKSRT_PAY_TYPE_DOWNLOAD => ['label' => __('付费下载', 'aksrt'), 'color' => '#F97316', 'icon' => 'fa-download'],
AKSRT_PAY_TYPE_GALLERY => ['label' => __('付费图库', 'aksrt'), 'color' => '#10B981', 'icon' => 'fa-images'],
AKSRT_PAY_TYPE_VIDEO => ['label' => __('付费视频', 'aksrt'), 'color' => '#8B5CF6', 'icon' => 'fa-play-circle'],
AKSRT_PAY_TYPE_VIP => ['label' => __('VIP 专属', 'aksrt'), 'color' => '#3B82F6', 'icon' => 'fa-crown'],
];
if (!isset($badges[$pay_type])) return '';
$b = $badges[$pay_type];
return sprintf(
'<span class="aksrt-pay-badge" style="background:%s"><i class="fas %s"></i> %s</span>',
esc_attr($b['color']), esc_attr($b['icon']), esc_html($b['label'])
);
}
}
// ============================================================
// 付费墙 HTML
// ============================================================
if (!function_exists('aksrt_get_paywall_html')) {
function aksrt_get_paywall_html(WP_Post $post): string {
$meta = aksrt_get_pay_meta($post);
$pay_type = $meta['pay_type'];
$price = (float) $meta['price'];
$currency = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
$type_labels = [
AKSRT_PAY_TYPE_READING => ['icon' => 'fa-lock', 'label' => __('付费阅读', 'aksrt'), 'desc' => __('付费后即可查看完整内容', 'aksrt')],
AKSRT_PAY_TYPE_GALLERY => ['icon' => 'fa-images', 'label' => __('付费图库', 'aksrt'), 'desc' => __('付费后即可查看全部图片', 'aksrt')],
AKSRT_PAY_TYPE_VIDEO => ['icon' => 'fa-play-circle', 'label' => __('付费视频', 'aksrt'), 'desc' => __('付费后即可观看完整视频', 'aksrt')],
];
$type_info = $type_labels[$pay_type] ?? $type_labels[AKSRT_PAY_TYPE_READING];
$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(get_permalink($post));
$is_logged_in = is_user_logged_in();
$original_price = (float) ($meta['original_price'] ?? 0);
$has_original = $original_price > 0 && $original_price > $price;
ob_start();
?>
<div class="aksrt-paywall" data-post-id="<?php echo esc_attr((string) $post->ID); ?>" data-pay-type="<?php echo esc_attr($pay_type); ?>">
<div class="aksrt-paywall__overlay">
<div class="aksrt-paywall__blur"></div>
<div class="aksrt-paywall__card">
<div class="aksrt-paywall__icon">
<i class="fas <?php echo esc_attr($type_info['icon']); ?>" aria-hidden="true"></i>
</div>
<h3 class="aksrt-paywall__title"><?php echo esc_html($type_info['label']); ?></h3>
<p class="aksrt-paywall__desc"><?php echo esc_html($type_info['desc']); ?></p>
<div class="aksrt-paywall__price">
<?php if ($price > 0) : ?>
<span class="aksrt-paywall__price-amount"><?php echo esc_html(aksrt_format_price($price) . ' ' . $currency); ?></span>
<?php if ($has_original) : ?>
<span class="aksrt-paywall__price-original"><?php echo esc_html(aksrt_format_price($original_price) . ' ' . $currency); ?></span>
<?php endif; ?>
<?php else : ?>
<span class="aksrt-paywall__price-free"><?php esc_html_e('免费', 'aksrt'); ?></span>
<?php endif; ?>
</div>
<?php if (!$is_logged_in) : ?>
<a href="<?php echo esc_url($login_url); ?>" class="btn-primary btn-lg aksrt-paywall__btn">
<i class="fas fa-sign-in-alt"></i> <?php esc_html_e('登录后购买', 'aksrt'); ?>
</a>
<?php elseif ($price > 0) : ?>
<button type="button" class="btn-primary btn-lg aksrt-paywall__btn" data-aksrt-pay-content="<?php echo esc_attr((string) $post->ID); ?>">
<i class="fas fa-shopping-cart"></i> <?php printf(esc_html__('立即购买(%s)', 'aksrt'), aksrt_format_price($price) . ' ' . $currency); ?>
</button>
<?php endif; ?>
<p class="aksrt-paywall__hint">
<i class="fas fa-shield-alt"></i> <?php esc_html_e('购买后永久有效,支持重复查看', 'aksrt'); ?>
</p>
</div>
</div>
</div>
<?php
return ob_get_clean();
}
}
// ============================================================
// 付费墙样式注入
// ============================================================
add_action('wp_head', function (): void {
if (is_admin() || !is_singular()) return;
global $post;
if (!($post instanceof WP_Post)) return;
if (!function_exists('aksrt_can_access_content') || aksrt_can_access_content($post)) return;
?>
<style>
.aksrt-paywall{position:relative;margin:24px 0}
.aksrt-paywall__overlay{position:relative}
.aksrt-paywall__blur{filter:blur(12px);pointer-events:none;user-select:none;max-height:200px;overflow:hidden;opacity:.3}
.aksrt-paywall__card{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface-1,#fff);border:1px solid var(--color-line,#e5e7eb);border-radius:var(--radius-lg,14px);padding:40px;text-align:center;box-shadow:0 12px 40px rgba(0,0,0,.12);min-width:320px;max-width:420px}
.aksrt-paywall__icon{font-size:48px;color:var(--color-primary,#f04494);margin-bottom:16px}
.aksrt-paywall__title{font-size:20px;font-weight:700;margin:0 0 8px;color:var(--color-ink,#15171A)}
.aksrt-paywall__desc{font-size:14px;color:var(--color-ink-3,#525866);margin:0 0 20px}
.aksrt-paywall__price{margin-bottom:20px}
.aksrt-paywall__price-amount{font-size:28px;font-weight:700;color:var(--color-error,#EF4444)}
.aksrt-paywall__price-original{font-size:14px;color:var(--color-ink-4,#8A8F98);text-decoration:line-through;margin-left:8px}
.aksrt-paywall__price-free{font-size:20px;font-weight:600;color:var(--color-success,#10B981)}
.aksrt-paywall__btn{display:inline-flex;align-items:center;gap:8px}
.aksrt-paywall__hint{font-size:12px;color:var(--color-ink-4,#8A8F98);margin:16px 0 0}
.aksrt-gallery-locked{filter:blur(8px);pointer-events:none;user-select:none}
</style>
<?php
});
// ============================================================
// 付费图库轮播(Swiper)
// ============================================================
if (!function_exists('aksrt_render_paid_gallery')) {
function aksrt_render_paid_gallery(WP_Post $post): string {
$meta = aksrt_get_pay_meta($post);
if ($meta['pay_type'] !== AKSRT_PAY_TYPE_GALLERY) return '';
$images = $meta['gallery'] ?? [];
if (empty($images)) return '';
$has_access = aksrt_has_paid_access($post);
$base_uri = get_template_directory_uri();
ob_start();
?>
<div class="aksrt-paid-gallery" data-post-id="<?php echo esc_attr((string) $post->ID); ?>">
<?php if ($has_access) : ?>
<!-- 已付费:Swiper 轮播 -->
<div class="swiper aksrt-paid-gallery__swiper">
<div class="swiper-wrapper">
<?php foreach ($images as $img_url) : ?>
<div class="swiper-slide">
<img src="<?php echo esc_url($img_url); ?>" alt="" loading="lazy">
</div>
<?php endforeach; ?>
</div>
<div class="swiper-pagination"></div>
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
<?php else : ?>
<!-- 未付费:模糊预览 + 付费墙 -->
<div class="aksrt-paid-gallery__locked">
<div class="aksrt-paid-gallery__preview">
<img src="<?php echo esc_url($images[0]); ?>" alt="" class="aksrt-gallery-locked">
</div>
<div class="aksrt-paid-gallery__overlay">
<i class="fas fa-images" aria-hidden="true"></i>
<p><?php esc_html_e('付费后即可查看全部图片', 'aksrt'); ?></p>
<?php
$price_label = aksrt_get_pay_price_label($post);
$currency = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
?>
<span class="aksrt-paid-gallery__price">
<?php echo esc_html($price_label['text']); ?>
</span>
</div>
</div>
<?php endif; ?>
</div>
<?php if ($has_access) : ?>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.css">
<script src="https://cdn.jsdelivr.net/npm/swiper@11/swiper-bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
new Swiper('.aksrt-paid-gallery__swiper', {
loop: true,
pagination: { el: '.swiper-pagination', clickable: true },
navigation: { nextEl: '.swiper-button-next', prevEl: '.swiper-button-prev' },
lazy: true,
});
});
</script>
<?php endif; ?>
<?php
return ob_get_clean();
}
}
// ============================================================
// 付费视频播放器
// ============================================================
if (!function_exists('aksrt_render_paid_video')) {
function aksrt_render_paid_video(WP_Post $post): string {
$meta = aksrt_get_pay_meta($post);
if ($meta['pay_type'] !== AKSRT_PAY_TYPE_VIDEO) return '';
$video_url = $meta['video_url'] ?? '';
if (empty($video_url)) return '';
$has_access = aksrt_has_paid_access($post);
ob_start();
?>
<div class="aksrt-paid-video" data-post-id="<?php echo esc_attr((string) $post->ID); ?>">
<?php if ($has_access) : ?>
<!-- 已付费:视频播放器 -->
<div class="aksrt-paid-video__player">
<?php if (preg_match('/\.(mp4|webm|ogg)$/i', $video_url)) : ?>
<video controls preload="metadata" style="width:100%;border-radius:var(--radius-lg);">
<source src="<?php echo esc_url($video_url); ?>">
<?php esc_html_e('您的浏览器不支持视频播放。', 'aksrt'); ?>
</video>
<?php else : ?>
<div class="aksrt-paid-video__embed">
<iframe src="<?php echo esc_url($video_url); ?>" frameborder="0" allowfullscreen></iframe>
</div>
<?php endif; ?>
</div>
<?php else : ?>
<!-- 未付费:锁定预览 -->
<div class="aksrt-paid-video__locked">
<div class="aksrt-paid-video__placeholder">
<i class="fas fa-play-circle" aria-hidden="true"></i>
<p><?php esc_html_e('付费后即可观看完整视频', 'aksrt'); ?></p>
<?php $price_label = aksrt_get_pay_price_label($post); ?>
<span class="aksrt-paid-video__price"><?php echo esc_html($price_label['text']); ?></span>
</div>
</div>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
}
// ============================================================
// 内容过滤钩子(注册在 core-hooks.php)
// ============================================================
// 这些函数供 core-hooks.php 中的 the_content 过滤器调用
if (!function_exists('aksrt_paid_content_filter')) {
function aksrt_paid_content_filter(string $content): string {
if (is_admin() || !is_singular()) return $content;
global $post;
if (!($post instanceof WP_Post)) return $content;
if (aksrt_can_access_content($post)) return $content;
// 图库类型:在内容末尾追加图库轮播
$meta = aksrt_get_pay_meta($post);
if ($meta['pay_type'] === AKSRT_PAY_TYPE_GALLERY) {
return aksrt_get_paywall_html($post) . aksrt_render_paid_gallery($post);
}
// 视频类型:在内容末尾追加视频播放器
if ($meta['pay_type'] === AKSRT_PAY_TYPE_VIDEO) {
return aksrt_get_paywall_html($post) . aksrt_render_paid_video($post);
}
return aksrt_get_paywall_html($post);
}
}
// 已付费的内容增强
if (!function_exists('aksrt_paid_content_enhance')) {
function aksrt_paid_content_enhance(string $content): string {
if (is_admin() || !is_singular()) return $content;
global $post;
if (!($post instanceof WP_Post)) return $content;
$meta = aksrt_get_pay_meta($post);
if ($meta['pay_type'] === AKSRT_PAY_TYPE_NO) return $content;
if (!aksrt_has_paid_access($post)) return $content;
// 已付费的图库:追加 Swiper 轮播
if ($meta['pay_type'] === AKSRT_PAY_TYPE_GALLERY) {
$gallery_html = aksrt_render_paid_gallery($post);
if ($gallery_html) $content .= $gallery_html;
}
// 已付费的视频:追加播放器
if ($meta['pay_type'] === AKSRT_PAY_TYPE_VIDEO) {
$video_html = aksrt_render_paid_video($post);
if ($video_html) $content .= $video_html;
}
return $content;
}
}
// ============================================================
// 付费图库/视频样式
// ============================================================
add_action('wp_head', function (): void {
if (is_admin() || !is_singular()) return;
global $post;
if (!($post instanceof WP_Post)) return;
$meta = aksrt_get_pay_meta($post);
if (!in_array($meta['pay_type'], [AKSRT_PAY_TYPE_GALLERY, AKSRT_PAY_TYPE_VIDEO], true)) return;
?>
<style>
.aksrt-paid-gallery{margin:24px 0}
.aksrt-paid-gallery__swiper{border-radius:var(--radius-lg);overflow:hidden}
.aksrt-paid-gallery__swiper img{width:100%;height:auto;display:block}
.aksrt-paid-gallery__locked{position:relative;border-radius:var(--radius-lg);overflow:hidden}
.aksrt-paid-gallery__preview img{width:100%;display:block}
.aksrt-paid-gallery__overlay{position:absolute;inset:0;background:rgba(0,0,0,.5);display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;gap:12px}
.aksrt-paid-gallery__overlay i{font-size:48px;opacity:.8}
.aksrt-paid-gallery__overlay p{font-size:16px;margin:0}
.aksrt-paid-gallery__price{font-size:20px;font-weight:700;background:rgba(255,255,255,.2);padding:6px 16px;border-radius:20px}
.aksrt-paid-video{margin:24px 0}
.aksrt-paid-video__embed{position:relative;padding-bottom:56.25%;height:0;overflow:hidden;border-radius:var(--radius-lg)}
.aksrt-paid-video__embed iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:none}
.aksrt-paid-video__locked{background:var(--surface-2);border-radius:var(--radius-lg);padding:60px 20px;text-align:center}
.aksrt-paid-video__placeholder i{font-size:64px;color:var(--color-ink-4);margin-bottom:16px}
.aksrt-paid-video__placeholder p{font-size:16px;color:var(--color-ink-3);margin:0 0 12px}
.aksrt-paid-video__price{font-size:20px;font-weight:700;color:var(--color-error)}
</style>
<?php
});
File diff suppressed because it is too large. Load diff
+579
View File
@@ -0,0 +1,579 @@
<?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';
}
}
+255
View File
@@ -0,0 +1,255 @@
<?php
/**
* AKSRT Shortcodes Registry — 短代码统一注册入口
*
* Phase 9 收口:所有主题短代码集中在此注册,不再散落到
* `AKSRT_Content_Visibility` / `AKSRT_Templates` 等业务类的 init() 中。
* 业务渲染仍由各自类/模板实现,registry 只负责「注册 → 委派」。
*
* 详见 shortcode-registry.md。
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!class_exists('AKSRT_Shortcodes')) :
class AKSRT_Shortcodes {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 注册全部短代码。在 `init` 上调用一次。
*/
public function register(): void {
// 隐藏内容(评论/登录/密码可见)→ AKSRT_Content_Visibility
add_shortcode('aksrt_hide', [$this, 'render_hide']);
// §3.8 用户友好短代码别名
add_shortcode('hide', [$this, 'render_hide_comment']);
add_shortcode('reply', [$this, 'render_reply']);
add_shortcode('vip', [$this, 'render_vip']);
add_shortcode('payshow', [$this, 'render_payshow']);
add_shortcode('content', [$this, 'render_content_gate']);
// 文章归档 / 个人资料卡 → AKSRT_Templates 模板
add_shortcode('aksrt_archive', [$this, 'render_archive']);
add_shortcode('aksrt_profile', [$this, 'render_profile']);
// 文章列表(统一渲染器)
add_shortcode('aksrt_posts', [$this, 'render_posts']);
}
/**
* [aksrt_hide mode="comment|login|password"]...[/aksrt_hide]
*/
public function render_hide(array $atts, string $content = ''): string {
if (!class_exists('AKSRT_Content_Visibility')) {
return '';
}
return AKSRT_Content_Visibility::get_instance()->render_hide_shortcode($atts, $content);
}
/**
* [hide]...[/hide] — 评论后可见(默认 mode=comment)
*/
public function render_hide_comment(array $atts = [], string $content = ''): string {
$atts = shortcode_atts(['message' => ''], $atts, 'hide');
return $this->render_hide(['mode' => 'comment', 'message' => $atts['message']], $content);
}
/**
* [reply]...[/reply] — 回复(评论)后可见
*/
public function render_reply(array $atts = [], string $content = ''): string {
$atts = shortcode_atts(['message' => ''], $atts, 'reply');
$msg = $atts['message'] ?: __('回复本文后可查看隐藏内容', 'aksrt');
return $this->render_hide(['mode' => 'comment', 'message' => $msg], $content);
}
/**
* [vip]...[/vip] — VIP 用户可见
*/
public function render_vip(array $atts = [], string $content = ''): string {
$atts = shortcode_atts(['message' => ''], $atts, 'vip');
$uid = get_current_user_id();
$is_vip = $uid > 0 && function_exists('aksrt_is_vip') && aksrt_is_vip($uid);
if ($is_vip) {
return '<div class="aksrt-hidden-content aksrt-hidden-content--unlocked aksrt-hidden-content--vip">'
. do_shortcode($content) . '</div>';
}
$message = $atts['message'] ?: __('此内容仅 VIP 用户可见', 'aksrt');
$html = '<div class="aksrt-hidden-content aksrt-hidden-content--locked aksrt-hidden-content--vip">';
$html .= '<div class="aksrt-hidden-content__message">';
$html .= '<i class="fas fa-crown" aria-hidden="true"></i> ';
$html .= '<span>' . esc_html($message) . '</span>';
$html .= '</div>';
if ($uid <= 0) {
$html .= '<a href="' . esc_url(wp_login_url(get_permalink())) . '" class="aksrt-hidden-content__login-btn btn-primary btn-sm">';
$html .= esc_html__('登录后查看', 'aksrt') . '</a>';
}
$html .= '</div>';
return $html;
}
/**
* [payshow]...[/payshow] — 付费后可见
*/
public function render_payshow(array $atts = [], string $content = ''): string {
$atts = shortcode_atts(['message' => ''], $atts, 'payshow');
$pid = get_the_ID();
$can_view = false;
if ($pid && class_exists('AKSRT_Content_Visibility')) {
$can_view = AKSRT_Content_Visibility::can_view_paid_content($pid);
}
if ($can_view) {
return '<div class="aksrt-hidden-content aksrt-hidden-content--unlocked aksrt-hidden-content--payshow">'
. do_shortcode($content) . '</div>';
}
$message = $atts['message'] ?: __('此内容需付费后查看', 'aksrt');
$uid = get_current_user_id();
$html = '<div class="aksrt-hidden-content aksrt-hidden-content--locked aksrt-hidden-content--payshow">';
$html .= '<div class="aksrt-hidden-content__message">';
$html .= '<i class="fas fa-lock" aria-hidden="true"></i> ';
$html .= '<span>' . esc_html($message) . '</span>';
$html .= '</div>';
if ($uid <= 0) {
$html .= '<a href="' . esc_url(wp_login_url(get_permalink())) . '" class="aksrt-hidden-content__login-btn btn-primary btn-sm">';
$html .= esc_html__('登录后购买查看', 'aksrt') . '</a>';
}
$html .= '</div>';
return $html;
}
/**
* [content type="admin|author|vip"]...[/content] — 按角色/身份控制可见性
*/
public function render_content_gate(array $atts = [], string $content = ''): string {
$atts = shortcode_atts(['type' => 'admin', 'message' => ''], $atts, 'content');
$type = sanitize_key((string) $atts['type']);
$uid = get_current_user_id();
$pid = get_the_ID();
$allowed = false;
switch ($type) {
case 'admin':
$allowed = $uid > 0 && user_can($uid, 'manage_options');
break;
case 'author':
if ($uid > 0 && $pid) {
$post = get_post($pid);
$allowed = $post && (int) $post->post_author === $uid;
}
break;
case 'vip':
$allowed = $uid > 0 && function_exists('aksrt_is_vip') && aksrt_is_vip($uid);
break;
case 'login':
$allowed = $uid > 0;
break;
}
if ($allowed) {
return '<div class="aksrt-hidden-content aksrt-hidden-content--unlocked aksrt-hidden-content--role">'
. do_shortcode($content) . '</div>';
}
$default_msgs = [
'admin' => __('此内容仅管理员可见', 'aksrt'),
'author' => __('此内容仅作者可见', 'aksrt'),
'vip' => __('此内容仅 VIP 用户可见', 'aksrt'),
'login' => __('此内容仅登录用户可见', 'aksrt'),
];
$message = $atts['message'] ?: ($default_msgs[$type] ?? __('此内容已隐藏', 'aksrt'));
$html = '<div class="aksrt-hidden-content aksrt-hidden-content--locked aksrt-hidden-content--role">';
$html .= '<div class="aksrt-hidden-content__message">';
$html .= '<i class="fas fa-eye-slash" aria-hidden="true"></i> ';
$html .= '<span>' . esc_html($message) . '</span>';
$html .= '</div>';
$html .= '</div>';
return $html;
}
/**
* [aksrt_archive] 文章归档
*/
public function render_archive(array $atts = [], string $content = ''): string {
if (!class_exists('AKSRT_Templates')) {
return '';
}
return AKSRT_Templates::get_instance()->render_archive_shortcode($atts, $content);
}
/**
* [aksrt_profile] 个人资料卡
*/
public function render_profile(array $atts = [], string $content = ''): string {
if (!class_exists('AKSRT_Templates')) {
return '';
}
return AKSRT_Templates::get_instance()->render_profile_shortcode($atts, $content);
}
/**
* [aksrt_posts count="6" cat="" tag="" orderby="date" order="DESC" mode="card"]
*
* 用统一渲染器输出文章列表,避免内容里手写卡片循环。
*/
public function render_posts(array $atts = [], string $content = ''): string {
if (!function_exists('aksrt_render_post_list')) {
return '';
}
$a = shortcode_atts([
'count' => 6,
'cat' => '',
'tag' => '',
'orderby' => 'date',
'order' => 'DESC',
'mode' => 'card',
], $atts, 'aksrt_posts');
$query_args = [
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => max(1, min(24, (int) $a['count'])),
'orderby' => sanitize_key((string) $a['orderby']) ?: 'date',
'order' => strtoupper((string) $a['order']) === 'ASC' ? 'ASC' : 'DESC',
'ignore_sticky_posts' => true,
'no_found_rows' => true,
];
$cat = sanitize_text_field((string) $a['cat']);
if ($cat !== '') {
$query_args[is_numeric($cat) ? 'cat' : 'category_name'] = $cat;
}
$tag = sanitize_text_field((string) $a['tag']);
if ($tag !== '') {
$query_args[is_numeric($tag) ? 'tag_id' : 'tag'] = $tag;
}
$mode = in_array($a['mode'], ['card', 'list', 'mini'], true) ? $a['mode'] : 'card';
$query = new WP_Query($query_args);
$html = aksrt_render_post_list($query, [
'context' => 'widget',
'mode' => $mode,
], false);
wp_reset_postdata();
return '<div class="aksrt-shortcode-posts">' . $html . '</div>';
}
}
endif;
+217
View File
@@ -0,0 +1,217 @@
<?php
/**
* AKSRT Slider Helper — 轮播渲染函数
*
* 对标 zibll 的 zib_new_slider(),提供服务端轮播 HTML 生成。
* 支持 data-* 属性驱动配置、视差、懒加载、缩略图分页。
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
/**
* 渲染 Swiper 轮播
*
* @param array $args {
* @type array $slides 幻灯片数据数组
* @type string $effect 动画效果: slide, fade, cube, coverflow, flip (默认 slide)
* @type bool $loop 是否循环 (默认 true)
* @type bool $autoplay 是否自动播放 (默认 true)
* @type int $interval 自动播放间隔 ms (默认 5000)
* @type int $speed 动画速度 ms (0=自动计算)
* @type string $direction 方向: horizontal, vertical (默认 horizontal)
* @type int $spaceBetween 幻灯片间距 px (默认 0)
* @type string $pagination_type 分页器类型: bullets, fraction, progressbar, thumbnail (默认 bullets)
* @type string $pc_height PC 端高度 CSS 值 (默认 auto)
* @type string $m_height 移动端高度 CSS 值 (默认 auto)
* @type bool $scale_height 是否使用比例高度 (默认 false)
* @type float $scale_ratio 比例值 (默认 0.45 = 45%)
* @type string $wrapper_class 额外 wrapper 类
* @type string $slider_class 额外 slider 类
* @type bool $show_nav 是否显示导航箭头 (默认 true)
* @type bool $show_pagination 是否显示分页器 (默认 true)
* }
*
* slide 数据格式:
* [
* 'image' => 'https://...',
* 'link' => 'https://...',
* 'title' => '标题',
* 'desc' => '描述',
* 'alt' => '图片 alt',
* 'bg_color' => '#f0f0f0', // 背景色
* 'target' => '_blank',
* ]
*
* @return string HTML
*/
function aksrt_render_slider(array $args = []): string {
$defaults = [
'slides' => [],
'effect' => 'slide',
'loop' => true,
'autoplay' => true,
'interval' => 5000,
'speed' => 0,
'direction' => 'horizontal',
'spaceBetween' => 0,
'pagination_type' => 'bullets',
'pc_height' => 'auto',
'm_height' => 'auto',
'scale_height' => false,
'scale_ratio' => 0.45,
'wrapper_class' => '',
'slider_class' => '',
'show_nav' => true,
'show_pagination' => true,
];
$args = wp_parse_args($args, $defaults);
$slides = $args['slides'];
if (empty($slides) || !is_array($slides)) {
return '';
}
// 构建 data 属性
$data_attrs = [];
$data_attrs[] = 'data-effect="' . esc_attr($args['effect']) . '"';
if ($args['loop']) $data_attrs[] = 'data-loop';
if ($args['autoplay']) {
$data_attrs[] = 'data-autoplay';
$data_attrs[] = 'data-interval="' . esc_attr((string) $args['interval']) . '"';
}
if ($args['speed'] > 0) $data_attrs[] = 'data-speed="' . esc_attr((string) $args['speed']) . '"';
if ($args['direction'] !== 'horizontal') $data_attrs[] = 'data-direction="' . esc_attr($args['direction']) . '"';
if ($args['spaceBetween'] > 0) $data_attrs[] = 'data-spaceBetween="' . esc_attr((string) $args['spaceBetween']) . '"';
if ($args['pagination_type'] !== 'bullets') $data_attrs[] = 'data-pagination-type="' . esc_attr($args['pagination_type']) . '"';
// 高度样式
$height_style = '';
if ($args['scale_height']) {
$height_style = sprintf('--pc-height:auto;--m-height:auto;--scale-ratio:%s;', esc_attr((string) $args['scale_ratio']));
} else {
$height_style = sprintf('--pc-height:%s;--m-height:%s;', esc_attr($args['pc_height']), esc_attr($args['m_height']));
}
// CSS 类
$slider_classes = ['new-swiper', 'slide-index'];
if ($args['slider_class']) $slider_classes[] = $args['slider_class'];
$slider_class_str = implode(' ', $slider_classes);
$wrapper_classes = ['aksrt-slider-wrapper'];
if ($args['wrapper_class']) $wrapper_classes[] = $args['wrapper_class'];
$wrapper_class_str = implode(' ', $wrapper_classes);
// 渲染 HTML
$html = '<div class="' . esc_attr($wrapper_class_str) . '">';
$html .= '<div class="' . esc_attr($slider_class_str) . '" ' . implode(' ', $data_attrs) . ' style="' . esc_attr($height_style) . '">';
$html .= '<div class="swiper-wrapper">';
foreach ($slides as $slide) {
$image = esc_url($slide['image'] ?? '');
$link = esc_url($slide['link'] ?? '');
$title = sanitize_text_field($slide['title'] ?? '');
$desc = sanitize_text_field($slide['desc'] ?? '');
$alt = sanitize_text_field($slide['alt'] ?? $title);
$bg_color = sanitize_hex_color($slide['bg_color'] ?? '');
$target = $slide['target'] === '_blank' ? '_blank' : '_self';
$slide_style = $bg_color ? 'background-color:' . esc_attr($bg_color) . ';' : '';
$html .= '<div class="swiper-slide"' . ($slide_style ? ' style="' . esc_attr($slide_style) . '"' : '') . '>';
if ($link) {
$html .= '<a href="' . esc_url($link) . '" target="' . esc_attr($target) . '">';
}
if ($image) {
$html .= '<img class="swiper-lazy" data-src="' . esc_url($image) . '" alt="' . esc_attr($alt) . '" loading="lazy">';
$html .= '<div class="swiper-lazy-preloader"></div>';
}
if ($link) {
$html .= '</a>';
}
// 文字覆盖层
if ($title || $desc) {
$html .= '<div class="aksrt-slide-text">';
if ($title) $html .= '<div class="aksrt-slide-text__title">' . esc_html($title) . '</div>';
if ($desc) $html .= '<div class="aksrt-slide-text__desc">' . esc_html($desc) . '</div>';
$html .= '</div>';
}
$html .= '</div>';
}
$html .= '</div>'; // .swiper-wrapper
if ($args['show_nav']) {
$html .= '<div class="swiper-button-prev"></div>';
$html .= '<div class="swiper-button-next"></div>';
}
if ($args['show_pagination']) {
$html .= '<div class="swiper-pagination"></div>';
}
$html .= '</div>'; // .new-swiper
$html .= '</div>'; // .aksrt-slider-wrapper
return $html;
}
/**
* 渲染水平滚动器(Tab 导航、横向内容列表)
*
* @param array $args {
* @type array $items 内容数组
* @type string $wrapper_class 额外 wrapper 类
* @type bool $autoplay 是否自动播放
* @type int $interval 自动播放间隔
* @type string $render_item 自定义渲染回调 (callable)
* }
* @return string HTML
*/
function aksrt_render_scroll_slider(array $args = []): string {
$defaults = [
'items' => [],
'wrapper_class' => '',
'autoplay' => false,
'interval' => 4000,
'render_item' => null,
];
$args = wp_parse_args($args, $defaults);
$items = $args['items'];
if (empty($items)) return '';
$data_attrs = ['class="swiper-scroll' . ($args['wrapper_class'] ? ' ' . esc_attr($args['wrapper_class']) : '') . '"'];
if ($args['autoplay']) {
$data_attrs[] = 'data-autoplay';
$data_attrs[] = 'data-interval="' . esc_attr((string) $args['interval']) . '"';
}
$html = '<div ' . implode(' ', $data_attrs) . '>';
$html .= '<div class="swiper-wrapper">';
foreach ($items as $i => $item) {
$html .= '<div class="swiper-slide">';
if (is_callable($args['render_item'])) {
$html .= call_user_func($args['render_item'], $item, $i);
} else {
$html .= '<div class="aksrt-scroll-item">' . esc_html($item['label'] ?? '') . '</div>';
}
$html .= '</div>';
}
$html .= '</div>';
$html .= '<div class="swiper-button-prev"></div>';
$html .= '<div class="swiper-button-next"></div>';
$html .= '</div>';
return $html;
}
File diff suppressed because it is too large. Load diff
+118
View File
@@ -0,0 +1,118 @@
<?php
/**
* AKSRT Transaction Helpers
*
* Frontend transaction view-model helpers.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
function aksrt_get_order_status_labels(): array {
if (class_exists('AKSRT_Domain_Model')) {
return AKSRT_Domain_Model::order_status_labels();
}
return [];
}
function aksrt_get_product_page_view(int $product_id): array {
if ($product_id <= 0 || !class_exists('AKSRT_Shop')) {
return [];
}
return AKSRT_Shop::get_instance()->get_product_detail_view($product_id);
}
function aksrt_get_product_edit_page_view(int $user_id, int $product_id = 0): array {
if ($user_id <= 0 || !class_exists('AKSRT_Shop')) {
return [
'allowed' => false,
'is_edit' => false,
'product' => [],
];
}
return AKSRT_Shop::get_instance()->get_product_edit_view($user_id, $product_id);
}
function aksrt_get_cart_page_view(int $user_id): array {
if ($user_id <= 0 || !class_exists('AKSRT_Shop')) {
return [
'payment_methods' => [],
'shipping_methods' => [],
'address_book' => [],
'coupon_enabled' => false,
'currency_symbol' => '¥',
'pending_orders' => [],
];
}
return AKSRT_Shop::get_instance()->get_cart_page_view($user_id);
}
function aksrt_get_orders_page_view(int $user_id, int $paged = 1, int $per_page = 20, string $status = ''): array {
if ($user_id <= 0 || !class_exists('AKSRT_Shop')) {
return [
'items' => [],
'total' => 0,
'total_pages' => 1,
'page' => 1,
];
}
return AKSRT_Shop::get_instance()->get_order_center_view($user_id, [
'page' => $paged,
'per_page' => $per_page,
'status' => $status,
]);
}
function aksrt_get_revenue_page_view(int $user_id): array {
$balance = 0.0;
$total_earned = 0.0;
$total_withdrawn = 0.0;
$records = [];
if (class_exists('AKSRT_Credits')) {
$balance = (float) AKSRT_Credits::balance($user_id);
}
if (class_exists('AKSRT_Revenue')) {
$revenues = AKSRT_Revenue::get_instance()->get_user_revenues($user_id, ['limit' => 200]);
foreach ($revenues as $rev) {
$total_earned += (float) ($rev->amount ?? 0);
$records[] = [
'type' => 'earning',
'title' => (string) ($rev->description ?? __('创作分成', 'aksrt')),
'amount' => (float) ($rev->amount ?? 0),
'created_at' => (string) ($rev->created_at ?? ''),
];
}
}
if (class_exists('AKSRT_Withdrawal')) {
$withdrawals = AKSRT_Withdrawal::get_instance()->get_user_withdrawals($user_id, ['limit' => 200]);
foreach ($withdrawals as $w) {
$total_withdrawn += (float) ($w->amount ?? 0);
$records[] = [
'type' => 'withdrawal',
'title' => __('提现', 'aksrt'),
'amount' => (float) ($w->amount ?? 0),
'created_at' => (string) ($w->created_at ?? ''),
];
}
}
usort($records, static function ($a, $b) {
return strcmp($b['created_at'] ?? '', $a['created_at'] ?? '');
});
return [
'balance' => $balance,
'total_earned' => $total_earned,
'total_withdrawn' => $total_withdrawn,
'records' => $records,
];
}
+99
View File
@@ -0,0 +1,99 @@
<?php
/**
* User Dashboard Data Helper
*
* Aggregates user data for author pages and public user homepages.
* Shared between author.php and page-user-home.php to avoid duplicate queries.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
/**
* Get aggregated dashboard view data for a user.
*
* @param int $user_id Target user ID.
* @param array $args Optional. 'viewer_id' => current viewer ID.
* @return array {
* @type int $post_count Published post count.
* @type int $comment_count Approved comment count.
* @type int $favorite_count Favorited post count.
* @type array $favorite_post_ids Array of favorited post IDs.
* @type array $user_contacts Social/contact links from user meta.
* @type array $profile_signals Identity chips (verified, level, badges).
* @type array $profile_notice Ban/restriction notice, if any.
* @type string $background_url Cover image URL.
* @type float $credit_balance Credit balance (own profile only).
* @type string $currency_name Currency/credits display name.
* @type bool $credits_enabled Whether credits system is active.
* @type bool $is_own_profile Whether viewer is the user themselves.
* }
*/
function aksrt_get_user_dashboard_view(int $user_id, array $args = []): array {
$viewer_id = (int) ($args['viewer_id'] ?? get_current_user_id());
$is_own_profile = is_user_logged_in() && $viewer_id === $user_id;
$post_count = (int) count_user_posts($user_id, 'post', true);
$comment_count = (int) get_comments([
'user_id' => $user_id,
'count' => true,
'status' => 'approve',
]);
$favorite_post_ids = function_exists('aksrt_get_user_interaction_posts')
? aksrt_get_user_interaction_posts('_aksrt_favorite_posts', $user_id)
: [];
$favorite_count = count($favorite_post_ids);
$recent_posts = get_posts([
'author' => $user_id,
'post_type' => 'post',
'post_status' => 'publish',
'posts_per_page' => 5,
'orderby' => 'date',
'order' => 'DESC',
'no_found_rows' => true,
]);
$user_contacts = get_user_meta($user_id, 'aksrt_user_contacts', true) ?: [];
if (!is_array($user_contacts)) {
$user_contacts = [];
}
$user_center_settings = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_user_center_settings()
: ['social_links' => true];
$background_url = get_user_meta($user_id, 'aksrt_background_image', true) ?: '';
$credits_enabled = class_exists('AKSRT_Settings') && AKSRT_Settings::credits_enabled();
$credit_balance = $credits_enabled && $is_own_profile && function_exists('aksrt_user_credits')
? (float) aksrt_user_credits($user_id)
: 0.0;
$currency_name = $credits_enabled && class_exists('AKSRT_Settings')
? AKSRT_Settings::get_currency_name()
: __('积分', 'aksrt');
$user_home_manager = class_exists('AKSRT_User_Home') ? AKSRT_User_Home::get_instance() : null;
$profile_signals = $user_home_manager ? $user_home_manager->get_profile_signals($user_id) : [];
$profile_notice = $user_home_manager ? $user_home_manager->get_profile_notice($user_id) : [];
$show_report = $user_home_manager ? $user_home_manager->show_report_entry() : false;
return [
'post_count' => $post_count,
'comment_count' => $comment_count,
'favorite_count' => $favorite_count,
'favorite_post_ids'=> $favorite_post_ids,
'recent_posts' => $recent_posts,
'user_contacts' => $user_contacts,
'social_links_on' => !empty($user_center_settings['social_links']),
'profile_signals' => $profile_signals,
'profile_notice' => $profile_notice,
'show_report' => $show_report,
'background_url' => $background_url,
'credit_balance' => $credit_balance,
'currency_name' => $currency_name,
'credits_enabled' => $credits_enabled,
'is_own_profile' => $is_own_profile,
];
}
+98
View File
@@ -0,0 +1,98 @@
<?php
/**
* AKSRT VIP Helper — VIP 会员展示辅助函数
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
/**
* 检查用户是否为 VIP
*
* 判定逻辑:
* 1. VIP 功能未启用 → false
* 2. 用户角色在 exempt_roles 白名单中 → true(管理员/编辑等特权角色)
* 3. 用户有有效的 VIP 订单(status=completed 且未过期) → true
*
* @param int $user_id
* @return bool
*/
function aksrt_is_vip(int $user_id = 0): bool {
if ($user_id <= 0) $user_id = get_current_user_id();
if ($user_id <= 0) return false;
if (!class_exists('AKSRT_Settings')) return false;
$vip_settings = AKSRT_Settings::get_payment_vip_settings();
if (empty($vip_settings['enabled'])) return false;
// 1. 角色豁免:VIP exempt_roles 中的角色直接视为 VIP
$paid_settings = AKSRT_Settings::get_paid_content_settings();
$exempt_roles = $paid_settings['vip_exempt_roles'] ?? [];
if (!empty($exempt_roles)) {
$user = get_userdata($user_id);
if ($user && !empty(array_intersect($user->roles, $exempt_roles))) {
return true;
}
}
// 2. 订单判定:检查是否有有效的 VIP 订单
if (class_exists('AKSRT_Order')) {
$order_mgr = AKSRT_Order::get_instance();
$valid_order = method_exists($order_mgr, 'get_valid_vip_order')
? $order_mgr->get_valid_vip_order($user_id)
: null;
if ($valid_order) {
return true;
}
}
return false;
}
/**
* 获取 VIP 徽标文本
*
* @return string
*/
function aksrt_get_vip_badge_text(): string {
if (!class_exists('AKSRT_Settings')) return 'VIP';
$vip_settings = AKSRT_Settings::get_payment_vip_settings();
return sanitize_text_field($vip_settings['badge_text'] ?? 'VIP');
}
/**
* 渲染 VIP 徽标 HTML
*
* @param int $user_id
* @param string $size sm | md | lg
* @return string
*/
function aksrt_render_vip_badge(int $user_id = 0, string $size = 'sm'): string {
if (!aksrt_is_vip($user_id)) return '';
$badge_text = aksrt_get_vip_badge_text();
$size_class = 'aksrt-vip-badge--' . $size;
return '<span class="aksrt-vip-badge aksrt-vip-badge--gold ' . esc_attr($size_class) . '"><i class="fas fa-crown"></i> ' . esc_html($badge_text) . '</span>';
}
/**
* 获取用户 VIP 等级信息(用于展示)
*
* @param int $user_id
* @return array{is_vip: bool, badge_text: string, badge_html: string, name: string}
*/
function aksrt_get_vip_info(int $user_id = 0): array {
$is_vip = aksrt_is_vip($user_id);
$badge_text = aksrt_get_vip_badge_text();
return [
'is_vip' => $is_vip,
'badge_text' => $badge_text,
'badge_html' => $is_vip ? aksrt_render_vip_badge($user_id) : '',
'name' => $is_vip ? ($badge_text . '会员') : '普通用户',
];
}
+145
View File
@@ -0,0 +1,145 @@
<?php
/**
* AKSRT Navigation Walker
*
* Custom walker for WordPress nav menus that adds:
* - FA5 icons from menu item classes
* - Badge labels from title attribute pattern [text:type]
* - Submenu style overrides (dropdown / mega / cards)
* - CSS class augmentation for hover effects
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Nav_Walker extends Walker_Nav_Menu {
/**
* @var array Decoration settings from aksrt_get_menu_decoration_settings().
*/
private $settings;
public function __construct(array $settings = []) {
$this->settings = $settings;
}
/**
* 当前处理的菜单项是否为 Mega Menu 容器
* @var bool
*/
private $is_mega_menu = false;
/**
* Mega Menu 列数
* @var int
*/
private $mega_columns = 3;
/**
* Start level: open submenu with appropriate class based on style.
*/
public function start_lvl(&$output, $depth = 0, $args = null): void {
$indent = str_repeat("\t", $depth);
if ($this->is_mega_menu && $depth === 0) {
// Mega Menu 子菜单容器
$output .= "\n{$indent}<ul class=\"sub-menu mega-menu__panel mega-menu__columns-{$this->mega_columns}\">\n";
} else {
$output .= "\n{$indent}<ul class=\"sub-menu\">\n";
}
}
/**
* Start element: add icon, badge, and classes.
*/
public function start_el(&$output, $item, $depth = 0, $args = null, $id = 0): void {
$indent = ($depth) ? str_repeat("\t", $depth) : '';
$classes = (array) $item->classes;
$decoration = function_exists('aksrt_get_menu_item_decoration')
? aksrt_get_menu_item_decoration($item)
: ['icon' => '', 'badge_text' => '', 'badge_type' => 'new', 'submenu_style' => ''];
// 检测 Mega Menu 标记
$this->is_mega_menu = false;
$this->mega_columns = 3;
if ($depth === 0) {
foreach ($classes as $cls) {
if ($cls === 'mega-menu') {
$this->is_mega_menu = true;
}
if (preg_match('/^mega-menu-columns-(\d)$/', $cls, $m)) {
$this->mega_columns = max(2, min(5, (int) $m[1]));
}
}
}
// Build li classes
$li_classes = ['menu-item', 'menu-item-' . $item->ID];
if (in_array('current-menu-item', $classes, true)) {
$li_classes[] = 'current-menu-item';
}
if (in_array('menu-item-has-children', $classes, true)) {
$li_classes[] = 'menu-item-has-children';
}
if ($this->is_mega_menu) {
$li_classes[] = 'mega-menu-item';
}
// Add decoration classes
$li_classes[] = 'aksrt-nav__item';
if ($decoration['icon'] !== '' && !empty($this->settings['icon_enabled'])) {
$li_classes[] = 'aksrt-nav__item--has-icon';
}
if ($decoration['badge_text'] !== '' && !empty($this->settings['badge_enabled'])) {
$li_classes[] = 'aksrt-nav__item--has-badge';
}
$hover = $this->settings['hover_effect'] ?? 'underline';
if ($hover !== 'none') {
$li_classes[] = 'aksrt-nav__item--hover-' . $hover;
}
$class_str = implode(' ', array_unique($li_classes));
$output .= $indent . '<li class="' . esc_attr($class_str) . '" data-menu-item-id="' . (int) $item->ID . '" data-menu-depth="' . (int) $depth . '"' . (in_array('menu-item-has-children', $classes, true) ? ' data-menu-has-children="1"' : '') . '>';
// Build link content
$icon_html = '';
if ($decoration['icon'] !== '' && !empty($this->settings['icon_enabled'])) {
$icon_html = aksrt_render_menu_icon($decoration['icon'], true);
}
$badge_html = '';
if ($decoration['badge_text'] !== '' && !empty($this->settings['badge_enabled'])) {
$badge_html = aksrt_render_menu_badge($decoration['badge_text'], $decoration['badge_type'], true);
}
// Mega Menu 子项:检查是否有描述文字(用作卡片副标题)
$description_html = '';
if ($this->is_mega_menu && $depth === 1 && !empty($item->description)) {
$description_html = '<span class="mega-menu__item-desc">' . esc_html($item->description) . '</span>';
}
$atts = [
'href' => $item->url,
'title' => $item->attr_title ?: '',
];
$link_inner = $icon_html . '<span>' . esc_html($item->title) . '</span>' . $description_html . $badge_html;
$output .= '<a';
foreach ($atts as $attr => $value) {
if ('' !== $value) {
$output .= ' ' . $attr . '="' . esc_attr($value) . '"';
}
}
$output .= '>' . $link_inner . '</a>';
}
/**
* End element.
*/
public function end_el(&$output, $item, $depth = 0, $args = null): void {
$output .= "</li>\n";
}
}
+161
View File
@@ -0,0 +1,161 @@
<?php
/**
* Menu Decoration Helpers
*
* Helper functions for reading menu item meta (icons, badges, submenu styles)
* and rendering decorated menu items.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
/**
* Get menu decoration settings from AKSRT_Settings.
*
* @return array
*/
function aksrt_get_menu_decoration_settings(): array {
static $cache = null;
if ($cache !== null) {
return $cache;
}
if (class_exists('AKSRT_Settings')) {
$all = AKSRT_Settings::get_all();
} else {
$all = [];
}
$cache = [
'icon_enabled' => !empty($all['menu_icon_enabled']),
'badge_enabled' => !empty($all['menu_badge_enabled']),
'submenu_style' => in_array($all['menu_submenu_style'] ?? '', ['dropdown', 'mega', 'cards'], true)
? $all['menu_submenu_style']
: 'dropdown',
'submenu_columns' => max(1, min(5, (int) ($all['menu_submenu_columns'] ?? 1))),
'submenu_width' => max(160, min(800, (int) ($all['menu_submenu_width'] ?? 220))),
'hover_effect' => in_array($all['menu_hover_effect'] ?? '', ['underline', 'background', 'color', 'none'], true)
? $all['menu_hover_effect']
: 'underline',
'item_padding' => max(4, min(32, (int) ($all['menu_item_padding'] ?? 12))),
'mobile_style' => in_array($all['menu_mobile_style'] ?? '', ['drawer', 'fullscreen', 'dropdown'], true)
? $all['menu_mobile_style']
: 'drawer',
];
return $cache;
}
/**
* Get decoration meta for a specific menu item.
*
* Reads CSS class and title attribute from the menu item to extract:
* - icon: FA5 class (e.g. "fas fa-home")
* - badge_text: badge label (e.g. "新", "热")
* - badge_type: badge color type (e.g. "new", "hot", "info")
* - submenu_style: override for this item's submenu
*
* @param WP_Post $menu_item The menu item post object.
* @return array
*/
function aksrt_get_menu_item_decoration($menu_item): array {
$classes = (array) ($menu_item->classes ?? []);
$title = (string) ($menu_item->title ?? '');
$attr = (string) ($menu_item->attr_title ?? '');
$result = [
'icon' => '',
'badge_text' => '',
'badge_type' => 'new',
'submenu_style' => '',
];
// Extract icon from CSS classes (pattern: icon-* or fa-*)
foreach ($classes as $cls) {
$cls = trim($cls);
if (strpos($cls, 'fa-') === 0 || strpos($cls, 'fas ') === 0 || strpos($cls, 'fab ') === 0 || strpos($cls, 'far ') === 0) {
$result['icon'] = $cls;
break;
}
if (strpos($cls, 'icon-') === 0) {
$result['icon'] = 'fas fa-' . substr($cls, 5);
break;
}
}
// Extract badge from title attribute (pattern: [新] or [热:hot])
if (preg_match('/^\[([^\]:]+)(?::(\w+))?\]/', $title, $m)) {
$result['badge_text'] = $m[1];
$result['badge_type'] = $m[2] ?? 'new';
// Remove badge markup from display title
$menu_item->title = trim(substr($title, strlen($m[0])));
}
// Extract submenu style override from classes
foreach ($classes as $cls) {
if (strpos($cls, 'submenu-') === 0) {
$style = substr($cls, 8);
if (in_array($style, ['dropdown', 'mega', 'cards'], true)) {
$result['submenu_style'] = $style;
}
break;
}
}
return $result;
}
/**
* Render a menu item icon if enabled.
*
* @param string $icon_class FA5 icon class.
* @param bool $enabled Whether icons are enabled globally.
* @return string HTML string.
*/
function aksrt_render_menu_icon(string $icon_class, bool $enabled = true): string {
if (!$enabled || $icon_class === '') {
return '';
}
return '<i class="' . esc_attr($icon_class) . ' aksrt-nav__icon" aria-hidden="true"></i>';
}
/**
* Render a menu item badge if enabled.
*
* @param string $text Badge text.
* @param string $type Badge type (new, hot, info, etc.).
* @param bool $enabled Whether badges are enabled globally.
* @return string HTML string.
*/
function aksrt_render_menu_badge(string $text, string $type = 'new', bool $enabled = true): string {
if (!$enabled || $text === '') {
return '';
}
return '<span class="aksrt-nav__badge aksrt-nav__badge--' . esc_attr($type) . '">' . esc_html($text) . '</span>';
}
/**
* Get CSS classes for a menu item based on decoration settings.
*
* @param array $decoration Decoration meta from aksrt_get_menu_item_decoration().
* @param array $settings Global decoration settings.
* @return string Space-separated CSS classes.
*/
function aksrt_get_menu_item_classes(array $decoration, array $settings): string {
$classes = ['aksrt-nav__item'];
if ($settings['hover_effect'] !== 'none') {
$classes[] = 'aksrt-nav__item--hover-' . $settings['hover_effect'];
}
if ($decoration['icon'] !== '') {
$classes[] = 'aksrt-nav__item--has-icon';
}
if ($decoration['badge_text'] !== '') {
$classes[] = 'aksrt-nav__item--has-badge';
}
return implode(' ', $classes);
}