Files
wordpress-theme-aksrt/inc/helpers/jio-vip.php
T
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

577 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Jio VIP Helper — VIP 会员等级与展示辅助函数
*
* @package Jio
*/
defined('ABSPATH') || exit;
/**
* Resolve the complete VIP membership state for a user.
*
* Explicit membership meta is authoritative, including an explicit level 0.
* Order fallback is only for users without a canonical membership level.
*
* @return array{level:int,expires:int,state:string,expired_level:int,source:string,has_explicit_state:bool,is_permanent:bool,current_level:int,current_expires:int,current_state:string,fallback_level:int,fallback_expires:int,fallback_state:string,has_fallback:bool,is_layered_upgrade:bool}
*/
function jio_get_user_vip_state(int $user_id = 0): array {
$empty = [
'level' => 0,
'expires' => 0,
'state' => 'unopened',
'expired_level' => 0,
'source' => 'none',
'has_explicit_state' => false,
'is_permanent' => false,
'current_level' => 0,
'current_expires' => 0,
'current_state' => 'unopened',
'fallback_level' => 0,
'fallback_expires' => 0,
'fallback_state' => 'none',
'has_fallback' => false,
'is_layered_upgrade' => false,
];
if ($user_id <= 0) {
$user_id = get_current_user_id();
}
if ($user_id <= 0 || !class_exists('\Jio\Settings\Settings')) {
return $empty;
}
$vip = \Jio\Settings\Settings::get_payment_vip_settings();
if (empty($vip['enabled'])) {
return $empty;
}
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
$vip1_enabled = !empty($levels[1]['enabled']);
$vip2_enabled = !empty($levels[2]['enabled']);
$has_explicit = metadata_exists('user', $user_id, 'jio_vip_level');
$level = min(2, max(0, (int) get_user_meta($user_id, 'jio_vip_level', true)));
$expires = max(0, (int) get_user_meta($user_id, 'jio_vip_expires', true));
$expired_level = min(2, max(0, (int) get_user_meta($user_id, 'jio_vip_level_expired', true)));
if ($has_explicit) {
if ($level > 0 && $expires > 0 && $expires <= time()) {
if (class_exists('\\Jio\\Content\\VipMembershipExpiry')) {
\Jio\Content\VipMembershipExpiry::process_user($user_id, 'resolver');
$level = min(2, max(0, (int) get_user_meta($user_id, 'jio_vip_level', true)));
$expires = max(0, (int) get_user_meta($user_id, 'jio_vip_expires', true));
} else {
$base_level = min(2, max(0, (int) get_user_meta($user_id, 'jio_vip_base_level', true)));
$base_expires = max(0, (int) get_user_meta($user_id, 'jio_vip_base_expires', true));
if ($base_level > 0 && ($base_expires === 0 || $base_expires > time())) {
update_user_meta($user_id, 'jio_vip_level', $base_level);
update_user_meta($user_id, 'jio_vip_expires', $base_expires);
delete_user_meta($user_id, 'jio_vip_base_level');
delete_user_meta($user_id, 'jio_vip_base_expires');
$level = $base_level;
$expires = $base_expires;
$expired_level = 0;
} else {
$expired_level = max($expired_level, $level);
update_user_meta($user_id, 'jio_vip_level_expired', $expired_level);
update_user_meta($user_id, 'jio_vip_level', 0);
update_user_meta($user_id, 'jio_vip_expires', 0);
delete_user_meta($user_id, 'jio_vip_base_level');
delete_user_meta($user_id, 'jio_vip_base_expires');
$level = 0;
$expires = 0;
}
}
}
$fallback_level = min(2, max(0, (int) get_user_meta($user_id, 'jio_vip_base_level', true)));
$fallback_expires = max(0, (int) get_user_meta($user_id, 'jio_vip_base_expires', true));
if ($level > 0) {
$level = $level >= 2 && $vip2_enabled ? 2 : ($vip1_enabled ? 1 : 0);
$current_state = $expires > 0 ? 'active' : 'permanent';
$fallback_state = $fallback_level > 0 ? ($fallback_expires > 0 ? 'active' : 'permanent') : 'none';
return [
'level' => $level,
'expires' => $expires,
'state' => $current_state,
'expired_level' => 0,
'source' => 'meta',
'has_explicit_state' => true,
'is_permanent' => $expires <= 0,
'current_level' => $level,
'current_expires' => $expires,
'current_state' => $current_state,
'fallback_level' => $fallback_level,
'fallback_expires' => $fallback_expires,
'fallback_state' => $fallback_state,
'has_fallback' => $fallback_level > 0,
'is_layered_upgrade' => $fallback_level > 0 && $fallback_level < $level,
];
}
return array_merge($empty, [
'state' => $expired_level > 0 ? 'expired' : 'unopened',
'expired_level' => $expired_level,
'source' => 'meta',
'has_explicit_state' => true,
'current_state' => $expired_level > 0 ? 'expired' : 'unopened',
]);
}
$paid = \Jio\Settings\Settings::get_paid_content_settings();
$user = get_userdata($user_id);
if ($user && array_intersect((array) $user->roles, (array) ($paid['vip_exempt_roles'] ?? []))) {
$level = $vip2_enabled ? 2 : ($vip1_enabled ? 1 : 0);
return array_merge($empty, [
'level' => $level,
'state' => $level > 0 ? 'permanent' : 'unopened',
'source' => 'exempt',
'is_permanent' => $level > 0,
'current_level' => $level,
'current_state' => $level > 0 ? 'permanent' : 'unopened',
]);
}
if (class_exists('\Jio\Payment\Order')) {
$orders = \Jio\Payment\Order::get_instance();
$order = method_exists($orders, 'get_valid_vip_order') ? $orders->get_valid_vip_order($user_id) : null;
if (is_array($order) && $order !== []) {
$order_meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
if ($order_meta === [] && is_string($order['note'] ?? null)) {
$decoded = json_decode($order['note'], true);
$order_meta = is_array($decoded) ? $decoded : [];
}
$level = (int) ($order_meta['level'] ?? 0);
$product_id = (int) ($order['product_id'] ?? 0);
if ($level <= 0 && $product_id > 0) {
$level = $product_id >= 1000 ? (int) floor($product_id / 1000) : 1;
}
$level = min(2, max(1, $level));
$level = $level >= 2 && $vip2_enabled ? 2 : ($vip1_enabled ? 1 : 0);
$order_expires = max(0, (int) ($order_meta['expires'] ?? $order_meta['vip_expires'] ?? 0));
return array_merge($empty, [
'level' => $level,
'expires' => $order_expires,
'state' => $level > 0 ? 'active' : 'unopened',
'source' => 'order',
'current_level' => $level,
'current_expires' => $order_expires,
'current_state' => $level > 0 ? 'active' : 'unopened',
]);
}
}
return $empty;
}
/**
* @param int $user_id User ID, or current user when zero.
* @return int 0|1|2
*/
function jio_get_user_vip_level(int $user_id = 0): int {
return (int) jio_get_user_vip_state($user_id)['level'];
}
/**
* 检查用户是否为 VIP(level >= 1)。
*
* @param int $user_id
* @return bool
*/
function jio_is_vip(int $user_id = 0): bool {
return jio_get_user_vip_level($user_id) >= 1;
}
/**
* 检查用户是否为 VIP2(level >= 2)。
*
* @param int $user_id
* @return bool
*/
function jio_is_vip2(int $user_id = 0): bool {
return jio_get_user_vip_level($user_id) >= 2;
}
/**
* 获取 VIP 徽标文本
*
* @return string
*/
function jio_get_vip_badge_text(int $user_id = 0): string {
if (!class_exists('\Jio\Settings\Settings')) {
return 'VIP';
}
$vip_settings = \Jio\Settings\Settings::get_payment_vip_settings();
$level = $user_id > 0 ? jio_get_user_vip_level($user_id) : 0;
if ($level >= 1 && !empty($vip_settings['levels'][$level]['name'])) {
return sanitize_text_field((string) $vip_settings['levels'][$level]['name']);
}
return sanitize_text_field($vip_settings['badge_text'] ?? 'VIP');
}
/**
* Daily free-download limit for a user (0 = unlimited).
* Aligns with legacy pay_download_limit / pay_download_limit_vip_{n}.
*/
if (!function_exists('jio_get_user_free_download_limit')) {
function jio_get_user_free_download_limit(int $user_id = 0): int {
if ($user_id <= 0) {
$user_id = get_current_user_id();
}
if ($user_id <= 0 || !class_exists('\Jio\Settings\Settings')) {
return 0;
}
$vip = \Jio\Settings\Settings::get_payment_vip_settings();
$level = function_exists('jio_get_user_vip_level') ? jio_get_user_vip_level($user_id) : 0;
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
$benefit = is_array($vip['benefit'] ?? null) ? $vip['benefit'] : [];
if ($level >= 1 && !empty($levels[$level]['enabled'])) {
return max(0, (int) ($levels[$level]['download_limit'] ?? 0));
}
return max(0, (int) ($benefit['download_limit'] ?? 0));
}
}
/**
* Count free downloads used today for a user (legacy pay_down_number).
*/
if (!function_exists('jio_get_user_free_downloaded_number')) {
function jio_get_user_free_downloaded_number(int $user_id = 0): int {
if ($user_id <= 0) {
$user_id = get_current_user_id();
}
if ($user_id <= 0) {
return 0;
}
$day = current_time('Y-m-d');
$mate = get_user_meta($user_id, 'jio_pay_down_number', true);
if (!is_array($mate) || empty($mate[$day]) || !is_array($mate[$day])) {
return 0;
}
return count($mate[$day]);
}
}
/**
* Record a free download for daily quota (legacy pay_down_number).
*/
if (!function_exists('jio_record_user_free_download')) {
function jio_record_user_free_download(int $user_id, int $post_id): void {
if ($user_id <= 0 || $post_id <= 0) {
return;
}
$day = current_time('Y-m-d');
$mate = get_user_meta($user_id, 'jio_pay_down_number', true);
if (!is_array($mate)) {
$mate = [];
}
// Drop old days to keep meta small.
foreach (array_keys($mate) as $k) {
if ($k !== $day) {
unset($mate[$k]);
}
}
if (!isset($mate[$day]) || !is_array($mate[$day])) {
$mate[$day] = [];
}
$mate[$day][(string) $post_id] = 1;
update_user_meta($user_id, 'jio_pay_down_number', $mate);
}
}
/**
* Whether a free download is allowed under daily quota.
* Paid downloads (cost > 0) always pass.
*
* @return true|WP_Error
*/
if (!function_exists('jio_check_free_download_quota')) {
function jio_check_free_download_quota(int $user_id, int $cost = 0) {
if ($cost > 0) {
return true;
}
if ($user_id <= 0) {
return true; // login gate handled elsewhere
}
$limit = jio_get_user_free_download_limit($user_id);
if ($limit <= 0) {
return true; // unlimited
}
$used = jio_get_user_free_downloaded_number($user_id);
if ($used >= $limit) {
$data = [
'status' => 429,
'limit' => $limit,
'used' => $used,
];
// Optional overbuy price for free-quota exhausted (legacy download_limit_over_price).
// Callers may pass post context via filter.
$over = apply_filters('jio_download_limit_over_price', 0, $user_id);
if ($over > 0) {
$data['download_limit_over_price'] = $over;
$data['allow_limit_over'] = true;
}
return new WP_Error(
'jio_download_limit',
__('您今日下载免费资源个数已超限,请明日再下载。', 'jio'),
$data
);
}
return true;
}
}
/**
* Remaining free downloads today (null = unlimited).
*/
if (!function_exists('jio_get_free_download_surplus')) {
function jio_get_free_download_surplus(int $user_id = 0): ?int {
if ($user_id <= 0) {
$user_id = get_current_user_id();
}
$limit = jio_get_user_free_download_limit($user_id);
if ($limit <= 0) {
return null;
}
$used = jio_get_user_free_downloaded_number($user_id);
return max(0, $limit - $used);
}
}
/**
* Render the configured VIP icon with the frontend identity-image contract.
*
* @param int $user_id User ID.
* @param string $class Additional image classes.
* @param bool $tooltip Whether to enable the tooltip trigger.
*/
function jio_get_vip_icon(int $user_id = 0, string $class = 'em12 ml3', bool $tooltip = true): string {
$level = jio_get_user_vip_level($user_id);
if ($level <= 0 || !class_exists('\Jio\Settings\Settings')) {
return '';
}
$vip_settings = \Jio\Settings\Settings::get_payment_vip_settings();
$icon_url = esc_url((string) ($vip_settings['levels'][$level]['img_icon'] ?? ''));
if ($icon_url === '') {
return '';
}
$name = sanitize_text_field((string) ($vip_settings['levels'][$level]['name'] ?? jio_get_vip_badge_text($user_id)));
$tooltip_attr = $tooltip ? ' data-toggle="tooltip"' : '';
return '<img' . jio_get_lazy_attr('lazy_other', $icon_url, trim('img-icon ' . $class), jio_get_lazy_thumb())
. $tooltip_attr . ' title="' . esc_attr($name) . '" alt="' . esc_attr($name) . '">';
}
/**
* 获取用户 VIP 等级信息(用于展示)
*
* @param int $user_id
* @return array{is_vip: bool, level: int, badge_text: string, badge_html: string, name: string, expires: int, expires_label: string}
*/
function jio_get_vip_info(int $user_id = 0): array {
$state = function_exists('jio_get_user_vip_state') ? jio_get_user_vip_state($user_id) : [];
$level = (int) ($state['level'] ?? 0);
$is_vip = $level >= 1;
$badge_text = jio_get_vip_badge_text($user_id);
$expires = max(0, (int) ($state['expires'] ?? 0));
$expires_label = __('未开通', 'jio');
if (($state['state'] ?? '') === 'expired') {
$expires_label = __('已过期', 'jio');
} elseif ($is_vip && $expires <= 0 && ($state['source'] ?? 'none') !== 'order') {
$expires_label = __('永久有效', 'jio');
} elseif ($is_vip) {
$expires_label = sprintf(__('到期时间:%s', 'jio'), wp_date('Y-m-d H:i', $expires));
}
$name = __('普通用户', 'jio');
if ($is_vip && class_exists('\Jio\Settings\Settings')) {
$vip = \Jio\Settings\Settings::get_payment_vip_settings();
$name = (string) ($vip['levels'][$level]['name'] ?? ($badge_text . __('会员', 'jio')));
} elseif ($is_vip) {
$name = $badge_text . __('会员', 'jio');
}
return [
'is_vip' => $is_vip,
'level' => $level,
'badge_text' => $badge_text,
'badge_html' => $is_vip ? jio_get_vip_icon($user_id) : '',
'name' => $name,
'expires' => $expires,
'expires_label' => $expires_label,
'state' => (string) ($state['state'] ?? 'unopened'),
'source' => (string) ($state['source'] ?? 'none'),
];
}
/**
* VIP 开通页入口(用户中心 credits 面板)。
*/
if (!function_exists('jio_get_vip_purchase_url')) {
function jio_get_vip_purchase_url(): string {
if (function_exists('jio_get_user_center_tab_url')) {
return jio_get_user_center_tab_url('vip');
}
if (function_exists('jio_get_user_center_url')) {
return jio_get_user_center_url('vip');
}
return home_url('/');
}
}
/**
* 用户中心 /user/vip/ 页面的积分兑换会员入口(参考主题 zibpay_user_page_tab_content_vip 1:1)。
*
* 参考主题在「我的会员」摘要与两张档位购买卡之后输出全宽入口横条:
* .c-yellow.zib-widget.flex.ac.jsb(zib-widget → 本主题 jio-panel 表面)
* ├─ .flex1.em12:points-color 图标 +「{自定义货币名}兑换会员」
* └─ .badg.radius.c-yellow:「立即兑换」+ fa-chevron-circle-right
* 门控对齐参考主题 zibpay_get_vip_points_exchange_link:已是会员不展示
* ($vip_level ? '' : link),且要求积分兑换会员开启、至少一个会员等级
* 开启并存在可兑换套餐(catalog.points_exchange 已内含 credits 门控)。
*
* 入口文案的货币名取 catalog.currency_name(后台「积分配置 → 自定义货币名称」),
* 与它打开的兑换弹窗标题、会员弹窗内的入口保持同一命名。
*
* 入口点击经 data-jio-vip-points-open 由 pay-vip.js 打开 wp_footer 输出的
* 静态兑换弹窗(jio_render_vip_points_exchange_modal),与会员购买弹窗内
* 的入口共用同一弹窗与开合开关。
*
* @param int $user_id 当前用户 ID(<=0 返回空串)。
* @param int $vip_level 用户当前会员等级(>0 时返回空串;缺省 -1 表示自行解析)。
* @return string 空串表示不满足展示条件。
*/
if (!function_exists('jio_get_vip_points_exchange_entry_html')) :
function jio_get_vip_points_exchange_entry_html(int $user_id, int $vip_level = -1): string {
if ($user_id <= 0 || !class_exists('\Jio\Content\VipService')) {
return '';
}
if ($vip_level < 0) {
$vip_level = function_exists('jio_get_user_vip_level') ? jio_get_user_vip_level($user_id) : 0;
}
// 参考主题契约:已经是会员就不再展示积分兑换入口。
if ($vip_level > 0) {
return '';
}
$catalog = \Jio\Content\VipService::get_instance()->get_catalog($user_id);
$points_exchange = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : [];
$packages = is_array($points_exchange['packages'] ?? null) ? $points_exchange['packages'] : [];
if (empty($points_exchange['enabled']) || $packages === []) {
return '';
}
// 入口文案跟随自定义货币名称(后台「积分配置 → 自定义货币名称」),与它打开的
// 兑换弹窗标题(VipServiceModal)和会员弹窗内的入口(pay-vip.js)保持同一命名。
$currency = (string) ($catalog['currency_name'] ?? '');
if ($currency === '') {
$currency = class_exists('\Jio\Settings\Settings')
? \Jio\Settings\Settings::get_currency_name()
: __('积分', 'jio');
}
return '<a class="uc-vip-points-entry flex ac jsb" href="javascript:;" data-jio-vip-points-open>'
. '<span class="flex1 em12 c-yellow">'
. '<svg class="icon mr6 em12" aria-hidden="true"><use xlink:href="#icon-points-color"></use></svg>'
. esc_html(sprintf(__('%s兑换会员', 'jio'), $currency))
. '</span>'
. '<span class="badg radius c-yellow">' . esc_html__('立即兑换', 'jio')
. '<i class="fa fas fa-chevron-circle-right ml6" aria-hidden="true"></i></span></a>';
}
endif;
/**
* Format VIP equity HTML for front-end display.
*
* Admin field is free-form HTML (typically one or more <li>). Do NOT
* re-parse / re-split the content into synthetic list items by punctuation
* or Chinese keyword heuristics — that mangles intentional HTML.
*
* Rules:
* - Sanitize with WordPress post-safe HTML.
* - If markup already has <li>, wrap the items in <ul class="v-equity …">.
* - Otherwise output the sanitized HTML inside <div class="v-equity …">.
* - Plain multi-line text: each non-empty line becomes one <li> (only \n split).
* - Empty input → empty string.
*
* @param string $raw Equity HTML/plain from settings.
* @param string $class Extra classes on the wrapper (e.g. "mb10 relative").
*/
if (!function_exists('jio_format_vip_equity_html')) {
function jio_format_vip_equity_html(string $raw, string $class = ''): string {
$raw = trim($raw);
if ($raw === '') {
return '';
}
$clean = trim(wp_kses_post($raw));
if ($clean === '') {
return '';
}
$class = trim('v-equity ' . $class);
$class_attr = esc_attr($class);
// Already list items — preserve them, only re-wrap.
if (preg_match('/<li[\s>]/i', $clean)) {
$inner = preg_replace('#</?(ul|ol)[^>]*>#i', '', $clean) ?? $clean;
$inner = trim($inner);
if ($inner === '') {
return '';
}
return '<ul class="' . $class_attr . '">' . $inner . '</ul>';
}
// Other HTML (p/br/a/…) — keep as-is, no list fabrication.
if (preg_match('/<[^>]+>/', $clean)) {
return '<div class="' . $class_attr . '">' . $clean . '</div>';
}
// Plain text only: split by newlines (author-authored line breaks), never
// by commas / Chinese keywords.
$lines = preg_split('/\R+/u', $clean) ?: [];
$lines = array_values(array_filter(array_map('trim', $lines), static fn($s) => $s !== ''));
if ($lines === []) {
return '';
}
$html = '<ul class="' . $class_attr . '">';
foreach ($lines as $line) {
$html .= '<li>' . esc_html($line) . '</li>';
}
$html .= '</ul>';
return $html;
}
}
/**
* 全局积分兑换会员弹窗(参考主题 refresh_modal 同构静态壳)。
*
* 入口有两处:会员购买弹窗(.pay-vip → assets/js/pay-vip.js)内的「积分兑换
* 会员」按钮,以及 /user/vip/ 档位购买卡下方的入口横条
* (jio_get_vip_points_exchange_entry_html);二者均带
* data-jio-vip-points-open,由 pay-vip.js 文档级委托打开本弹窗。.pay-vip
* 触发点全站可见,因此弹窗壳在满足条件时随 wp_footer 全局输出一次;显隐由
* pay-vip.js 自包含开关驱动,静止态由 vip-modal.css 的
* .modal[data-jio-vip-points-modal] 显式隐藏。
*/
if (!function_exists('jio_render_vip_points_exchange_modal')) :
function jio_render_vip_points_exchange_modal(): void {
if (!is_user_logged_in() || !class_exists('\Jio\Content\VipService')) {
return;
}
$html = \Jio\Content\VipService::get_instance()->get_points_exchange_modal_html(get_current_user_id());
if ($html === '') {
return;
}
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- 各字段已在生成时转义。
}
endif;
add_action('wp_footer', 'jio_render_vip_points_exchange_modal');