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';
?>
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;
}
}