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

414 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* AKSRT Content Visibility
*
* Central access policy for shortcode-gated content and paid-content runtime.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
class AKSRT_Content_Visibility {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
// 短代码注册统一在 AKSRT_Shortcodes(registry);本类只提供渲染实现。
add_action('wp_ajax_aksrt_check_visibility', [$this, 'ajax_check_visibility']);
add_action('wp_ajax_nopriv_aksrt_check_visibility', [$this, 'ajax_check_visibility']);
}
/**
* @param int|WP_Post|null $post
* @return array<string, mixed>
*/
public static function get_post_access_state($post = null, int $user_id = 0): array {
$resource_post = get_post($post);
$uid = $user_id > 0 ? $user_id : get_current_user_id();
$pay_meta = function_exists('aksrt_get_pay_meta')
? aksrt_get_pay_meta($resource_post)
: AKSRT_Domain_Model::default_pay_meta();
$pay_type = AKSRT_Domain_Model::normalize_pay_type((string) ($pay_meta['pay_type'] ?? AKSRT_Domain_Model::PAY_TYPE_NO));
$priced = function_exists('aksrt_get_user_pay_price')
? aksrt_get_user_pay_price($resource_post, $uid)
: null;
$pay_modo = is_array($priced)
? ((($priced['modo'] ?? '0') === 'points') ? 'points' : '0')
: ((($pay_meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0');
$price = is_array($priced)
? (float) ($priced['amount'] ?? 0)
: (($pay_modo === 'points')
? (float) ($pay_meta['points_price'] ?? 0)
: (float) ($pay_meta['price'] ?? 0));
$paid_settings = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_paid_content_settings()
: [
'download_login' => true,
'download_credit' => true,
'download_captcha' => false,
];
$state = [
'post_id' => $resource_post instanceof WP_Post ? (int) $resource_post->ID : 0,
'pay_type' => $pay_type,
'pay_modo' => $pay_modo,
'price' => $price,
'need_login' => false,
'need_purchase' => false,
'need_vip' => false,
'require_captcha' => false,
'has_paid_access' => false,
'can_view' => true,
'can_download' => true,
'download_cost' => 0,
'pay_limit' => 0,
'user_vip_level' => 0,
];
if (!($resource_post instanceof WP_Post)) {
$state['can_view'] = false;
$state['can_download'] = false;
return $state;
}
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_NO) {
$state['has_paid_access'] = true;
return $state;
}
$has_free_access = $price <= 0 && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_VIP;
$pay_limit = (int) ($pay_meta['pay_limit'] ?? 0);
if (!in_array($pay_limit, [0, 1, 2], true)) {
$pay_limit = 0;
}
$state['pay_limit'] = $pay_limit;
$has_paid_order = false;
if ($uid > 0 && class_exists('AKSRT_Order')) {
$has_paid_order = AKSRT_Order::get_instance()->has_paid_access(
$uid,
AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE,
(int) $resource_post->ID
);
}
$has_vip_access = false;
$user_vip_level = is_array($priced) ? (int) ($priced['vip_level'] ?? 0) : 0;
if ($uid > 0) {
if ($user_vip_level <= 0 && function_exists('aksrt_get_user_vip_level')) {
$user_vip_level = aksrt_get_user_vip_level($uid);
}
if (function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($uid)) {
$has_vip_access = true;
$user_vip_level = max($user_vip_level, 2);
} elseif ($pay_type === AKSRT_Domain_Model::PAY_TYPE_VIP || $pay_limit > 0) {
$has_vip_access = $user_vip_level >= max(1, $pay_limit);
} elseif ($user_vip_level > 0) {
// VIP tier pricing may zero cost; still track membership.
$has_vip_access = $pay_limit <= 0 || $user_vip_level >= $pay_limit;
}
}
$state['user_vip_level'] = $user_vip_level;
// Member-only purchase restriction (Zibll pay_limit).
if ($pay_limit > 0 && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_NO) {
$state['need_vip'] = true;
$allowed_member = $uid > 0 && $user_vip_level >= $pay_limit;
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) {
$download_cost = function_exists('aksrt_resource_download_cost')
? (int) aksrt_resource_download_cost($resource_post)
: (int) ceil($price);
// If member is free via VIP price, cost is 0.
if ($allowed_member && $price <= 0) {
$download_cost = 0;
}
$state['download_cost'] = max(0, $download_cost);
$state['need_login'] = $uid <= 0;
$state['need_purchase'] = $allowed_member && $state['download_cost'] > 0 && !$has_paid_order;
$state['has_paid_access'] = $allowed_member && ($has_paid_order || $state['download_cost'] <= 0);
$state['can_view'] = true;
$state['can_download'] = $allowed_member && ($uid > 0) && ($has_paid_order || $state['download_cost'] <= 0);
if (!$allowed_member) {
$state['can_download'] = false;
$state['has_paid_access'] = false;
$state['need_purchase'] = false;
}
return $state;
}
$state['has_paid_access'] = ($allowed_member && $price <= 0) || $has_paid_order || ($allowed_member && $has_vip_access && $pay_type === AKSRT_Domain_Model::PAY_TYPE_VIP);
// For reading/gallery/video with pay_limit: members still need to purchase unless free.
if ($allowed_member) {
$state['has_paid_access'] = $has_paid_order || $price <= 0;
$state['need_purchase'] = !$state['has_paid_access'];
} else {
$state['has_paid_access'] = false;
$state['need_purchase'] = false;
}
$state['can_view'] = $state['has_paid_access'];
$state['can_download'] = false;
return $state;
}
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) {
$download_cost = function_exists('aksrt_resource_download_cost')
? (int) aksrt_resource_download_cost($resource_post)
: (int) ceil($price);
$state['download_cost'] = max(0, $download_cost);
// Zibll pay_free_logged_show: free download still requires login.
$free_logged_show = !empty($paid_settings['free_logged_show']);
$state['need_login'] = !empty($paid_settings['download_login'])
|| !empty($pay_meta['require_login'])
|| ($free_logged_show && $state['download_cost'] <= 0);
$state['require_captcha'] = !empty($paid_settings['download_captcha']) || !empty($pay_meta['require_captcha']);
// Cash mode with cost handled by orders: need purchase when no order and base price > 0.
$needs_pay = $state['download_cost'] > 0 || ($pay_modo === '0' && $price > 0 && !$has_paid_order && empty($paid_settings['download_credit']));
if ($pay_modo === '0' && $price > 0 && empty($paid_settings['download_credit'])) {
$state['need_purchase'] = !$has_paid_order && !($has_vip_access && $price <= 0);
$state['has_paid_access'] = $has_paid_order || $price <= 0;
// Download still gated by login/credit path for token; cash unlock is order-based.
$state['can_view'] = true;
$state['can_download'] = ($has_paid_order || $price <= 0) && (!$state['need_login'] || $uid > 0);
return $state;
}
$state['need_purchase'] = !$has_paid_order && $state['download_cost'] > 0 && !$has_free_access;
// VIP with free tier amount already reflected in download_cost.
$state['has_paid_access'] = $has_paid_order || $state['download_cost'] <= 0 || $has_free_access;
$state['can_view'] = true;
$state['can_download'] = (!$state['need_login'] || $uid > 0) && ($has_paid_order || $state['download_cost'] <= 0 || true);
// can_download true only means "may request token"; rest_request still deducts credits.
return $state;
}
if ($pay_type === AKSRT_Domain_Model::PAY_TYPE_VIP) {
$state['need_vip'] = true;
$state['has_paid_access'] = $has_vip_access;
$state['can_view'] = $has_vip_access;
$state['can_download'] = false;
return $state;
}
$state['need_login'] = !empty($pay_meta['require_login']);
$state['need_purchase'] = !$has_free_access && !$has_paid_order;
$state['has_paid_access'] = $has_free_access || $has_paid_order;
// VIP free tier for reading etc. via amount.
if (!$state['has_paid_access'] && $price <= 0 && $pay_type !== AKSRT_Domain_Model::PAY_TYPE_VIP) {
$state['has_paid_access'] = true;
$state['need_purchase'] = false;
}
$state['can_view'] = $state['has_paid_access'] && (!$state['need_login'] || $uid > 0);
$state['can_download'] = false;
return $state;
}
public static function can_view_paid_content($post = null, int $user_id = 0): bool {
$state = self::get_post_access_state($post, $user_id);
return !empty($state['can_view']);
}
/**
* @param int|WP_Post|null $post
* @return true|WP_Error
*/
public static function can_download_resource($post = null, int $user_id = 0) {
$resource_post = get_post($post);
if (!($resource_post instanceof WP_Post) || !function_exists('aksrt_is_downloadable_content') || !aksrt_is_downloadable_content($resource_post)) {
return new WP_Error('aksrt_not_downloadable', __('该内容未启用下载能力。', 'aksrt'));
}
$state = self::get_post_access_state($resource_post, $user_id);
$uid = $user_id > 0 ? $user_id : get_current_user_id();
if (($state['pay_type'] ?? '') !== AKSRT_Domain_Model::PAY_TYPE_DOWNLOAD) {
return new WP_Error('aksrt_not_downloadable', __('该内容未启用下载能力。', 'aksrt'));
}
if (!empty($state['need_login']) && $uid <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录后再下载。', 'aksrt'), [
'status' => 401,
'login_url' => wp_login_url(get_permalink($resource_post)),
]);
}
// Free download + pay_free_logged_show (belt-and-suspenders with state.need_login).
$cost = (int) ($state['download_cost'] ?? 0);
$paid_settings = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_paid_content_settings()
: ['download_credit' => true, 'free_logged_show' => false];
if ($cost <= 0 && !empty($paid_settings['free_logged_show']) && $uid <= 0) {
return new WP_Error('aksrt_need_login', __('免费资源,请登录后下载。', 'aksrt'), [
'status' => 401,
'login_url' => wp_login_url(get_permalink($resource_post)),
]);
}
// Member-only (pay_limit).
if (!empty($state['need_vip']) && empty($state['has_paid_access']) && empty($state['can_download'])) {
$need_level = (int) ($state['pay_limit'] ?? 1);
return new WP_Error('aksrt_need_vip', sprintf(
/* translators: %d: required VIP level */
__('该资源仅限 VIP%s 及以上会员购买/下载。', 'aksrt'),
max(1, $need_level)
), [
'status' => 403,
'pay_limit' => $need_level,
]);
}
// Cash purchase required (order path).
if (!empty($state['need_purchase']) && empty($state['has_paid_access']) && $cost <= 0 && (($state['pay_modo'] ?? '0') === '0')) {
return new WP_Error('aksrt_need_purchase', __('请先购买后再下载。', 'aksrt'), [
'status' => 402,
]);
}
// Zibll daily free-download quota.
if ($cost <= 0 && function_exists('aksrt_check_free_download_quota')) {
$quota = aksrt_check_free_download_quota($uid, $cost);
if (is_wp_error($quota)) {
return $quota;
}
}
$pay_modo = (string) ($state['pay_modo'] ?? '0');
$need_credit_check = $cost > 0 && (
$pay_modo === 'points'
|| !empty($paid_settings['download_credit'])
);
if ($need_credit_check) {
$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,
]);
}
}
return true;
}
public function render_hide_shortcode(array $atts, string $content = ''): string {
$atts = shortcode_atts([
'mode' => 'comment',
'message' => '',
'pid' => get_the_ID(),
], $atts, 'aksrt_hide');
$mode = sanitize_key((string) $atts['mode']);
$pid = (int) $atts['pid'];
$message = (string) $atts['message'];
if ($pid <= 0) {
return '';
}
if ($this->is_unlocked($mode, $pid)) {
return '<div class="aksrt-hidden-content aksrt-hidden-content--unlocked">' . do_shortcode($content) . '</div>';
}
if ($message === '') {
switch ($mode) {
case 'comment':
$message = __('评论后查看隐藏内容', 'aksrt');
break;
case 'login':
$message = __('登录后查看隐藏内容', 'aksrt');
break;
case 'password':
$message = __('输入密码后查看隐藏内容', 'aksrt');
break;
default:
$message = __('此内容已隐藏', 'aksrt');
}
}
$wrapper_class = 'aksrt-hidden-content aksrt-hidden-content--locked aksrt-hidden-content--' . esc_attr($mode);
$html = '<div class="' . $wrapper_class . '" data-aksrt-visibility-mode="' . esc_attr($mode) . '" data-aksrt-visibility-pid="' . esc_attr((string) $pid) . '">';
$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 ($mode === 'password') {
$html .= '<div class="aksrt-hidden-content__form">';
$html .= '<input type="password" class="aksrt-hidden-content__password-input" placeholder="' . esc_attr__('请输入密码', 'aksrt') . '">';
$html .= '<button type="button" class="aksrt-hidden-content__submit-btn btn-primary btn-sm">' . esc_html__('验证', 'aksrt') . '</button>';
$html .= '</div>';
}
$html .= '</div>';
return $html;
}
private function is_unlocked(string $mode, int $pid): bool {
switch ($mode) {
case 'comment':
if (!is_user_logged_in()) {
return false;
}
return get_comments([
'post_id' => $pid,
'user_id' => get_current_user_id(),
'count' => true,
'status' => 'approve',
]) > 0;
case 'login':
return is_user_logged_in();
case 'password':
return !empty($_COOKIE['aksrt_hide_pwd_' . $pid]);
default:
return false;
}
}
public function ajax_check_visibility(): void {
check_ajax_referer('aksrt_visibility', '_nonce');
$mode = sanitize_key((string) ($_POST['mode'] ?? ''));
$pid = (int) ($_POST['pid'] ?? 0);
if ($pid <= 0 || !in_array($mode, ['comment', 'login', 'password'], true)) {
wp_send_json_error(['message' => __('参数无效', 'aksrt')]);
}
if ($mode === 'password') {
$password = sanitize_text_field((string) ($_POST['password'] ?? ''));
$post_password = get_post_password($pid);
if ($post_password === '' || $password !== $post_password) {
wp_send_json_error(['message' => __('密码错误', 'aksrt')]);
}
setcookie('aksrt_hide_pwd_' . $pid, '1', time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true);
wp_send_json_success(['message' => __('验证成功', 'aksrt')]);
}
if ($this->is_unlocked($mode, $pid)) {
wp_send_json_success(['message' => __('已解锁', 'aksrt')]);
}
wp_send_json_error(['message' => __('未满足条件', 'aksrt')]);
}
}