forum = $forum; } // ── Hook: forum comment posted ─────────────────────────────────── public function on_forum_comment_posted(int $comment_id, $approved, array $commentdata = []): void { $comment = get_comment($comment_id); if (!$comment) return; $post = get_post((int) $comment->comment_post_ID); if (!$post || $post->post_type !== 'aksrt_forum_topic') return; if ((int) $comment->comment_approved !== 1) return; $commenter_id = (int) $comment->user_id; if ($commenter_id <= 0) return; // Topic-level last_reply for board sort tabs. update_post_meta((int) $post->ID, '_aksrt_last_reply', (string) $comment->comment_date); $this->forum->notify_topic_reply((int) $post->ID, $commenter_id); } // ── AJAX: get forum comments ───────────────────────────────────── /** * AJAX: Get forum comments with sort/filter/pagination. */ public function ajax_forum_comments(): void { check_ajax_referer('aksrt_public_nonce', 'nonce'); $topic_id = (int) ($_POST['topic_id'] ?? 0); $sort = sanitize_key((string) ($_POST['sort'] ?? 'newest')); $sort = in_array($sort, ['newest', 'oldest', 'hottest'], true) ? $sort : 'newest'; $author_only = !empty($_POST['author_only']); $page = max(1, (int) ($_POST['page'] ?? 1)); $per_page = max(1, min(100, (int) ($_POST['per_page'] ?? 30))); if ($topic_id <= 0) { wp_send_json_error(['message' => __('参数无效', 'aksrt')]); } $result = $this->get_forum_comments($topic_id, $sort, $author_only, $page, $per_page); ob_start(); $this->render_forum_comments($result['comments'], $topic_id, $result['adopted_id']); $html = ob_get_clean(); wp_send_json_success([ 'html' => $html, 'total' => $result['total'], 'max_pages' => $result['max_pages'], 'page' => $result['page'], 'has_more' => $result['page'] < $result['max_pages'], ]); } // ── AJAX: comment topping ──────────────────────────────────────── /** * AJAX: Set comment topping level (moderator action). */ public function ajax_comment_topping(): void { check_ajax_referer('aksrt_public_nonce', 'nonce'); $comment_id = (int) ($_POST['comment_id'] ?? 0); $level = max(0, min(3, (int) ($_POST['level'] ?? 0))); $user_id = get_current_user_id(); $comment = get_comment($comment_id); if (!$comment) { wp_send_json_error(['message' => __('评论不存在', 'aksrt')]); } $topic_id = (int) $comment->comment_post_ID; if (!$this->forum->forum_user_can($user_id, 'reply_top', $topic_id)) { wp_send_json_error(['message' => __('权限不足', 'aksrt')]); } if ($this->set_comment_topping($comment_id, $level)) { wp_send_json_success([ 'message' => $level > 0 ? __('已置顶', 'aksrt') : __('已取消置顶', 'aksrt'), 'level' => $level, ]); } wp_send_json_error(['message' => __('操作失败', 'aksrt')]); } // ── Render forum comments ──────────────────────────────────────── /** * Render forum comments HTML for AJAX and initial load. * * @param WP_Comment[] $comments Comments (parents + children mixed). * @param int $topic_id Topic post ID. * @param int $adopted_id Adopted comment ID (0 if none). */ public function render_forum_comments(array $comments, int $topic_id, int $adopted_id = 0): void { $post = get_post($topic_id); $post_author_id = $post ? (int) $post->post_author : 0; $current_user_id = get_current_user_id(); $is_question = 'question' === (string) get_post_meta($topic_id, '_aksrt_bbs_type', true); $can_adopt = $is_question && $current_user_id > 0 && ($current_user_id === $post_author_id || $this->forum->forum_user_can($current_user_id, 'reply_adopt_other', $topic_id)); $can_moderate = $this->forum->forum_user_can($current_user_id, 'topic_moderate', $topic_id); if (empty($comments)) { echo '
' . esc_html__('暂无回复,来抢沙发吧!', 'aksrt') . '
'; return; } foreach ($comments as $comment) { if (!($comment instanceof \WP_Comment)) continue; if ((int) $comment->comment_parent !== 0) continue; $comment_id = (int) $comment->comment_ID; $commenter_uid = (int) $comment->user_id; $is_adopted = $comment_id === $adopted_id; $is_hot = $this->is_hot_comment($comment_id); $is_post_author = $commenter_uid > 0 && $commenter_uid === $post_author_id; $topping = (int) get_comment_meta($comment_id, '_aksrt_comment_topping', true); $badge = $this->get_commenter_badge($commenter_uid, $topic_id); $like_count = function_exists('aksrt_get_comment_likes') ? aksrt_get_comment_likes($comment_id) : 0; $is_liked = function_exists('aksrt_is_comment_liked') ? aksrt_is_comment_liked($comment_id) : false; $classes = ['aksrt-forum-reply']; if ($is_adopted) $classes[] = 'is-adopted'; if ($is_hot) $classes[] = 'is-hot'; if ($topping > 0) $classes[] = 'is-pinned'; ?>
0) : ?>
comment_parent === $comment_id; }); if (!empty($child_comments)) : ?>
comment_ID; $child_uid = (int) $child->user_id; $child_badge = $this->get_commenter_badge($child_uid, $topic_id); $child_is_author = $child_uid > 0 && $child_uid === $post_author_id; $child_like_count = function_exists('aksrt_get_comment_likes') ? aksrt_get_comment_likes($child_id) : 0; $child_is_liked = function_exists('aksrt_is_comment_liked') ? aksrt_is_comment_liked($child_id) : false; $parent_comment = get_comment((int) $child->comment_parent); $parent_name = $parent_comment ? get_comment_author((int) $parent_comment->comment_ID) : ''; ?>
comment_parent !== $comment_id) : ?>
post_type !== 'aksrt_forum_topic') { return new \WP_Error('invalid_topic', __('帖子不存在', 'aksrt')); } $bbs_type = sanitize_key((string) get_post_meta($topic_id, '_aksrt_bbs_type', true)); if ($bbs_type !== 'question') { return new \WP_Error('not_question', __('该帖不是提问类型', 'aksrt')); } $author_id = (int) $post->post_author; if ($operator_id !== $author_id && !$this->forum->can_moderate_topic($operator_id, $topic_id)) { return new \WP_Error('forbidden', __('仅提问者或版主可采纳答案', 'aksrt')); } $comment = get_comment($comment_id); if (!$comment || (int) $comment->comment_post_ID !== $topic_id) { return new \WP_Error('invalid_comment', __('回复不存在', 'aksrt')); } $existing = get_post_meta($topic_id, '_aksrt_adopted_comment', true); if ($existing && (int) $existing === $comment_id) { return new \WP_Error('already_adopted', __('该回复已被采纳', 'aksrt')); } if ($existing) { delete_comment_meta((int) $existing, '_aksrt_adopted'); } update_comment_meta($comment_id, '_aksrt_adopted', '1'); update_post_meta($topic_id, '_aksrt_adopted_comment', $comment_id); update_post_meta($topic_id, '_aksrt_question_status', 1); // QUESTION_SOLVED $adoptee_id = (int) $comment->user_id; if ($adoptee_id > 0 && class_exists('AKSRT_Credits')) { $config = $this->forum->get_config(); $amount = (int) ($config['credits']['adopted'] ?? 20); if ($amount > 0) { $already = get_comment_meta($comment_id, '_aksrt_adopt_credited', true); if (!$already) { \AKSRT_Credits::add($adoptee_id, $amount, 'forum_adopted', [ 'type' => 'comment', 'id' => $comment_id, ]); update_comment_meta($comment_id, '_aksrt_adopt_credited', '1'); } } $this->forum->notify($adoptee_id, 'system', sprintf( __('你的回复被采纳为最佳答案(帖子「%s」)', 'aksrt'), get_the_title($topic_id) ), [ 'title' => __('回复被采纳', 'aksrt'), 'target_url' => get_permalink($topic_id) . '#comment-' . $comment_id, ]); } return [ 'message' => __('已采纳该回复为最佳答案', 'aksrt'), 'comment_id' => $comment_id, 'solved' => true, ]; } public function get_adopted_comment_id(int $topic_id): int { return (int) get_post_meta($topic_id, '_aksrt_adopted_comment', true); } // ── Get forum comments (query) ────────────────────────────────── /** * Get forum comments with sort/filter support. * * @param int $topic_id Topic post ID. * @param string $sort 'newest'|'oldest'|'hottest' * @param bool $author_only Only show post-author replies. * @param int $page Page number. * @param int $per_page Items per page. * @return array{comments:WP_Comment[],total:int,max_pages:int,page:int,adopted_id:int} */ public function get_forum_comments(int $topic_id, string $sort = 'newest', bool $author_only = false, int $page = 1, int $per_page = 30): array { $post = get_post($topic_id); if (!$post || $post->post_type !== 'aksrt_forum_topic') { return ['comments' => [], 'total' => 0, 'max_pages' => 1, 'page' => 1, 'adopted_id' => 0]; } $args = [ 'post_id' => $topic_id, 'status' => 'approve', 'parent' => 0, 'number' => $per_page, 'offset' => ($page - 1) * $per_page, ]; if ($author_only) { $args['user_id'] = (int) $post->post_author; } if ($sort === 'hottest') { $args['meta_key'] = 'aksrt_comment_likes'; $args['orderby'] = 'meta_value_num'; $args['order'] = 'DESC'; } elseif ($sort === 'oldest') { $args['orderby'] = 'comment_date_gmt'; $args['order'] = 'ASC'; } else { $args['orderby'] = 'comment_date_gmt'; $args['order'] = 'DESC'; } // Total count for pagination $count_args = $args; unset($count_args['number'], $count_args['offset']); $count_args['count'] = true; $total = (int) get_comments($count_args); $top_level = get_comments($args); // Fetch child comments for these parents if (!empty($top_level)) { $parent_ids = wp_list_pluck($top_level, 'comment_ID'); $children = get_comments([ 'post_id' => $topic_id, 'status' => 'approve', 'parent__in' => $parent_ids, 'orderby' => 'comment_date_gmt', 'order' => 'ASC', 'number' => 200, ]); } else { $children = []; } // Float pinned top-level comments $pinned = []; $normal = []; foreach ($top_level as $c) { $topping = (int) get_comment_meta((int) $c->comment_ID, '_aksrt_comment_topping', true); if ($topping > 0) { $c->aksrt_topping = $topping; $pinned[] = $c; } else { $normal[] = $c; } } usort($pinned, function ($a, $b) { return ($b->aksrt_topping ?? 0) <=> ($a->aksrt_topping ?? 0); }); $comments = array_merge($pinned, $normal, $children); return [ 'comments' => $comments, 'total' => $total, 'max_pages' => max(1, (int) ceil($total / $per_page)), 'page' => $page, 'adopted_id' => $this->get_adopted_comment_id($topic_id), ]; } // ── Set comment topping ───────────────────────────────────────── /** * Set comment topping level. * * @param int $comment_id Comment ID. * @param int $level 0=unpin, 1=normal pin, 2=hot pin, 3=best answer pin. */ public function set_comment_topping(int $comment_id, int $level): bool { $comment = get_comment($comment_id); if (!$comment) { return false; } if ($level <= 0) { delete_comment_meta($comment_id, '_aksrt_comment_topping'); delete_comment_meta($comment_id, '_aksrt_comment_pinned'); } else { update_comment_meta($comment_id, '_aksrt_comment_topping', $level); update_comment_meta($comment_id, '_aksrt_comment_pinned', '1'); } return true; } // ── Maybe mark hot comment ────────────────────────────────────── /** * Check and mark a comment as hot ("神评") if it crosses the like threshold. * * @return bool True if newly marked as hot. */ public function maybe_mark_hot_comment(int $comment_id): bool { $comment = get_comment($comment_id); if (!$comment || '1' !== (string) $comment->comment_approved) { return false; } $post = get_post((int) $comment->comment_post_ID); if (!$post || $post->post_type !== 'aksrt_forum_topic') { return false; } $already_hot = '1' === (string) get_comment_meta($comment_id, '_aksrt_is_hot', true); if ($already_hot) { return false; } $config = $this->forum->get_config(); $threshold = max(1, (int) ($config['reply']['hot_threshold'] ?? 10)); $likes = max(0, (int) get_comment_meta($comment_id, 'aksrt_comment_likes', true)); if ($likes < $threshold) { return false; } update_comment_meta($comment_id, '_aksrt_is_hot', '1'); // Credit the comment author $author_id = (int) $comment->user_id; if ($author_id > 0 && class_exists('AKSRT_Credits')) { $amount = (int) ($config['credits']['comment_like'] ?? 1); if ($amount > 0) { \AKSRT_Credits::add($author_id, $amount, 'forum_hot_comment', [ 'type' => 'comment', 'id' => $comment_id, ]); } } // Notify the author if ($author_id > 0) { $this->forum->notify($author_id, 'system', sprintf( __('你的回复成为了神评(帖子「%s」)', 'aksrt'), get_the_title((int) $comment->comment_post_ID) ), [ 'title' => __('恭喜获得神评', 'aksrt'), 'target_url' => get_permalink((int) $comment->comment_post_ID) . '#comment-' . $comment_id, ]); } return true; } // ── Is hot comment ────────────────────────────────────────────── /** * Check if a comment is a hot comment ("神评"). */ public function is_hot_comment(int $comment_id): bool { return '1' === (string) get_comment_meta($comment_id, '_aksrt_is_hot', true); } // ── Get commenter badge ───────────────────────────────────────── /** * Get the user's moderator tier label for badge rendering in comments. * * @return array{tier:int,label:string,class:string}|null */ public function get_commenter_badge(int $user_id, int $topic_id): ?array { if ($user_id <= 0) { return null; } if ($this->forum->is_forum_admin($user_id)) { return [ 'tier' => AKSRT_Forum::TIER_FORUM_ADMIN, 'label' => __('管理员', 'aksrt'), 'class' => 'aksrt-forum-reply__role-badge--admin', ]; } $plate_id = $this->forum->get_topic_plate_id($topic_id); if ($plate_id > 0) { $tier = $this->forum->get_user_plate_tier($user_id, $plate_id); if ($tier >= AKSRT_Forum::TIER_CAT_MOD) { $names = $this->forum->get_config()['names'] ?? []; return [ 'tier' => $tier, 'label' => $names['cat_moderator'] ?? __('分区版主', 'aksrt'), 'class' => 'aksrt-forum-reply__role-badge--catmod', ]; } if ($tier >= AKSRT_Forum::TIER_PLATE_AUTHOR) { $names = $this->forum->get_config()['names'] ?? []; return [ 'tier' => $tier, 'label' => $names['plate_author'] ?? __('超级版主', 'aksrt'), 'class' => 'aksrt-forum-reply__role-badge--super', ]; } if ($tier >= AKSRT_Forum::TIER_MODERATOR) { $names = $this->forum->get_config()['names'] ?? []; return [ 'tier' => $tier, 'label' => $names['plate_moderator'] ?? __('版主', 'aksrt'), 'class' => 'aksrt-forum-reply__role-badge--mod', ]; } } return null; } }