success([ 'data' => [ 'items' => [], 'reports' => [], 'counts' => ['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0, 'total' => 0], 'total' => 0, 'page' => 1, 'options' => [ 'statuses' => $this->get_report_status_options(), ], ], ]); } global $wpdb; $table = \AKSRT\Core\DBMigrator::table('report'); if (!$table) { return $this->success([ 'data' => [ 'items' => [], 'reports' => [], 'counts' => ['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0, 'total' => 0], 'total' => 0, 'page' => 1, 'options' => [ 'statuses' => $this->get_report_status_options(), ], ], ]); } $page = max(1, (int) ($request->get_param('page') ?: 1)); $per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: $request->get_param('limit') ?: 20))); $status = sanitize_key((string) $request->get_param('status')); $category = sanitize_key((string) $request->get_param('category')); $target_type = sanitize_key((string) $request->get_param('target_type')); $keyword = trim(sanitize_text_field((string) $request->get_param('keyword'))); $date_from = sanitize_text_field((string) $request->get_param('date_from')); $date_to = sanitize_text_field((string) $request->get_param('date_to')); $where = []; $params = []; if ($status !== '') { $where[] = 'status = %s'; $params[] = $status; } if ($category !== '') { $where[] = 'category = %s'; $params[] = $category; } if ($target_type !== '') { $where[] = 'target_type = %s'; $params[] = $target_type; } if ($date_from !== '') { $where[] = 'created_at >= %s'; $params[] = $date_from . ' 00:00:00'; } if ($date_to !== '') { $where[] = 'created_at <= %s'; $params[] = $date_to . ' 23:59:59'; } $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT 500"; $rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A); $items = []; foreach ((array) $rows as $row) { $item = $this->build_report_admin_item($row); if ($keyword !== '' && !$this->report_matches_keyword($item, $keyword)) { continue; } $items[] = $item; } $total = count($items); $offset = ($page - 1) * $per_page; $paged = array_slice($items, $offset, $per_page); $counts = \AKSRT\Messages\ReportCenter::get_instance()->get_status_counts(); $categories = []; foreach ((array) $rows as $row) { $cat = sanitize_key((string) ($row['category'] ?? '')); if ($cat !== '') { $categories[$cat] = $cat; } } return $this->success([ 'data' => [ 'items' => array_values($paged), 'reports' => array_values($paged), 'total' => $total, 'page' => $page, 'per_page' => $per_page, 'counts' => $counts, 'options' => [ 'statuses' => $this->get_report_status_options(), 'actions' => $this->get_report_action_options(), 'targets' => $this->get_report_target_type_options(), 'categories' => array_map(static function ($item) { return ['label' => $item, 'value' => $item]; }, array_values($categories)), ], ], ]); } public function get_report_detail(\WP_REST_Request $request): \WP_REST_Response { global $wpdb; $report_id = (int) $request->get_param('id'); $table = \AKSRT\Core\DBMigrator::table('report'); if ($report_id <= 0 || !$table) { return $this->error(__('举报工单不存在', 'aksrt'), 404); } $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $report_id), ARRAY_A); if (!$row) { return $this->error(__('举报工单不存在', 'aksrt'), 404); } $related = (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE target_type = %s AND target_id = %d", sanitize_key((string) $row['target_type']), (int) $row['target_id'] )); return $this->success([ 'data' => [ 'report' => $this->build_report_admin_item($row), 'related' => ['same_target_count' => $related], 'options' => [ 'statuses' => $this->get_report_status_options(), 'actions' => $this->get_report_action_options(sanitize_key((string) $row['target_type'])), ], ], ]); } public function process_report(\WP_REST_Request $request): \WP_REST_Response { $report_id = (int) $request->get_param('id'); if ($report_id <= 0) { return $this->error(__('参数无效', 'aksrt')); } return $this->handle_report_process($report_id, $this->get_json_params($request)); } private function handle_report_process(int $report_id, array $payload): \WP_REST_Response { global $wpdb; $table = \AKSRT\Core\DBMigrator::table('report'); if (!$table) { return $this->error(__('举报系统未启用', 'aksrt')); } $status = sanitize_key((string) ($payload['status'] ?? '')); $action_taken = sanitize_key((string) ($payload['action_taken'] ?? 'none')); $note = sanitize_textarea_field((string) ($payload['note'] ?? '')); $valid_statuses = array_column($this->get_report_status_options(), 'value'); $valid_actions = array_column($this->get_report_action_options(), 'value'); if ($status === '' || !in_array($status, $valid_statuses, true)) { return $this->error(__('处理状态无效', 'aksrt')); } if (!in_array($action_taken, $valid_actions, true)) { $action_taken = 'none'; } $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $report_id), ARRAY_A); if (!$row) { return $this->error(__('举报工单不存在', 'aksrt'), 404); } $effect = $this->apply_report_action( sanitize_key((string) $row['target_type']), (int) $row['target_id'], $action_taken, (string) ($row['reason'] ?? '') ); $notes = json_decode((string) ($row['notes_json'] ?? '[]'), true); if (!is_array($notes)) { $notes = []; } $notes[] = [ 'note' => $note, 'status' => $status, 'action_taken' => $action_taken, 'operator' => get_current_user_id(), 'timestamp' => current_time('mysql'), 'effect' => $effect, ]; $updated = $wpdb->update( $table, [ 'status' => $status, 'updated_at' => current_time('mysql'), 'notes_json' => wp_json_encode($notes), ], ['id' => $report_id], ['%s', '%s', '%s'], ['%d'] ); if ($updated === false) { return $this->error(__('处理失败', 'aksrt')); } return $this->success([ 'message' => __('举报工单已处理', 'aksrt'), ]); } private function build_report_admin_item(array $row): array { $reporter = get_userdata((int) ($row['reporter_id'] ?? 0)); $notes = json_decode((string) ($row['notes_json'] ?? '[]'), true); if (!is_array($notes)) { $notes = []; } $normalized_notes = []; foreach ($notes as $entry) { $operator_id = (int) ($entry['operator'] ?? 0); $operator_user = $operator_id > 0 ? get_userdata($operator_id) : null; $normalized_notes[] = [ 'note' => (string) ($entry['note'] ?? ''), 'status' => sanitize_key((string) ($entry['status'] ?? '')), 'action_taken' => sanitize_key((string) ($entry['action_taken'] ?? 'none')), 'operator' => $operator_id, 'operator_name' => $operator_user ? $operator_user->display_name : '', 'timestamp' => (string) ($entry['timestamp'] ?? ''), 'effect' => (string) ($entry['effect'] ?? ''), ]; } $last_note = !empty($normalized_notes) ? $normalized_notes[count($normalized_notes) - 1] : null; return [ 'id' => (int) ($row['id'] ?? 0), 'target_type' => sanitize_key((string) ($row['target_type'] ?? '')), 'target_id' => (int) ($row['target_id'] ?? 0), 'reason' => (string) ($row['reason'] ?? ''), 'category' => sanitize_key((string) ($row['category'] ?? '')), 'status' => sanitize_key((string) ($row['status'] ?? 'pending')), 'created_at' => (string) ($row['created_at'] ?? ''), 'updated_at' => (string) ($row['updated_at'] ?? ''), 'reporter' => $this->build_admin_user_summary($reporter ? (int) $reporter->ID : 0, $reporter), 'target' => $this->build_report_target_summary( sanitize_key((string) ($row['target_type'] ?? '')), (int) ($row['target_id'] ?? 0) ), 'notes' => $normalized_notes, 'note_count' => count($normalized_notes), 'last_note' => $last_note, ]; } private function report_matches_keyword(array $item, string $keyword): bool { $haystack = implode(' ', array_filter([ (string) ($item['reason'] ?? ''), (string) ($item['category'] ?? ''), (string) ($item['reporter']['display_name'] ?? ''), (string) ($item['reporter']['email'] ?? ''), (string) ($item['target']['title'] ?? ''), (string) ($item['target']['display_name'] ?? ''), (string) ($item['target']['excerpt'] ?? ''), (string) ($item['target_id'] ?? ''), ])); return mb_stripos($haystack, $keyword) !== false; } private function build_report_target_summary(string $target_type, int $target_id): array { $summary = [ 'type' => $target_type, 'id' => $target_id, 'title' => '', 'excerpt' => '', 'display_name' => '', 'status' => '', 'url' => '', 'ban_state' => '', ]; if ($target_type === 'post') { $post = get_post($target_id); if ($post instanceof \WP_Post) { $summary['title'] = get_the_title($post); $summary['status'] = (string) $post->post_status; $summary['url'] = get_permalink($post); } } elseif ($target_type === 'comment') { $comment = get_comment($target_id); if ($comment instanceof \WP_Comment) { $summary['display_name'] = (string) $comment->comment_author; $summary['excerpt'] = wp_trim_words(wp_strip_all_tags((string) $comment->comment_content), 24); $summary['status'] = $comment->comment_approved === '1' ? 'approved' : (string) $comment->comment_approved; $summary['url'] = get_comment_link($comment); if (!empty($comment->comment_post_ID)) { $summary['title'] = get_the_title((int) $comment->comment_post_ID); } } } elseif ($target_type === 'user') { $user = get_userdata($target_id); if ($user instanceof \WP_User) { $summary['display_name'] = $user->display_name; $summary['title'] = $user->display_name; $summary['url'] = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($target_id) : ''; $summary['ban_state'] = $this->get_user_restriction_state($target_id); } } return $summary; } /** * 用户当前限制状态摘要(banned|restricted|muted|空),与 Restriction 表实时同步。 */ private function get_user_restriction_state(int $user_id): string { if ($user_id <= 0 || !class_exists('\AKSRT\User\Restriction')) { return ''; } $svc = \AKSRT\User\Restriction::get_instance(); if ($svc->is_banned($user_id)) { return 'banned'; } if ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_SUSPEND)) { return 'restricted'; } if ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_MUTE)) { return 'muted'; } return ''; } private function apply_report_action(string $target_type, int $target_id, string $action_taken, string $reason = ''): string { if (!class_exists('\AKSRT\Messages\ReportCenter')) { return ''; } return \AKSRT\Messages\ReportCenter::get_instance()->apply_action($target_type, $target_id, $action_taken, $reason); } private function get_report_status_options(): array { return [ ['label' => __('待处理', 'aksrt'), 'value' => 'pending'], ['label' => __('处理中', 'aksrt'), 'value' => 'reviewing'], ['label' => __('已解决', 'aksrt'), 'value' => 'resolved'], ['label' => __('已驳回', 'aksrt'), 'value' => 'dismissed'], ]; } private function get_report_target_type_options(): array { return [ ['label' => __('文章', 'aksrt'), 'value' => 'post'], ['label' => __('评论', 'aksrt'), 'value' => 'comment'], ['label' => __('用户', 'aksrt'), 'value' => 'user'], ]; } private function get_report_action_options(string $target_type = ''): array { $common = [ ['label' => __('仅更新状态', 'aksrt'), 'value' => 'none'], ['label' => __('升级人工处理', 'aksrt'), 'value' => 'escalated'], ]; if ($target_type === 'user' || $target_type === '') { $common = array_merge($common, [ ['label' => __('发送警告', 'aksrt'), 'value' => 'warn'], ['label' => __('禁言', 'aksrt'), 'value' => 'mute'], ['label' => __('停权', 'aksrt'), 'value' => 'suspend'], ['label' => __('封禁账号', 'aksrt'), 'value' => 'ban'], ['label' => __('解除限制', 'aksrt'), 'value' => 'clear'], ]); } if ($target_type === 'post' || $target_type === 'comment' || $target_type === '') { $common[] = ['label' => __('转为待审内容', 'aksrt'), 'value' => 'content_removed']; } return $common; } /** * POST /report/submit — 前台举报提交(登录用户) */ public function submit_report(\WP_REST_Request $request): \WP_REST_Response { if (!class_exists('\AKSRT\Messages\ReportCenter')) { return $this->error('举报系统未启用', 500); } $params = $this->get_json_params($request); $target_type = sanitize_key((string) ($params['target_type'] ?? '')); $target_id = (int) ($params['target_id'] ?? 0); $reason = sanitize_textarea_field((string) ($params['reason'] ?? '')); $category = sanitize_key((string) ($params['category'] ?? '')); if ($target_type === '' || $target_id <= 0 || $reason === '') { return $this->error('请填写完整的举报信息'); } $valid_types = ['post', 'comment', 'user']; if (!in_array($target_type, $valid_types, true)) { return $this->error('无效的举报类型'); } $reporter_id = get_current_user_id(); if ($reporter_id <= 0) { return $this->error(__('请先登录', 'aksrt'), 401); } // 同一人对同一目标的待处理工单只允许存在一条 if (\AKSRT\Messages\ReportCenter::get_instance()->has_pending_report($reporter_id, $target_type, $target_id)) { return $this->error(__('你已提交过相同的举报,请等待处理结果', 'aksrt'), 409); } if ($target_type === 'user') { if ($target_id === $reporter_id) { return $this->error(__('不能举报自己', 'aksrt'), 400); } if (!function_exists('aksrt_user_can') || !aksrt_user_can('user_report', $reporter_id, $target_id)) { return $this->error(__('当前账号没有举报用户的权限', 'aksrt'), 403); } } $report_id = \AKSRT\Messages\ReportCenter::get_instance()->submit( $reporter_id, $target_type, $target_id, $reason, $category ); if ($report_id > 0) { return $this->success([ 'message' => __('举报已提交,我们会尽快处理', 'aksrt'), 'data' => ['report_id' => $report_id], ]); } return $this->error(__('举报提交失败', 'aksrt')); } }