Move ~94 root class shells and root *-traits into domain directories (core, auth, user, credits, content, payment/gateways, forum, shop, sms, ai, messages, extensions, widgets, settings, admin). Update bootstrap require paths, co-locate traits with owners, fix forum/user relative requires, drop duplicate shop controller requires, and wire home-modules load. Path-only; class names and init order unchanged.
310 lines
10 KiB
PHP
310 lines
10 KiB
PHP
<?php
|
||
/**
|
||
* AKSRT Report Center — 举报治理
|
||
*
|
||
* 使用专用表 {prefix}aksrt_report 存储举报工单。
|
||
* 提供举报受理、状态流转、后台处置入口。
|
||
* 一次性迁移入口保留用于旧 option 数据导入。
|
||
*
|
||
* @package AKSRT
|
||
*/
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
class AKSRT_Report_Center {
|
||
|
||
private static $instance = null;
|
||
|
||
public static function get_instance(): self {
|
||
if (null === self::$instance) self::$instance = new self();
|
||
return self::$instance;
|
||
}
|
||
|
||
private function table(): string {
|
||
return class_exists('AKSRT_DB_Migrator') ? AKSRT_DB_Migrator::table('report') : '';
|
||
}
|
||
|
||
/**
|
||
* 提交举报
|
||
*
|
||
* @param int $reporter_id 举报人 ID
|
||
* @param string $target_type 举报目标类型 (post/comment/user)
|
||
* @param int $target_id 目标 ID
|
||
* @param string $reason 举报原因
|
||
* @param string $category 举报分类
|
||
* @return int 举报 ID(0 表示失败)
|
||
*/
|
||
public function submit(int $reporter_id, string $target_type, int $target_id, string $reason, string $category = ''): int {
|
||
if ($reporter_id <= 0 || $target_id <= 0) return 0;
|
||
|
||
$valid_types = ['post', 'comment', 'user'];
|
||
if (!in_array($target_type, $valid_types, true)) return 0;
|
||
|
||
$table = $this->table();
|
||
if ($table === '') return 0;
|
||
|
||
global $wpdb;
|
||
$wpdb->insert($table, [
|
||
'reporter_id' => $reporter_id,
|
||
'target_type' => sanitize_key($target_type),
|
||
'target_id' => $target_id,
|
||
'reason' => sanitize_textarea_field($reason),
|
||
'category' => sanitize_key($category),
|
||
'status' => 'pending',
|
||
'created_at' => current_time('mysql'),
|
||
], ['%d', '%s', '%d', '%s', '%s', '%s', '%s']);
|
||
|
||
return $wpdb->insert_id ? (int) $wpdb->insert_id : 0;
|
||
}
|
||
|
||
/**
|
||
* 获取待处理举报数
|
||
*/
|
||
public function get_pending_count(): int {
|
||
$table = $this->table();
|
||
if ($table === '') return 0;
|
||
|
||
global $wpdb;
|
||
return (int) $wpdb->get_var(
|
||
"SELECT COUNT(*) FROM {$table} WHERE status = 'pending'"
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 获取举报列表
|
||
*/
|
||
public function get_reports(array $args = []): array {
|
||
$table = $this->table();
|
||
if ($table === '') return [];
|
||
|
||
global $wpdb;
|
||
|
||
$where = [];
|
||
$params = [];
|
||
|
||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||
if ($status !== '') {
|
||
$where[] = 'status = %s';
|
||
$params[] = $status;
|
||
}
|
||
|
||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||
|
||
$where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : '';
|
||
$sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||
$params[] = $limit;
|
||
$params[] = $offset;
|
||
|
||
$rows = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
|
||
|
||
// 附加审核备注
|
||
foreach ($rows as &$row) {
|
||
$row['id'] = (int) $row['id'];
|
||
$row['reporter_id'] = (int) $row['reporter_id'];
|
||
$row['target_id'] = (int) $row['target_id'];
|
||
$row['notes'] = isset($row['notes_json']) ? json_decode($row['notes_json'], true) ?? [] : [];
|
||
}
|
||
|
||
return $rows;
|
||
}
|
||
|
||
/**
|
||
* 更新举报状态
|
||
*/
|
||
public function update_status(int $report_id, string $new_status, string $note = ''): bool {
|
||
$valid_statuses = ['pending', 'reviewing', 'resolved', 'dismissed'];
|
||
if (!in_array($new_status, $valid_statuses, true)) return false;
|
||
|
||
$table = $this->table();
|
||
if ($table === '') return false;
|
||
|
||
global $wpdb;
|
||
|
||
$existing = $wpdb->get_row($wpdb->prepare(
|
||
"SELECT id, notes_json FROM {$table} WHERE id = %d",
|
||
$report_id
|
||
));
|
||
|
||
if (!$existing) return false;
|
||
|
||
$update_data = [
|
||
'status' => $new_status,
|
||
'updated_at' => current_time('mysql'),
|
||
];
|
||
$format = ['%s', '%s'];
|
||
|
||
if ($note !== '') {
|
||
$notes = json_decode($existing->notes_json ?? '[]', true) ?? [];
|
||
$notes[] = [
|
||
'note' => sanitize_textarea_field($note),
|
||
'status' => $new_status,
|
||
'operator' => get_current_user_id(),
|
||
'timestamp' => current_time('mysql'),
|
||
];
|
||
$update_data['notes_json'] = wp_json_encode($notes);
|
||
$format[] = '%s';
|
||
}
|
||
|
||
$result = $wpdb->update($table, $update_data, ['id' => $report_id], $format, ['%d']);
|
||
return $result !== false;
|
||
}
|
||
|
||
/**
|
||
* Get status counts for the admin dashboard.
|
||
*
|
||
* @return array{pending: int, reviewing: int, resolved: int, dismissed: int, total: int}
|
||
*/
|
||
public function get_status_counts(): array {
|
||
$table = $this->table();
|
||
if ($table === '') {
|
||
return ['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0, 'total' => 0];
|
||
}
|
||
|
||
global $wpdb;
|
||
$rows = $wpdb->get_results(
|
||
"SELECT status, COUNT(*) as cnt FROM {$table} GROUP BY status",
|
||
ARRAY_A
|
||
);
|
||
|
||
$counts = ['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0];
|
||
$total = 0;
|
||
foreach ((array) $rows as $row) {
|
||
$s = $row['status'] ?? '';
|
||
if (isset($counts[$s])) {
|
||
$counts[$s] = (int) $row['cnt'];
|
||
}
|
||
$total += (int) $row['cnt'];
|
||
}
|
||
$counts['total'] = $total;
|
||
return $counts;
|
||
}
|
||
|
||
/**
|
||
* 一次性迁移:把 option 中的举报数据搬到表中。
|
||
* 幂等 — 已存在的 ID 不会重复插入。
|
||
*/
|
||
public function migrate_from_option(): int {
|
||
$table = $this->table();
|
||
if ($table === '') return 0;
|
||
|
||
$reports = get_option('aksrt_reports', []);
|
||
if (!is_array($reports) || empty($reports)) return 0;
|
||
|
||
global $wpdb;
|
||
$migrated = 0;
|
||
|
||
foreach ($reports as $old_id => $report) {
|
||
// 检查是否已迁移(通过 reporter_id + target_type + target_id + created_at 去重)
|
||
$existing = $wpdb->get_var($wpdb->prepare(
|
||
"SELECT id FROM {$table} WHERE reporter_id = %d AND target_type = %s AND target_id = %d AND created_at = %s",
|
||
(int) ($report['reporter_id'] ?? 0),
|
||
sanitize_key((string) ($report['target_type'] ?? '')),
|
||
(int) ($report['target_id'] ?? 0),
|
||
(string) ($report['created_at'] ?? '')
|
||
));
|
||
if ($existing) continue;
|
||
|
||
$notes_json = '';
|
||
if (!empty($report['notes']) && is_array($report['notes'])) {
|
||
$notes_json = wp_json_encode($report['notes']);
|
||
}
|
||
|
||
$wpdb->insert($table, [
|
||
'reporter_id' => (int) ($report['reporter_id'] ?? 0),
|
||
'target_type' => sanitize_key((string) ($report['target_type'] ?? '')),
|
||
'target_id' => (int) ($report['target_id'] ?? 0),
|
||
'reason' => sanitize_textarea_field((string) ($report['reason'] ?? '')),
|
||
'category' => sanitize_key((string) ($report['category'] ?? '')),
|
||
'status' => sanitize_key((string) ($report['status'] ?? 'pending')),
|
||
'created_at' => (string) ($report['created_at'] ?? current_time('mysql')),
|
||
'updated_at' => (string) ($report['updated_at'] ?? ''),
|
||
'notes_json' => $notes_json,
|
||
], ['%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s', '%s']);
|
||
|
||
if ($wpdb->insert_id) $migrated++;
|
||
}
|
||
|
||
if ($migrated > 0) {
|
||
update_option('aksrt_report_migrated_to_table', time());
|
||
}
|
||
|
||
return $migrated;
|
||
}
|
||
|
||
/**
|
||
* Initialize AJAX hooks for admin operations.
|
||
*/
|
||
public function init(): void {
|
||
add_action('wp_ajax_aksrt_report_update_status', [$this, 'ajax_update_status']);
|
||
add_action('wp_ajax_aksrt_report_list', [$this, 'ajax_list']);
|
||
add_action('wp_ajax_aksrt_report_counts', [$this, 'ajax_counts']);
|
||
|
||
// 一次性迁移钩子
|
||
add_action('admin_init', [$this, 'maybe_migrate'], 20);
|
||
}
|
||
|
||
/**
|
||
* 自动迁移旧 option 数据(幂等)
|
||
*/
|
||
public function maybe_migrate(): void {
|
||
if (get_option('aksrt_report_migrated_to_table')) return;
|
||
$this->migrate_from_option();
|
||
}
|
||
|
||
/**
|
||
* AJAX: Update report status (admin only).
|
||
*/
|
||
public function ajax_update_status(): void {
|
||
if (!current_user_can('manage_options')) {
|
||
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
|
||
}
|
||
|
||
check_ajax_referer('aksrt_report', '_nonce');
|
||
|
||
$report_id = (int) ($_POST['report_id'] ?? 0);
|
||
$new_status = sanitize_key((string) ($_POST['status'] ?? ''));
|
||
$note = sanitize_textarea_field((string) ($_POST['note'] ?? ''));
|
||
|
||
if ($report_id <= 0 || $new_status === '') {
|
||
wp_send_json_error(['message' => __('参数无效', 'aksrt')]);
|
||
}
|
||
|
||
$result = $this->update_status($report_id, $new_status, $note);
|
||
if ($result) {
|
||
wp_send_json_success(['message' => __('状态已更新', 'aksrt')]);
|
||
}
|
||
|
||
wp_send_json_error(['message' => __('更新失败', 'aksrt')]);
|
||
}
|
||
|
||
/**
|
||
* AJAX: Get report list (admin only).
|
||
*/
|
||
public function ajax_list(): void {
|
||
if (!current_user_can('manage_options')) {
|
||
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
|
||
}
|
||
|
||
$status = sanitize_key((string) ($_GET['status'] ?? ''));
|
||
$limit = max(1, min(100, (int) ($_GET['limit'] ?? 20)));
|
||
$offset = max(0, (int) ($_GET['offset'] ?? 0));
|
||
|
||
$reports = $this->get_reports(['status' => $status, 'limit' => $limit, 'offset' => $offset]);
|
||
$counts = $this->get_status_counts();
|
||
|
||
wp_send_json_success(['reports' => $reports, 'counts' => $counts]);
|
||
}
|
||
|
||
/**
|
||
* AJAX: Get status counts (admin only).
|
||
*/
|
||
public function ajax_counts(): void {
|
||
if (!current_user_can('manage_options')) {
|
||
wp_send_json_error(['message' => __('权限不足', 'aksrt')]);
|
||
}
|
||
|
||
wp_send_json_success($this->get_status_counts());
|
||
}
|
||
}
|