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 = AKSRT_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' ? AKSRT_Forum::REVIEW_APPROVED : AKSRT_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); $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, [AKSRT_Forum::BBS_TYPE_STANDARD, AKSRT_Forum::BBS_TYPE_QUESTION], true)) { $bbs_type = AKSRT_Forum::BBS_TYPE_STANDARD; } $question_status = $bbs_type === AKSRT_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); return [ 'id' => $topic_id, 'title' => (string) get_the_title($topic_id), 'excerpt' => (string) $excerpt, 'content' => (string) $post->post_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 > AKSRT_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), ]; } /** * 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 !== AKSRT_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; } if ($visibility === AKSRT_Forum::VISIBILITY_PUBLIC) { return true; } if ($visibility === AKSRT_Forum::VISIBILITY_LOGIN) { return $viewer_user_id > 0; } if ($visibility === AKSRT_Forum::VISIBILITY_COMMENT) { if ($viewer_user_id <= 0) { return false; } return $this->has_user_commented_on($viewer_user_id, (int) $post->ID); } if ($visibility === AKSRT_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 === AKSRT_Forum::VISIBILITY_CREDITS) { if ($viewer_user_id <= 0) { return false; } return $this->has_user_unlocked_topic($viewer_user_id, (int) $post->ID); } if ($visibility === AKSRT_Forum::VISIBILITY_PAY) { if ($viewer_user_id <= 0) { return false; } return $this->has_user_paid_topic($viewer_user_id, (int) $post->ID); } if ($visibility === AKSRT_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 !== AKSRT_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) { return $this->forum->error(__('积分价格未设置', 'aksrt')); } if (!class_exists('AKSRT_Credits')) { return $this->forum->error(__('积分系统不可用', 'aksrt')); } $balance = AKSRT_Credits::get_balance($user_id); if ($balance < $cost) { return $this->forum->error(sprintf(__('积分不足,需要 %d 积分', 'aksrt'), $cost)); } $deducted = AKSRT_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::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')); } /** * Cash/balance purchase for VISIBILITY_PAY topics. * * Reuses AKSRT_Order product_type=resource + balance credits conversion * (1 CNY = 100 credits, same as AKSRT_Balance_Gateway). * * @return array{success:bool,message:string} */ public function purchase_topic_with_balance(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')); } if ($user_id <= 0) { return $this->forum->error(__('请先登录', 'aksrt')); } $visibility = (string) get_post_meta($topic_id, '_aksrt_visibility', true); if ($visibility !== AKSRT_Forum::VISIBILITY_PAY) { return $this->forum->error(__('该主题不是付费可见内容', 'aksrt')); } if ($this->has_user_paid_topic($user_id, $topic_id)) { return $this->forum->success(__('已购买', 'aksrt')); } $price = $this->resolve_topic_pay_price($topic_id, $user_id); if (!empty($price['is_free'])) { $this->mark_topic_unlocked($user_id, $topic_id); return $this->forum->success(__('VIP 免费解锁成功', 'aksrt'), [ 'amount' => 0, 'free' => true, ]); } $amount = (float) ($price['amount'] ?? 0); if ($amount <= 0) { return $this->forum->error(__('价格未设置', 'aksrt')); } if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Credits')) { return $this->forum->error(__('支付系统不可用', 'aksrt')); } $product_type = class_exists('AKSRT_Domain_Model') ? AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE : 'resource'; $order_number = AKSRT_Order::get_instance()->create($user_id, $product_type, $topic_id, $amount, 'CNY'); if ($order_number === '') { return $this->forum->error(__('创建订单失败', 'aksrt')); } // Align with AKSRT_Balance_Gateway: 1 CNY = 100 credits. $credits_cost = (int) ceil($amount * 100); $balance = AKSRT_Credits::get_balance($user_id); if ($balance < $credits_cost) { AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_CANCELLED, [ 'note' => __('余额不足,自动取消', 'aksrt'), ]); return $this->forum->error(sprintf( __('余额不足,需要 %d 积分(约 ¥%s),当前余额 %d', 'aksrt'), $credits_cost, number_format($amount, 2, '.', ''), $balance )); } $deducted = AKSRT_Credits::deduct( $user_id, $credits_cost, 'forum_topic_pay', [ 'topic_id' => $topic_id, 'order' => $order_number, ], sprintf(__('购买帖子「%s」', 'aksrt'), get_the_title($topic_id)) ); if (is_wp_error($deducted) || $deducted === false) { AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_CANCELLED, [ 'note' => __('扣款失败,自动取消', 'aksrt'), ]); $msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('扣款失败', 'aksrt'); return $this->forum->error($msg ?: __('扣款失败', 'aksrt')); } $paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [ 'payment_method' => 'balance', 'payment_transaction_id' => 'BAL_' . $order_number, 'note' => sprintf(__('论坛付费主题余额支付 %d 积分', 'aksrt'), $credits_cost), ]); if (!$paid) { // Best-effort refund so user is not charged without access. AKSRT_Credits::add( $user_id, $credits_cost, 'forum_topic_pay_refund', ['topic_id' => $topic_id, 'order' => $order_number], sprintf(__('购买失败退款:%s', 'aksrt'), get_the_title($topic_id)) ); return $this->forum->error(__('订单确认失败,已尝试退回积分', 'aksrt')); } $this->mark_topic_unlocked($user_id, $topic_id); return $this->forum->success(__('购买成功', 'aksrt'), [ 'order_number' => $order_number, 'amount' => $amount, 'credits_cost' => $credits_cost, ]); } /** * Resolve cash price for a pay-visibility topic. * * @return array{amount:float,is_vip_price:bool,is_free:bool,label:string,base:float,vip_set:bool} */ 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_raw = get_post_meta($topic_id, '_aksrt_pay_vip_price', true); $vip_set = ($vip_raw !== '' && $vip_raw !== null && $vip_raw !== false); $vip_price = $vip_set && is_numeric($vip_raw) ? (float) $vip_raw : 0.0; $is_vip = $user_id > 0 && function_exists('aksrt_is_vip') && aksrt_is_vip($user_id); if (!$is_vip && $user_id > 0 && function_exists('aksrt_is_vip_exempt') && aksrt_is_vip_exempt($user_id)) { $is_vip = true; } if ($is_vip && $vip_set) { return [ 'amount' => max(0.0, $vip_price), 'is_vip_price' => true, 'is_free' => $vip_price <= 0, 'label' => $vip_price <= 0 ? __('VIP 免费', 'aksrt') : sprintf(__('VIP ¥%s', 'aksrt'), number_format($vip_price, 2, '.', '')), 'base' => $base, 'vip_set' => true, ]; } 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, ]; } 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 ($user_id <= 0 || $topic_id <= 0) { return false; } if ($this->has_user_unlocked_topic($user_id, $topic_id)) { return true; } if (class_exists('AKSRT_Order')) { $product_type = class_exists('AKSRT_Domain_Model') ? AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE : 'resource'; if (AKSRT_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 [ AKSRT_Forum::VISIBILITY_PUBLIC => __('公开可见', 'aksrt'), AKSRT_Forum::VISIBILITY_LOGIN => __('登录可见', 'aksrt'), AKSRT_Forum::VISIBILITY_COMMENT => __('评论可见', 'aksrt'), AKSRT_Forum::VISIBILITY_ROLE => __('角色可见', 'aksrt'), AKSRT_Forum::VISIBILITY_CREDITS => __('积分查看', 'aksrt'), AKSRT_Forum::VISIBILITY_PAY => __('付费查看', 'aksrt'), AKSRT_Forum::VISIBILITY_MODERATOR => __('版主可见', 'aksrt'), ]; } /** * Review labels. */ public function get_review_map(): array { return [ AKSRT_Forum::REVIEW_APPROVED => __('已通过', 'aksrt'), AKSRT_Forum::REVIEW_PENDING => __('待审核', 'aksrt'), 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[AKSRT_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[AKSRT_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; } }