feat(users): enforce real bans from report processing and fix appeal lifting

- apply_report_action now writes aksrt_user_restriction via Restriction::restrict
  and lifts via new lift_user_restriction(), replacing the display-only
  aksrt_user_ban_state meta dual-track
- ban type restrictions destroy the target user's sessions on apply
- appeal approval lifts active ban/suspend/mute restrictions instead of
  updating a nonexistent status column
- UsersBan.vue posts to /users/{id}/restrict with valid types (warn/mute/
  suspend/ban), scope and expires_at
- add comment report entry to the comment actions dropdown
- add duplicate pending report guard on submit
- remove dead ReportCenter ajax handlers, option migration, REST alias
  routes and write-only restriction meta
- fix report admin mail link to the real aksrt_user_ban page
- add report-ban contract tests
This commit is contained in:
kate522 committed 2026-09-19 21:38:56 +08:00
1 parent 4dafe96c4b
commit 835e0f983d
12 files changed
+291 -214

No files matched your search

+9 -120
View File
@@ -190,129 +190,18 @@ class ReportCenter {
}
/**
* 一次性迁移:把 option 中的举报数据搬到表中。
* 幂等 — 已存在的 ID 不会重复插入。
* 同一举报人对同一目标是否已有待处理工单(防止重复提交)。
*/
public function migrate_from_option(): int {
public function has_pending_report(int $reporter_id, string $target_type, int $target_id): bool {
$table = $this->table();
if ($table === '') return 0;
$reports = get_option('aksrt_reports', []);
if (!is_array($reports) || empty($reports)) return 0;
if ($table === '') return false;
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());
return (bool) $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$table} WHERE reporter_id = %d AND target_type = %s AND target_id = %d AND status = 'pending' LIMIT 1",
$reporter_id,
sanitize_key($target_type),
$target_id
));
}
}