- rename the users.php submenu to 举报&禁封申诉 and the page heading to 用户禁封申诉及用户举报管理 - merge report tickets and ban appeals into one Zibll-style queue: type filter buttons (全部/禁封申诉/用户举报), status subsubsub (全部/待处理/已批准/已拒绝), search box, sortable widefat table with the same column set (状态/类型/提交用户/详情/提交时间/更新时间), inline process forms and 20-per-page pagination - report process form shows the reported target, restriction status and an enforcement action select; appeal form offers approve/reject radios with a message to the user - move restriction enforcement into ReportCenter::apply_action so REST and the native page share one implementation; add get_report_row() - drop the old three-tab rendering (reports/appeals tabs) and fix all notice/mail/SPA links to the merged page
462 lines
18 KiB
PHP
462 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* @package AKSRT
|
|
*/
|
|
|
|
namespace AKSRT\Admin;
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
trait UserControllerReportOperations {
|
|
|
|
public function get_report_list(\WP_REST_Request $request): \WP_REST_Response {
|
|
if (!class_exists('\AKSRT\Messages\ReportCenter')) {
|
|
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(),
|
|
],
|
|
],
|
|
]);
|
|
}
|
|
|
|
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'));
|
|
}
|
|
}
|