ID; $author_id = (int) $post->post_author; $board_terms = get_the_terms($topic_id, 'aksrt_forum_board'); $tag_terms = get_the_terms($topic_id, 'aksrt_forum_tag'); $boards = []; if ($board_terms && !is_wp_error($board_terms)) { foreach ($board_terms as $board_term) { $board_link = get_term_link($board_term); $boards[] = [ 'id' => (int) $board_term->term_id, 'name' => (string) $board_term->name, 'url' => !is_wp_error($board_link) ? (string) $board_link : '', ]; } } $tags = []; if ($tag_terms && !is_wp_error($tag_terms)) { foreach ($tag_terms as $tag_term) { $tag_link = get_term_link($tag_term); $tags[] = [ 'id' => (int) $tag_term->term_id, 'name' => (string) $tag_term->name, 'url' => !is_wp_error($tag_link) ? (string) $tag_link : '', ]; } } $topic_cat_terms = get_the_terms($topic_id, 'aksrt_forum_topic_cat'); $topic_cat = null; if ($topic_cat_terms && !is_wp_error($topic_cat_terms)) { $first = $topic_cat_terms[0]; $tc_link = get_term_link($first); $topic_cat = [ 'id' => (int) $first->term_id, 'name' => (string) $first->name, 'url' => !is_wp_error($tc_link) ? (string) $tc_link : '', ]; } $visibility = sanitize_key((string) get_post_meta($topic_id, '_aksrt_visibility', true)); if (!isset($this->get_visibility_map()[$visibility])) { $visibility = Forum::VISIBILITY_PUBLIC; } $review_status = sanitize_key((string) get_post_meta($topic_id, '_aksrt_review_status', true)); if (!isset($this->get_review_map()[$review_status])) { $review_status = $post->post_status === 'publish' ? Forum::REVIEW_APPROVED : Forum::REVIEW_PENDING; } $can_view = $this->can_view_topic($post, $viewer_user_id, $visibility, $review_status); $excerpt = $post->post_excerpt !== '' ? $post->post_excerpt : wp_trim_words(wp_strip_all_tags((string) $post->post_content), 40); $pay_hide_part = (int) get_post_meta($topic_id, '_aksrt_pay_hide_part', true) === 1; $content = (string) $post->post_content; if (!$can_view) { // Partial hide: keep body for shortcode processing; strip locked blocks at render. // Full hide (default): blank content to avoid API/template leaks. if ($pay_hide_part && in_array($visibility, [Forum::VISIBILITY_CREDITS, Forum::VISIBILITY_PAY], true)) { // leave content; payshow/hidecontent shortcodes gate paid parts $excerpt = wp_trim_words(wp_strip_all_tags( preg_replace('/\[(payshow|hidecontent)[^\]]*\].*?\[\/\1\]/is', '', $content) ?? $content ), 40); } else { $content = ''; if (in_array($visibility, [Forum::VISIBILITY_CREDITS, Forum::VISIBILITY_PAY, Forum::VISIBILITY_LOGIN, Forum::VISIBILITY_COMMENT, Forum::VISIBILITY_ROLE, Forum::VISIBILITY_MODERATOR], true)) { $excerpt = ''; } } } $sticky_level = $this->resolve_sticky_level($topic_id); $featured = get_post_meta($topic_id, '_aksrt_featured', true) === '1'; $locked = get_post_meta($topic_id, '_aksrt_locked', true) === '1'; $bbs_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_bbs_type', true)); if (!in_array($bbs_type, [Forum::BBS_TYPE_STANDARD, Forum::BBS_TYPE_QUESTION], true)) { $bbs_type = Forum::BBS_TYPE_STANDARD; } $question_status = $bbs_type === Forum::BBS_TYPE_QUESTION ? (int) get_post_meta($topic_id, '_aksrt_question_status', true) : null; $favorite_count = (int) get_post_meta($topic_id, '_aksrt_favorite_count', true); $is_favorited = $viewer_user_id > 0 && $this->forum->is_favorited($viewer_user_id, $topic_id); $score = (int) get_post_meta($topic_id, '_aksrt_score', true); $cover = $this->build_cover_payload($topic_id); $credits_cost = max(0, (int) get_post_meta($topic_id, '_aksrt_credits_cost', true)); if ($credits_cost <= 0) { $credits_cost = max(0, (int) get_post_meta($topic_id, '_aksrt_pay_credits', true)); } return [ 'id' => $topic_id, 'title' => (string) get_the_title($topic_id), 'excerpt' => (string) $excerpt, 'content' => $content, 'status' => (string) $post->post_status, 'author_id' => $author_id, 'author' => $this->build_user_summary($author_id), 'date' => get_the_date('Y-m-d H:i', $topic_id), 'modified' => get_the_modified_date('Y-m-d H:i', $topic_id), 'comment_count' => (int) get_comments([ 'post_id' => $topic_id, 'count' => true, 'status' => 'approve', ]), 'views' => (int) get_post_meta($topic_id, '_aksrt_views', true), 'permalink' => (string) get_permalink($topic_id), 'sticky' => $sticky_level > Forum::STICKY_NONE, 'sticky_level' => $sticky_level, 'sticky_label' => $this->get_sticky_label($sticky_level), 'featured' => $featured, 'locked' => $locked, 'special_topic' => (string) get_post_meta($topic_id, '_aksrt_special_topic', true), 'bbs_type' => $bbs_type, 'question_status' => $question_status, 'favorite_count' => $favorite_count, 'is_favorited' => $is_favorited, 'score' => $score, 'cover' => $cover, 'visibility' => [ 'key' => $visibility, 'label' => $this->get_visibility_label($visibility), ], 'review_status' => [ 'key' => $review_status, 'label' => $this->get_review_label($review_status), ], 'moderation_note' => (string) get_post_meta($topic_id, '_aksrt_moderation_note', true), 'plate_id' => (int) get_post_meta($topic_id, '_aksrt_plate_id', true), 'boards' => $boards, 'tags' => $tags, 'topic_cat' => $topic_cat, 'vote' => $this->get_vote_payload($topic_id, $viewer_user_id), 'can_view' => $can_view, 'can_edit' => $this->forum->can_edit_topic($viewer_user_id, $topic_id), 'can_moderate' => $this->forum->can_moderate_topic($viewer_user_id, $topic_id), 'pay_hide_part' => $pay_hide_part, 'credits_cost' => $credits_cost, 'pay_price' => (float) get_post_meta($topic_id, '_aksrt_pay_price', true), ]; } /** * Determine whether current viewer can read a topic. */ public function can_view_topic(\WP_Post $post, int $viewer_user_id, string $visibility, string $review_status): bool { if (user_can($viewer_user_id, 'manage_options')) { return true; } if ($post->post_status !== 'publish' || $review_status !== Forum::REVIEW_APPROVED) { return $viewer_user_id > 0 && ((int) $post->post_author === $viewer_user_id || $this->forum->can_moderate_topic($viewer_user_id, (int) $post->ID)); } if ((int) $post->post_author === $viewer_user_id) { return true; } // Plate gate first (zibll plate → post). Moderators of plate already pass can_view_plate. $plate_id = (int) get_post_meta((int) $post->ID, '_aksrt_plate_id', true); if ($plate_id > 0 && method_exists($this->forum, 'can_view_plate')) { if (!$this->forum->can_view_plate($viewer_user_id, $plate_id)) { return false; } } if ($visibility === Forum::VISIBILITY_PUBLIC) { return true; } if ($visibility === Forum::VISIBILITY_LOGIN) { return $viewer_user_id > 0; } if ($visibility === Forum::VISIBILITY_COMMENT) { if ($viewer_user_id <= 0) { return false; } return $this->has_user_commented_on($viewer_user_id, (int) $post->ID); } if ($visibility === Forum::VISIBILITY_ROLE) { if ($viewer_user_id <= 0) { return false; } $allowed_roles = (array) get_post_meta((int) $post->ID, '_aksrt_visibility_roles', true); if (empty($allowed_roles)) { return true; } $user = get_userdata($viewer_user_id); if (!$user) { return false; } return !empty(array_intersect($user->roles, $allowed_roles)); } if ($visibility === Forum::VISIBILITY_CREDITS) { if ($viewer_user_id <= 0) { return false; } return $this->has_user_unlocked_topic($viewer_user_id, (int) $post->ID); } if ($visibility === Forum::VISIBILITY_PAY) { if ($viewer_user_id <= 0) { return function_exists('aksrt_guest_has_paid_access') && aksrt_guest_has_paid_access((int) $post->ID); } return $this->has_user_paid_topic($viewer_user_id, (int) $post->ID); } if ($visibility === Forum::VISIBILITY_MODERATOR) { if ($viewer_user_id <= 0) { return false; } return $this->forum->can_moderate_topic($viewer_user_id, (int) $post->ID); } return true; } /** * Build cover payload for a topic. * * Supports three cover types: image (single), slide (multiple images), video. * Falls back to WP featured image (thumbnail) when no explicit cover_type is set. */ public function build_cover_payload(int $topic_id): ?array { $cover_type = (string) get_post_meta($topic_id, '_aksrt_cover_type', true); if ($cover_type === 'image') { $url = (string) get_post_meta($topic_id, '_aksrt_cover_image', true); if ($url === '') { $url = (string) get_the_post_thumbnail_url($topic_id, 'medium_large'); } if ($url === '') return null; return [ 'type' => 'image', 'url' => $url, 'ratio' => (string) (get_post_meta($topic_id, '_aksrt_cover_ratio', true) ?: '16:9'), ]; } if ($cover_type === 'slide') { $slides = get_post_meta($topic_id, '_aksrt_featured_slide', true); if (!is_array($slides) || empty($slides)) return null; return [ 'type' => 'slide', 'images' => array_map('esc_url', array_slice($slides, 0, 9)), 'ratio' => (string) (get_post_meta($topic_id, '_aksrt_cover_ratio', true) ?: '16:9'), ]; } if ($cover_type === 'video') { $video_url = (string) get_post_meta($topic_id, '_aksrt_featured_video', true); if ($video_url === '') return null; $poster = (string) get_the_post_thumbnail_url($topic_id, 'medium_large'); return [ 'type' => 'video', 'url' => $video_url, 'poster' => $poster, 'ratio' => (string) (get_post_meta($topic_id, '_aksrt_cover_ratio', true) ?: '16:9'), ]; } // Fallback: use WP featured image if set $thumb_url = (string) get_the_post_thumbnail_url($topic_id, 'medium_large'); if ($thumb_url !== '') { return [ 'type' => 'image', 'url' => $thumb_url, 'ratio' => '16:9', ]; } return null; } /** * Build user summary payload. */ public function build_user_summary(int $user_id): array { $user = $user_id > 0 ? get_userdata($user_id) : false; return [ 'id' => $user_id, 'display_name' => $user ? $user->display_name : '', 'user_email' => $user ? $user->user_email : '', 'avatar_url' => $user ? $this->get_user_avatar_url($user_id) : '', ]; } /** * Get avatar url. */ public function get_user_avatar_url(int $user_id): string { if (function_exists('aksrt_get_avatar_url')) { return (string) aksrt_get_avatar_url($user_id); } return (string) get_avatar_url($user_id); } // ================================================================ // Topic unlock // ================================================================ public function unlock_topic_with_credits(int $user_id, int $topic_id): array { $post = get_post($topic_id); if (!$post || $post->post_type !== 'aksrt_forum_topic') { return $this->forum->error(__('主题不存在', 'aksrt')); } $visibility = (string) get_post_meta($topic_id, '_aksrt_visibility', true); if ($visibility !== Forum::VISIBILITY_CREDITS) { return $this->forum->error(__('该主题不需要积分解锁', 'aksrt')); } if ($this->has_user_unlocked_topic($user_id, $topic_id)) { return $this->forum->success(__('已解锁', 'aksrt')); } $cost = max(0, (int) get_post_meta($topic_id, '_aksrt_credits_cost', true)); if ($cost <= 0) { $cost = max(0, (int) get_post_meta($topic_id, '_aksrt_pay_credits', true)); } // VIP tier discount for credits. $vip_level = 0; if (function_exists('aksrt_get_user_vip_level')) { $vip_level = (int) aksrt_get_user_vip_level($user_id); } elseif (function_exists('aksrt_is_vip2') && aksrt_is_vip2($user_id)) { $vip_level = 2; } elseif (function_exists('aksrt_is_vip') && aksrt_is_vip($user_id)) { $vip_level = 1; } if ($vip_level >= 2) { $v2 = get_post_meta($topic_id, '_aksrt_credits_vip2', true); if ($v2 !== '' && $v2 !== null && $v2 !== false) { $cost = max(0, (int) $v2); } } elseif ($vip_level >= 1) { $v1 = get_post_meta($topic_id, '_aksrt_credits_vip1', true); if ($v1 !== '' && $v1 !== null && $v1 !== false) { $cost = max(0, (int) $v1); } } if ($cost <= 0 && $vip_level > 0) { // VIP free credits unlock. $this->mark_topic_unlocked($user_id, $topic_id); return $this->forum->success(__('VIP 免费解锁成功', 'aksrt'), ['free' => true]); } if ($cost <= 0) { return $this->forum->error(__('积分价格未设置', 'aksrt')); } if (!class_exists('\AKSRT\Credits\Credits')) { return $this->forum->error(__('积分系统不可用', 'aksrt')); } $balance = \AKSRT\Credits\Credits::get_balance($user_id); if ($balance < $cost) { return $this->forum->error(sprintf(__('积分不足,需要 %d 积分', 'aksrt'), $cost)); } $deducted = \AKSRT\Credits\Credits::deduct($user_id, $cost, 'forum_topic_unlock', [ 'topic_id' => $topic_id, ], sprintf(__('解锁帖子「%s」', 'aksrt'), get_the_title($topic_id))); if (is_wp_error($deducted) || $deducted === false) { $msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt'); return $this->forum->error($msg ?: __('积分扣除失败', 'aksrt')); } $this->mark_topic_unlocked($user_id, $topic_id); $author_id = (int) $post->post_author; if ($author_id > 0 && $author_id !== $user_id) { $author_share = max(0, (int) ($this->forum->get_forum_config()['credits']['author_share_percent'] ?? 70)); $author_amount = (int) floor($cost * $author_share / 100); if ($author_amount > 0) { \AKSRT\Credits\Credits::add($author_id, $author_amount, 'forum_content_income', [ 'topic_id' => $topic_id, 'buyer_id' => $user_id, ], sprintf(__('帖子「%s」被付费查看', 'aksrt'), get_the_title($topic_id))); } } return $this->forum->success(__('解锁成功', 'aksrt')); } /** * Resolve cash price for a pay-visibility topic. * * VIP resolution: VIP2 price → VIP1/legacy VIP price → base. * * @return array{amount:float,is_vip_price:bool,is_free:bool,label:string,base:float,vip_set:bool,vip_level:int} */ public function resolve_topic_pay_price(int $topic_id, int $user_id = 0): array { $base_raw = get_post_meta($topic_id, '_aksrt_pay_price', true); $base = is_numeric($base_raw) ? (float) $base_raw : 0.0; $vip_level = 0; if ($user_id > 0) { if (function_exists('aksrt_get_user_vip_level')) { $vip_level = (int) aksrt_get_user_vip_level($user_id); } elseif (function_exists('aksrt_is_vip2') && aksrt_is_vip2($user_id)) { $vip_level = 2; } elseif (function_exists('aksrt_is_vip') && aksrt_is_vip($user_id)) { $vip_level = 1; } if ($vip_level <= 0 && function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($user_id)) { $vip_level = 1; } } $pick = static function ($raw) { if ($raw === '' || $raw === null || $raw === false) { return null; } return is_numeric($raw) ? (float) $raw : null; }; $vip2 = $pick(get_post_meta($topic_id, '_aksrt_pay_vip2_price', true)); $vip1 = $pick(get_post_meta($topic_id, '_aksrt_pay_vip1_price', true)); if ($vip1 === null) { // Legacy single VIP key. $vip1 = $pick(get_post_meta($topic_id, '_aksrt_pay_vip_price', true)); } $amount = $base; $is_vip_price = false; $vip_set = ($vip1 !== null || $vip2 !== null); if ($vip_level >= 2 && $vip2 !== null) { $amount = max(0.0, $vip2); $is_vip_price = true; } elseif ($vip_level >= 1 && $vip1 !== null) { $amount = max(0.0, $vip1); $is_vip_price = true; } elseif ($vip_level >= 2 && $vip1 !== null) { // VIP2 user falls back to VIP1 price when VIP2 unset. $amount = max(0.0, $vip1); $is_vip_price = true; } if ($is_vip_price) { return [ 'amount' => $amount, 'is_vip_price' => true, 'is_free' => $amount <= 0, 'label' => $amount <= 0 ? __('VIP 免费', 'aksrt') : sprintf(__('VIP ¥%s', 'aksrt'), number_format($amount, 2, '.', '')), 'base' => $base, 'vip_set' => true, 'vip_level' => $vip_level, ]; } return [ 'amount' => max(0.0, $base), 'is_vip_price' => false, 'is_free' => false, 'label' => $base > 0 ? sprintf(__('¥%s', 'aksrt'), number_format($base, 2, '.', '')) : __('价格未设置', 'aksrt'), 'base' => $base, 'vip_set' => $vip_set, 'vip_level' => $vip_level, ]; } public function mark_topic_unlocked(int $user_id, int $topic_id): void { if ($user_id <= 0 || $topic_id <= 0) { return; } $unlocked = get_user_meta($user_id, 'aksrt_unlocked_topics', true); if (!is_array($unlocked)) { $unlocked = []; } $unlocked[] = $topic_id; update_user_meta($user_id, 'aksrt_unlocked_topics', array_values(array_unique(array_map('intval', $unlocked)))); } public function has_user_unlocked_topic(int $user_id, int $post_id): bool { $unlocked = get_user_meta($user_id, 'aksrt_unlocked_topics', true); if (!is_array($unlocked)) { return false; } return in_array($post_id, array_map('intval', $unlocked), true); } /** * Paid access for VISIBILITY_PAY: unlock list OR paid resource order OR VIP free. */ public function has_user_paid_topic(int $user_id, int $topic_id): bool { if ($topic_id <= 0) { return false; } if ($user_id <= 0) { return function_exists('aksrt_guest_has_paid_access') && aksrt_guest_has_paid_access($topic_id); } if ($this->has_user_unlocked_topic($user_id, $topic_id)) { return true; } if (class_exists('\AKSRT\Payment\Order')) { $product_type = class_exists('\AKSRT\Core\DomainModel') ? \AKSRT\Core\DomainModel::ORDER_PRODUCT_RESOURCE : 'resource'; if (\AKSRT\Payment\Order::get_instance()->has_paid_access($user_id, $product_type, $topic_id)) { return true; } } $price = $this->resolve_topic_pay_price($topic_id, $user_id); if (!empty($price['is_free'])) { return true; } return false; } public function has_user_commented_on(int $user_id, int $post_id): bool { global $wpdb; $count = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->comments} WHERE comment_post_ID = %d AND user_id = %d AND comment_approved = '1' LIMIT 1", $post_id, $user_id )); return $count > 0; } // ================================================================ // Status helpers // ================================================================ /** * Normalize post status filter. * * @return array|string */ public function normalize_post_status_query(string $status) { if ($status === 'all') { return ['publish', 'pending', 'draft']; } return $status !== '' ? $status : 'publish'; } /** * Visibility labels. */ public function get_visibility_map(): array { return [ Forum::VISIBILITY_PUBLIC => __('公开可见', 'aksrt'), Forum::VISIBILITY_LOGIN => __('登录可见', 'aksrt'), Forum::VISIBILITY_COMMENT => __('评论可见', 'aksrt'), Forum::VISIBILITY_ROLE => __('角色可见', 'aksrt'), Forum::VISIBILITY_CREDITS => __('积分查看', 'aksrt'), Forum::VISIBILITY_PAY => __('付费查看', 'aksrt'), Forum::VISIBILITY_MODERATOR => __('版主可见', 'aksrt'), ]; } /** * Review labels. */ public function get_review_map(): array { return [ Forum::REVIEW_APPROVED => __('已通过', 'aksrt'), Forum::REVIEW_PENDING => __('待审核', 'aksrt'), Forum::REVIEW_REJECTED => __('已拒绝', 'aksrt'), ]; } /** * Get visibility label. */ public function get_visibility_label(string $visibility): string { $map = $this->get_visibility_map(); return $map[$visibility] ?? $map[Forum::VISIBILITY_PUBLIC]; } /** * Get review label. */ public function get_review_label(string $review_status): string { $map = $this->get_review_map(); return $map[$review_status] ?? $map[Forum::REVIEW_PENDING]; } /** * Get visibility options. */ public function get_visibility_options(): array { $options = []; foreach ($this->get_visibility_map() as $value => $label) { $options[] = ['label' => $label, 'value' => $value]; } return $options; } }