'GET', 'callback' => [$this, 'list_comments'], 'permission_callback' => [$this, 'check_permission'], 'args' => [ 'page' => ['default' => 1, 'sanitize_callback' => 'absint'], 'per_page' => ['default' => 20, 'sanitize_callback' => 'absint'], 'post_id' => ['default' => 0, 'sanitize_callback' => 'absint'], 'status' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'], 'search' => ['default' => '', 'sanitize_callback' => 'sanitize_text_field'], ], ]); // Create comment (admin or frontend) register_rest_route(self::API_NAMESPACE, '/comments', [ 'methods' => 'POST', 'callback' => [$this, 'create_comment'], 'permission_callback' => [$this, 'check_logged_in_permission'], 'args' => [ 'post_id' => ['required' => true, 'sanitize_callback' => 'absint'], 'content' => ['required' => true, 'sanitize_callback' => 'sanitize_textarea_field'], 'parent' => ['default' => 0, 'sanitize_callback' => 'absint'], ], ]); // Update comment status (admin) register_rest_route(self::API_NAMESPACE, '/comments/(?P\d+)/status', [ 'methods' => 'POST', 'callback' => [$this, 'update_comment_status'], 'permission_callback' => [$this, 'check_permission'], 'args' => [ 'id' => ['required' => true, 'sanitize_callback' => 'absint'], 'status' => ['required' => true, 'sanitize_callback' => 'sanitize_text_field'], ], ]); // Edit comment content (admin) register_rest_route(self::API_NAMESPACE, '/comments/(?P\d+)', [ 'methods' => 'POST', 'callback' => [$this, 'update_comment'], 'permission_callback' => [$this, 'check_permission'], 'args' => [ 'id' => ['required' => true, 'sanitize_callback' => 'absint'], 'content' => ['required' => true, 'sanitize_callback' => 'sanitize_textarea_field'], ], ]); // Delete comment (admin) register_rest_route(self::API_NAMESPACE, '/comments/(?P\d+)', [ 'methods' => 'DELETE', 'callback' => [$this, 'delete_comment'], 'permission_callback' => [$this, 'check_permission'], 'args' => [ 'id' => ['required' => true, 'sanitize_callback' => 'absint'], ], ]); // Bulk action (admin) register_rest_route(self::API_NAMESPACE, '/comments/bulk', [ 'methods' => 'POST', 'callback' => [$this, 'bulk_action'], 'permission_callback' => [$this, 'check_permission'], ]); // Frontend: list comments for a post (public) register_rest_route(self::API_NAMESPACE, '/comments/post/(?P\d+)', [ 'methods' => 'GET', 'callback' => [$this, 'get_post_comments'], 'permission_callback' => '__return_true', 'args' => [ 'post_id' => ['required' => true, 'sanitize_callback' => 'absint'], 'page' => ['default' => 1, 'sanitize_callback' => 'absint'], 'per_page' => ['default' => 10, 'sanitize_callback' => 'absint'], 'order' => ['default' => 'DESC', 'sanitize_callback' => 'sanitize_text_field'], ], ]); } /** * GET /comments — List comments with filtering. */ public function list_comments(\WP_REST_Request $request): \WP_REST_Response { $args = [ 'number' => min(100, (int) $request->get_param('per_page')), 'offset' => ((int) $request->get_param('page') - 1) * (int) $request->get_param('per_page'), 'orderby' => 'comment_date_gmt', 'order' => 'DESC', ]; $post_id = (int) $request->get_param('post_id'); if ($post_id > 0) { $args['post_id'] = $post_id; } $status = $request->get_param('status'); if ($status !== '') { $args['status'] = $status; } $search = $request->get_param('search'); if ($search !== '') { $args['search'] = $search; } $query = new \WP_Comment_Query($args); // Batch prefetch to avoid N+1 queries $user_ids = array_unique(array_filter(array_map(fn($c) => (int) $c->user_id, $query->comments))); $post_ids = array_unique(array_map(fn($c) => (int) $c->comment_post_ID, $query->comments)); $users_map = []; $posts_map = []; if (!empty($user_ids)) { $users = get_users(['include' => $user_ids, 'fields' => ['ID', 'display_name']]); foreach ($users as $u) $users_map[$u->ID] = $u; } if (!empty($post_ids)) { global $wpdb; $ids_csv = implode(',', array_map('intval', $post_ids)); $titles = $wpdb->get_results("SELECT ID, post_title FROM {$wpdb->posts} WHERE ID IN ({$ids_csv})", OBJECT_K); foreach ($titles as $t) $posts_map[$t->ID] = $t->post_title; } $comments = []; foreach ($query->comments as $comment) { $comments[] = $this->format_comment($comment, $users_map, $posts_map); } // Total count for pagination $count_args = $args; unset($count_args['number'], $count_args['offset'], $count_args['orderby'], $count_args['order']); $count_args['count'] = true; $total = (int) (new \WP_Comment_Query($count_args))->get_comments('count'); return $this->success([ 'data' => $comments, 'total' => $total, 'pages' => ceil($total / (int) $request->get_param('per_page')), ]); } /** * POST /comments — Create a comment. */ public function create_comment(\WP_REST_Request $request): \WP_REST_Response { $user_id = get_current_user_id(); $post_id = (int) $request->get_param('post_id'); $content = trim((string) $request->get_param('content')); $parent = (int) $request->get_param('parent'); if (!$content) { return $this->error('content_required', '评论内容不能为空。', 400); } $post = get_post($post_id); if (!$post || $post->post_status !== 'publish') { return $this->error('invalid_post', '文章不存在或未发布。', 400); } $comment_data = [ 'comment_post_ID' => $post_id, 'comment_content' => $content, 'user_id' => $user_id, 'comment_parent' => $parent, 'comment_approved'=> 1, // Admin-created comments are auto-approved ]; $comment_id = wp_insert_comment($comment_data); if (is_wp_error($comment_id)) { return $this->error('create_failed', $comment_id->get_error_message(), 500); } $comment = get_comment($comment_id); return $this->success(['data' => $this->format_comment($comment)]); } /** * POST /comments/{id}/status — Approve/unapprove/spam/trash. */ public function update_comment_status(\WP_REST_Request $request): \WP_REST_Response { $comment_id = (int) $request->get_param('id'); $new_status = $request->get_param('status'); $comment = get_comment($comment_id); if (!$comment) { return $this->error('not_found', '评论不存在。', 404); } $valid_statuses = ['approve', 'hold', 'spam', 'trash']; if (!in_array($new_status, $valid_statuses, true)) { return $this->error('invalid_status', '无效的状态值。', 400); } $result = wp_set_comment_status($comment_id, $new_status); if (is_wp_error($result)) { return $this->error('update_failed', $result->get_error_message(), 500); } // Handle spam/trash if ($new_status === 'spam') { wp_spam_comment($comment_id); } elseif ($new_status === 'trash') { wp_trash_comment($comment_id); } $comment = get_comment($comment_id); return $this->success(['data' => $this->format_comment($comment)]); } /** * POST /comments/{id} — Edit comment content. */ public function update_comment(\WP_REST_Request $request): \WP_REST_Response { $comment_id = (int) $request->get_param('id'); $content = trim((string) $request->get_param('content')); $comment = get_comment($comment_id); if (!$comment) { return $this->error('not_found', '评论不存在。', 404); } if (!$content) { return $this->error('content_required', '评论内容不能为空。', 400); } $result = wp_update_comment(['comment_ID' => $comment_id, 'comment_content' => $content]); if (is_wp_error($result)) { return $this->error('update_failed', $result->get_error_message(), 500); } $comment = get_comment($comment_id); return $this->success(['data' => $this->format_comment($comment)]); } /** * DELETE /comments/{id} — Delete a comment. */ public function delete_comment(\WP_REST_Request $request): \WP_REST_Response { $comment_id = (int) $request->get_param('id'); $comment = get_comment($comment_id); if (!$comment) { return $this->error('not_found', '评论不存在。', 404); } $result = wp_delete_comment($comment_id, true); if (!$result) { return $this->error('delete_failed', '删除评论失败。', 500); } return $this->success(['data' => ['deleted' => true, 'id' => $comment_id]]); } /** * POST /comments/bulk — Bulk action on comments. */ public function bulk_action(\WP_REST_Request $request): \WP_REST_Response { $body = $request->get_json_params(); $ids = array_map('absint', $body['ids'] ?? []); $action = sanitize_text_field($body['action'] ?? ''); if (empty($ids) || !$action) { return $this->error('missing_params', '缺少参数。', 400); } $success = 0; $failed = 0; foreach ($ids as $id) { $comment = get_comment($id); if (!$comment) { $failed++; continue; } switch ($action) { case 'approve': wp_set_comment_status($id, 'approve'); $success++; break; case 'hold': wp_set_comment_status($id, 'hold'); $success++; break; case 'spam': wp_spam_comment($id); $success++; break; case 'trash': wp_trash_comment($id); $success++; break; case 'delete': wp_delete_comment($id, true); $success++; break; default: $failed++; } } return $this->success(['data' => ['success' => $success, 'failed' => $failed]]); } /** * GET /comments/post/{post_id} — Public: list comments for a post with nesting. */ public function get_post_comments(\WP_REST_Request $request): \WP_REST_Response { $post_id = (int) $request->get_param('post_id'); $page = max(1, (int) $request->get_param('page')); $per_page = min(50, max(1, (int) $request->get_param('per_page'))); $order = in_array(strtoupper((string) $request->get_param('order')), ['ASC', 'DESC'], true) ? strtoupper((string) $request->get_param('order')) : 'DESC'; $args = [ 'post_id' => $post_id, 'status' => 'approve', 'number' => $per_page, 'offset' => ($page - 1) * $per_page, 'orderby' => 'comment_date_gmt', 'order' => $order, 'hierarchical' => 'threaded', ]; $query = new \WP_Comment_Query($args); // Batch prefetch users $user_ids = array_unique(array_filter(array_map(fn($c) => (int) $c->user_id, $query->comments))); $users_map = []; if (!empty($user_ids)) { $users = get_users(['include' => $user_ids, 'fields' => ['ID', 'display_name']]); foreach ($users as $u) $users_map[$u->ID] = $u; } $comments = []; foreach ($query->comments as $comment) { $comments[] = $this->format_comment_public($comment, $users_map); } // Total approved comments for this post $total = (int) get_comments_number($post_id); $total_approved = get_comments([ 'post_id' => $post_id, 'status' => 'approve', 'count' => true, ]); return $this->success([ 'data' => $comments, 'total' => (int) $total_approved, 'pages' => ceil((int) $total_approved / $per_page), ]); } /** * Format comment for admin views. */ private function format_comment(\WP_Comment $comment, array $users_map = [], array $posts_map = []): array { $uid = (int) $comment->user_id; $pid = (int) $comment->comment_post_ID; $user = $users_map[$uid] ?? ($uid ? get_userdata($uid) : null); return [ 'id' => (int) $comment->comment_ID, 'post_id' => $pid, 'post_title' => $posts_map[$pid] ?? get_the_title($pid), 'author' => $comment->comment_author, 'author_email' => $comment->comment_author_email, 'user_id' => (int) $comment->user_id, 'user_name' => $user ? $user->display_name : '', 'content' => $comment->comment_content, 'status' => $comment->comment_approved, 'parent' => (int) $comment->comment_parent, 'date' => $comment->comment_date_gmt, 'author_ip' => $comment->comment_author_IP, 'avatar_url' => get_avatar_url($comment->comment_author_email, ['size' => 48]), ]; } /** * Format comment for public views (no email/IP). */ private function format_comment_public(\WP_Comment $comment, array $users_map = []): array { $uid = (int) $comment->user_id; $user = $users_map[$uid] ?? ($uid ? get_userdata($uid) : null); return [ 'id' => (int) $comment->comment_ID, 'author' => $comment->comment_author, 'user_id' => (int) $comment->user_id, 'content' => $comment->comment_content, 'date' => $comment->comment_date_gmt, 'parent' => (int) $comment->comment_parent, 'avatar_url' => get_avatar_url($comment->comment_author_email, ['size' => 48]), 'children' => [], // Will be populated by JS if threaded ]; } /** * Helper: check logged-in permission (any authenticated user). */ public function check_logged_in_permission(): bool { return is_user_logged_in(); } }