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:
2026-09-19 21:38:56 +08:00
parent 4dafe96c4b
commit 835e0f983d
12 changed files with 291 additions and 214 deletions
+61 -33
View File
@@ -121,27 +121,6 @@ trait UserControllerReportOperations {
]);
}
public function update_report_status(\WP_REST_Request $request): \WP_REST_Response {
if (!class_exists('\AKSRT\Messages\ReportCenter')) {
return $this->error(__('举报系统未启用', 'aksrt'));
}
$params = $this->get_json_params($request);
$report_id = (int) ($params['report_id'] ?? 0);
if ($report_id <= 0) {
return $this->error(__('参数无效', 'aksrt'));
}
return $this->handle_report_process($report_id, $params);
}
public function get_report_counts(): \WP_REST_Response {
if (!class_exists('\AKSRT\Messages\ReportCenter')) {
return $this->success(['pending' => 0, 'reviewing' => 0, 'resolved' => 0, 'dismissed' => 0, 'total' => 0]);
}
return $this->success(\AKSRT\Messages\ReportCenter::get_instance()->get_status_counts());
}
public function get_report_detail(\WP_REST_Request $request): \WP_REST_Response {
global $wpdb;
@@ -214,7 +193,8 @@ trait UserControllerReportOperations {
$effect = $this->apply_report_action(
sanitize_key((string) $row['target_type']),
(int) $row['target_id'],
$action_taken
$action_taken,
(string) ($row['reason'] ?? '')
);
$notes = json_decode((string) ($row['notes_json'] ?? '[]'), true);
@@ -346,27 +326,70 @@ trait UserControllerReportOperations {
$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'] = (string) get_user_meta($target_id, 'aksrt_user_ban_state', true);
$summary['ban_state'] = $this->get_user_restriction_state($target_id);
}
}
return $summary;
}
private function apply_report_action(string $target_type, int $target_id, string $action_taken): string {
/**
* 用户当前限制状态摘要(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 ($target_id <= 0 || $action_taken === 'none') {
return '';
}
if ($target_type === 'user') {
if (in_array($action_taken, ['muted', 'restricted', 'banned'], true)) {
update_user_meta($target_id, 'aksrt_user_ban_state', $action_taken);
return sprintf(__('已同步用户限制状态:%s', 'aksrt'), $action_taken);
if (!class_exists('\AKSRT\User\Restriction')) {
return '';
}
$svc = \AKSRT\User\Restriction::get_instance();
if ($action_taken === 'clear') {
delete_user_meta($target_id, 'aksrt_user_ban_state');
return __('已清除用户限制状态', 'aksrt');
$removed = $svc->lift_user_restriction($target_id);
return $removed > 0
? sprintf(__('已解除 %d 条生效中的限制', 'aksrt'), $removed)
: __('该用户没有生效中的限制', 'aksrt');
}
if (in_array($action_taken, \AKSRT\User\Restriction::TYPES, true)) {
$labels = [
\AKSRT\User\Restriction::TYPE_WARN => __('警告', 'aksrt'),
\AKSRT\User\Restriction::TYPE_MUTE => __('禁言', 'aksrt'),
\AKSRT\User\Restriction::TYPE_SUSPEND => __('停权', 'aksrt'),
\AKSRT\User\Restriction::TYPE_BAN => __('封禁', 'aksrt'),
];
$result = $svc->restrict(
$target_id,
$action_taken,
$reason !== '' ? $reason : __('举报处理处罚', 'aksrt'),
\AKSRT\User\Restriction::SCOPE_GLOBAL,
'',
get_current_user_id()
);
if (!empty($result['success'])) {
return sprintf(__('已对用户执行限制:%s', 'aksrt'), $labels[$action_taken] ?? $action_taken);
}
return '';
}
return '';
@@ -413,10 +436,10 @@ trait UserControllerReportOperations {
if ($target_type === 'user' || $target_type === '') {
$common = array_merge($common, [
['label' => __('发送警告', 'aksrt'), 'value' => 'warned'],
['label' => __('禁言', 'aksrt'), 'value' => 'muted'],
['label' => __('限制功能', 'aksrt'), 'value' => 'restricted'],
['label' => __('封禁账号', 'aksrt'), 'value' => 'banned'],
['label' => __('发送警告', 'aksrt'), 'value' => 'warn'],
['label' => __('禁言', 'aksrt'), 'value' => 'mute'],
['label' => __('停权', 'aksrt'), 'value' => 'suspend'],
['label' => __('封禁账号', 'aksrt'), 'value' => 'ban'],
['label' => __('解除限制', 'aksrt'), 'value' => 'clear'],
]);
}
@@ -455,6 +478,11 @@ trait UserControllerReportOperations {
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);