' . esc_html(sprintf(
/* translators: %d count */
@@ -131,7 +132,7 @@ class UsersMenus {
'fields' => 'ID',
]));
if ($pending_appeals > 0) {
- $url = admin_url('users.php?page=aksrt_user_ban&tab=appeals&status=pending');
+ $url = admin_url('users.php?page=aksrt_user_ban&type=appeal&status=pending');
echo '
';
@@ -283,87 +284,27 @@ class UsersMenus {
public function render_ban_page(): void {
$this->ensure_cap();
- echo '
' . esc_html__('举报&禁封', 'aksrt') . '
';
+ echo '
' . esc_html__('用户禁封申诉及用户举报管理', 'aksrt') . '
';
$this->flash();
- $tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : 'reports';
+ $tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : 'tickets';
+ if (!in_array($tab, ['tickets', 'restrictions'], true)) {
+ $tab = 'tickets';
+ }
+
echo '
';
- printf('%s', $tab === 'reports' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban&tab=reports')), esc_html__('举报', 'aksrt'));
+ printf('%s', $tab === 'tickets' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban')), esc_html__('申诉&举报', 'aksrt'));
printf('%s', $tab === 'restrictions' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban&tab=restrictions')), esc_html__('禁封限制', 'aksrt'));
- printf('%s', $tab === 'appeals' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban&tab=appeals')), esc_html__('禁封申诉', 'aksrt'));
echo '
';
if ($tab === 'restrictions') {
$this->render_restrictions_tab();
- } elseif ($tab === 'appeals') {
- $this->render_appeals_tab();
} else {
- $this->render_reports_tab();
+ $this->render_tickets_tab();
}
echo '
';
}
- private function render_reports_tab(): void {
- if (!class_exists('\AKSRT\Messages\ReportCenter')) {
- echo '
' . esc_html__('举报中心未初始化。', 'aksrt') . '
';
- return;
- }
- $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : 'pending';
- $svc = \AKSRT\Messages\ReportCenter::get_instance();
- $counts = $svc->get_status_counts();
-
- echo '
' . esc_html(sprintf(
- /* translators: 1 pending 2 total */
- __('待处理 %1$d / 共 %2$d', 'aksrt'),
- (int) ($counts['pending'] ?? 0),
- (int) ($counts['total'] ?? 0)
- )) . '
';
-
- echo '
';
-
- $items = $svc->get_reports(['status' => $status, 'limit' => 50, 'offset' => 0]);
- echo '
';
- echo '| ID | ' . esc_html__('举报人', 'aksrt') . ' | ' . esc_html__('目标', 'aksrt') . ' | ' . esc_html__('原因', 'aksrt') . ' | ' . esc_html__('状态', 'aksrt') . ' | ' . esc_html__('时间', 'aksrt') . ' | ' . esc_html__('操作', 'aksrt') . ' | ';
- echo '
';
- if (!$items) {
- echo '| ' . esc_html__('暂无举报', 'aksrt') . ' |
';
- }
- foreach ($items as $row) {
- $id = (int) ($row['id'] ?? 0);
- $st = (string) ($row['status'] ?? '');
- $rid = (int) ($row['reporter_id'] ?? 0);
- $ru = $rid > 0 ? get_userdata($rid) : null;
- echo '';
- echo '| ' . esc_html((string) $id) . ' | ';
- echo '' . ($ru ? esc_html($ru->display_name) : esc_html((string) $rid)) . ' | ';
- echo '' . esc_html((string) ($row['target_type'] ?? '') . ' #' . (string) ($row['target_id'] ?? '')) . ' | ';
- echo '' . esc_html((string) ($row['reason'] ?? '')) . ' | ';
- echo '' . esc_html($st) . ' | ';
- echo '' . esc_html((string) ($row['created_at'] ?? '')) . ' | ';
- echo ' |
';
- }
- echo '
';
- }
-
private function render_restrictions_tab(): void {
if (!class_exists('\AKSRT\User\Restriction')) {
echo '
' . esc_html__('限制模块未初始化。', 'aksrt') . '
';
@@ -436,12 +377,29 @@ class UsersMenus {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
- $id = isset($_POST['report_id']) ? (int) $_POST['report_id'] : 0;
- $new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
- $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
+ $id = isset($_POST['report_id']) ? (int) $_POST['report_id'] : 0;
+ $new_status = isset($_POST['new_status']) ? sanitize_key((string) wp_unslash($_POST['new_status'])) : '';
+ $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
+ $action_taken = isset($_POST['action_taken']) ? sanitize_key((string) wp_unslash($_POST['action_taken'])) : 'none';
check_admin_referer('aksrt_users_update_report_' . $id);
- $ok = $id > 0 && class_exists('\AKSRT\Messages\ReportCenter') && \AKSRT\Messages\ReportCenter::get_instance()->update_status($id, $new_status, $note);
- $this->redirect('aksrt_user_ban', $ok ? 'report_updated' : 'report_failed', ['tab' => 'reports']);
+
+ $ok = false;
+ if ($id > 0 && class_exists('\AKSRT\Messages\ReportCenter')) {
+ $svc = \AKSRT\Messages\ReportCenter::get_instance();
+ $ok = $svc->update_status($id, $new_status, $note);
+ if ($ok && $action_taken !== '' && $action_taken !== 'none') {
+ $row = $svc->get_report_row($id);
+ if ($row) {
+ $svc->apply_action(
+ sanitize_key((string) $row['target_type']),
+ (int) $row['target_id'],
+ $action_taken,
+ (string) ($row['reason'] ?? '')
+ );
+ }
+ }
+ }
+ $this->redirect('aksrt_user_ban', $ok ? 'report_updated' : 'report_failed', ['tab' => 'tickets']);
}
public function handle_add_restriction(): void {
@@ -471,90 +429,17 @@ class UsersMenus {
$this->redirect('aksrt_user_ban', $ok ? 'restriction_removed' : 'restriction_failed', ['tab' => 'restrictions']);
}
-
- private function render_appeals_tab(): void {
- if (!class_exists('\AKSRT\User\BanAppeal')) {
- echo '
' . esc_html__('申诉模块未初始化。', 'aksrt') . '
';
- return;
- }
- $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : 'pending';
- if (!in_array($status, ['pending', 'approved', 'rejected', 'all'], true)) {
- $status = 'pending';
- }
-
- echo '
';
-
- $users = get_users([
- 'meta_key' => \AKSRT\User\BanAppeal::META_STATUS,
- 'meta_compare' => 'EXISTS',
- 'number' => 100,
- 'fields' => ['ID', 'display_name', 'user_login', 'user_email'],
- 'orderby' => 'ID',
- 'order' => 'DESC',
- ]);
-
- echo '
';
- echo '| UID | ' . esc_html__('用户', 'aksrt') . ' | ' . esc_html__('状态', 'aksrt') . ' | ' . esc_html__('类型', 'aksrt') . ' | ' . esc_html__('说明', 'aksrt') . ' | ' . esc_html__('提交时间', 'aksrt') . ' | ' . esc_html__('操作', 'aksrt') . ' | ';
- echo '
';
- $shown = 0;
- foreach ($users as $u) {
- $uid = (int) $u->ID;
- $st = \AKSRT\User\BanAppeal::get_status($uid);
- if ($st === '') {
- continue;
- }
- if ($status !== 'all' && $st !== $status) {
- continue;
- }
- $payload = \AKSRT\User\BanAppeal::get_payload($uid);
- $shown++;
- echo '';
- echo '| ' . esc_html((string) $uid) . ' | ';
- echo '' . esc_html($u->display_name . ' (' . $u->user_login . ')') . ' ' . esc_html($u->user_email) . ' | ';
- echo '' . esc_html($st) . ' | ';
- echo '' . esc_html((string) ($payload['reason_key'] ?? '—')) . ' | ';
- echo '' . esc_html((string) ($payload['reason'] ?? '')) . ' | ';
- echo '' . esc_html((string) ($payload['time'] ?? '—')) . ' | ';
- echo '';
- if ($st === 'pending') {
- echo '';
- } else {
- echo '—';
- }
- echo ' |
';
- }
- if ($shown < 1) {
- echo '| ' . esc_html__('暂无申诉', 'aksrt') . ' |
';
- }
- echo '
';
- }
-
public function handle_process_appeal(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
- $user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
+ $user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
$decision = isset($_POST['decision']) ? sanitize_key((string) wp_unslash($_POST['decision'])) : '';
- $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
+ $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : '';
check_admin_referer('aksrt_users_process_appeal_' . $user_id);
if ($user_id <= 0 || !in_array($decision, ['approve', 'reject'], true) || !class_exists('\AKSRT\User\BanAppeal')) {
- $this->redirect('aksrt_user_ban', 'appeal_failed', ['tab' => 'appeals']);
+ $this->redirect('aksrt_user_ban', 'appeal_failed', ['tab' => 'tickets', 'type' => 'appeal']);
}
$status = $decision === 'approve' ? 'approved' : 'rejected';
@@ -564,7 +449,7 @@ class UsersMenus {
$payload['admin_note'] = $note;
$payload['handled_at'] = current_time('Y-m-d H:i');
$payload['handled_by'] = get_current_user_id();
- $payload['decision'] = $status;
+ $payload['decision'] = $status;
update_user_meta($user_id, \AKSRT\User\BanAppeal::META_PAYLOAD, $payload);
$history = get_user_meta($user_id, \AKSRT\User\BanAppeal::META_HISTORY, true);
@@ -598,7 +483,7 @@ class UsersMenus {
$this->redirect(
'aksrt_user_ban',
$decision === 'approve' ? 'appeal_approved' : 'appeal_rejected',
- ['tab' => 'appeals']
+ ['tab' => 'tickets', 'type' => 'appeal']
);
}
diff --git a/src/Admin/Native/UsersMenusTickets.php b/src/Admin/Native/UsersMenusTickets.php
new file mode 100644
index 00000000..80f448f6
--- /dev/null
+++ b/src/Admin/Native/UsersMenusTickets.php
@@ -0,0 +1,452 @@
+get_reports(['limit' => 500]);
+ foreach ((array) $reports as $r) {
+ $st = sanitize_key((string) ($r['status'] ?? ''));
+ $norm = $st === 'reviewing' ? 'pending' : $st;
+ if (!in_array($norm, ['pending', 'resolved', 'dismissed'], true)) {
+ continue;
+ }
+ if ($status !== '' && $norm !== $status) {
+ continue;
+ }
+ if ($send_user > 0 && (int) ($r['reporter_id'] ?? 0) !== $send_user) {
+ continue;
+ }
+ $hay = (string) ($r['reason'] ?? '') . ' ' . (string) ($r['category'] ?? '') . ' ' . (string) ($r['target_type'] ?? '') . ' #' . (string) ($r['target_id'] ?? '');
+ if ($s !== '' && mb_stripos($hay, $s) === false) {
+ continue;
+ }
+ $rows[] = [
+ 'kind' => 'report',
+ 'id' => (int) ($r['id'] ?? 0),
+ 'user_id' => (int) ($r['reporter_id'] ?? 0),
+ 'status' => $norm,
+ 'raw_status' => $st,
+ 'time' => (string) ($r['created_at'] ?? ''),
+ 'updated' => (string) ($r['updated_at'] ?? ''),
+ 'payload' => $r,
+ ];
+ }
+ }
+
+ if ($type !== 'report' && class_exists('\AKSRT\User\BanAppeal')) {
+ $users = get_users([
+ 'meta_key' => \AKSRT\User\BanAppeal::META_STATUS,
+ 'meta_compare' => 'EXISTS',
+ 'number' => 500,
+ 'fields' => ['ID'],
+ 'orderby' => 'ID',
+ 'order' => 'DESC',
+ ]);
+ foreach ($users as $u) {
+ $uid = (int) $u->ID;
+ $st = \AKSRT\User\BanAppeal::get_status($uid);
+ if ($st === '' || !in_array($st, ['pending', 'approved', 'rejected'], true)) {
+ continue;
+ }
+ $norm = $st === 'approved' ? 'resolved' : ($st === 'rejected' ? 'dismissed' : 'pending');
+ if ($status !== '' && $norm !== $status) {
+ continue;
+ }
+ if ($send_user > 0 && $uid !== $send_user) {
+ continue;
+ }
+ $payload = \AKSRT\User\BanAppeal::get_payload($uid);
+ $hay = (string) ($payload['reason'] ?? '') . ' ' . (string) ($payload['reason_key'] ?? '');
+ if ($s !== '' && mb_stripos($hay, $s) === false) {
+ continue;
+ }
+ $rows[] = [
+ 'kind' => 'appeal',
+ 'id' => $uid,
+ 'user_id' => $uid,
+ 'status' => $norm,
+ 'raw_status' => $st,
+ 'time' => (string) ($payload['time'] ?? ''),
+ 'updated' => (string) ($payload['handled_at'] ?? ''),
+ 'payload' => $payload,
+ ];
+ }
+ }
+
+ $weights = ['pending' => 0, 'resolved' => 1, 'dismissed' => 2];
+ usort($rows, static function (array $a, array $b) use ($orderby, $order, $weights): int {
+ if ($orderby === 'status') {
+ $cmp = ($weights[$a['status']] ?? 9) <=> ($weights[$b['status']] ?? 9);
+ } elseif ($orderby === 'type') {
+ $cmp = strcmp($a['kind'], $b['kind']);
+ } else {
+ $cmp = strcmp($a['time'], $b['time']);
+ }
+ return $order === 'ASC' ? $cmp : -$cmp;
+ });
+
+ return $rows;
+ }
+
+ public function render_tickets_tab(): void {
+ $type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : '';
+ $type = in_array($type, ['report', 'appeal'], true) ? $type : '';
+ $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
+ $status = in_array($status, ['pending', 'resolved', 'dismissed'], true) ? $status : '';
+ $s = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : '';
+ $send_user = isset($_GET['send_user']) ? (int) $_GET['send_user'] : 0;
+ $paged = isset($_GET['paged']) ? max(1, (int) $_GET['paged']) : 1;
+ $orderby = isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : 'time';
+ $orderby = in_array($orderby, ['time', 'status', 'type'], true) ? $orderby : 'time';
+ $order = (isset($_GET['order']) && strtoupper((string) $_GET['order']) === 'ASC') ? 'ASC' : 'DESC';
+
+ $action_url = admin_url('users.php?page=aksrt_user_ban');
+ $page_url = add_query_arg(array_filter([
+ 'tab' => 'tickets',
+ 'type' => $type,
+ 'status' => $status,
+ 's' => $s,
+ 'send_user' => $send_user,
+ ], static function ($v) { return $v !== '' && $v !== 0; }), $action_url);
+
+ $rows = $this->collect_ticket_rows($type, $status, $s, $send_user, $orderby, $order);
+ $total = count($rows);
+ $pages = max(1, (int) ceil($total / self::TICKETS_PER_PAGE));
+ $paged = min($paged, $pages);
+ $tickets = array_slice($rows, ($paged - 1) * self::TICKETS_PER_PAGE, self::TICKETS_PER_PAGE);
+
+ // 类型筛选按钮组(对齐参考主题:全部 / 禁封申诉 / 用户举报)
+ echo '
';
+ foreach (['' => __('全部', 'aksrt'), 'appeal' => __('禁封申诉', 'aksrt'), 'report' => __('用户举报', 'aksrt')] as $t => $label) {
+ $url = $t === ''
+ ? remove_query_arg(['s', 'type', 'action', 'id'], $page_url)
+ : add_query_arg('type', $t, remove_query_arg(['s', 'action', 'id'], $page_url));
+ printf('
%s ', $type === $t ? ' button-primary' : '', esc_url($url), esc_html($label));
+ }
+ echo '
';
+
+ // 状态 subsubsub(对齐参考主题:全部 | 待处理 | 已批准 | 已拒绝)
+ echo '
';
+ $status_links = [
+ '' => __('全部', 'aksrt'),
+ 'pending' => __('待处理', 'aksrt'),
+ 'resolved' => __('已批准', 'aksrt'),
+ 'dismissed'=> __('已拒绝', 'aksrt'),
+ ];
+ $i = 0;
+ foreach ($status_links as $key => $label) {
+ $url = $key === '' ? remove_query_arg(['s', 'status'], $page_url) : add_query_arg('status', $key, remove_query_arg('s', $page_url));
+ $sep = ++$i < count($status_links) ? ' |' : '';
+ printf('- %s%s
', esc_url($url), esc_html($label), esc_html($sep));
+ }
+ echo '
';
+
+ // 搜索(对齐参考主题:右侧搜索记录表单)
+ echo '';
+
+ if ($s !== '') {
+ echo '
"' . esc_html($s) . '" ' . esc_html__('的搜索结果', 'aksrt') . '
';
+ }
+
+ echo '
';
+ echo '
';
+ echo '';
+
+ // 表头(对齐参考主题:状态/类型/提交用户/详情/提交时间/更新时间 + 排序箭头)
+ $theads = [
+ ['width' => '5%', 'orderby' => 'status', 'name' => __('状态', 'aksrt')],
+ ['width' => '5%', 'orderby' => 'type', 'name' => __('类型', 'aksrt')],
+ ['width' => '5%', 'orderby' => '', 'name' => __('提交用户', 'aksrt')],
+ ['width' => '18%', 'orderby' => '', 'name' => __('详情', 'aksrt')],
+ ['width' => '6%', 'orderby' => 'time', 'name' => __('提交时间', 'aksrt')],
+ ['width' => '6%', 'orderby' => 'updated','name' => __('更新时间', 'aksrt')],
+ ];
+ foreach ($theads as $thead) {
+ $orderby_html = '';
+ if ($thead['orderby'] !== '') {
+ $orderby_url = add_query_arg('orderby', $thead['orderby'], $page_url);
+ $orderby_html = '';
+ $orderby_html .= '';
+ $orderby_html .= '';
+ $orderby_html .= '';
+ }
+ echo '| ' . esc_html($thead['name']) . $orderby_html . ' | ';
+ }
+ echo '
';
+
+ if (!$tickets) {
+ echo '| ' . esc_html__('暂无申请记录', 'aksrt') . ' |
';
+ }
+
+ $process_action = isset($_GET['action']) ? sanitize_key((string) wp_unslash($_GET['action'])) : '';
+ $process_id = isset($_GET['id']) ? (string) wp_unslash($_GET['id']) : '';
+
+ foreach ($tickets as $row) {
+ $this->render_ticket_row($row, $page_url);
+ if ($process_action === 'process' && $process_id === (string) $row['kind'] . '-' . (string) $row['id']) {
+ $this->render_ticket_process_form($row, $action_url);
+ }
+ }
+
+ echo '
';
+
+ $this->pagination($pages, $paged);
+ }
+
+ private function render_ticket_row(array $row, string $page_url): void {
+ $kind = $row['kind'];
+ $status = $row['status'];
+ $user_id = (int) $row['user_id'];
+ $url = add_query_arg(['action' => 'process', 'id' => $kind . '-' . $row['id']], $page_url);
+
+ // 状态列(对齐参考主题:待处理→立即处理按钮 / 已批准 / 处理完成 / 已拒绝)
+ if ($status === 'pending') {
+ $status_html = '
' . esc_html__('立即处理', 'aksrt') . '';
+ } elseif ($status === 'resolved') {
+ $status_html = $kind === 'appeal'
+ ? '
' . esc_html__('已批准', 'aksrt') . ''
+ : '
' . esc_html__('处理完成', 'aksrt') . '';
+ } else {
+ $status_html = '
' . esc_html__('已拒绝', 'aksrt') . '';
+ }
+
+ // 类型列(对齐参考主题:不良举报红 / 禁封申诉绿)
+ $type_html = $kind === 'report'
+ ? '
' . esc_html__('不良举报', 'aksrt') . ''
+ : '
' . esc_html__('禁封申诉', 'aksrt') . '';
+
+ // 提交用户列(显示名链接 + 管理|查看 row-actions)
+ $user = $user_id > 0 ? get_userdata($user_id) : null;
+ if ($user) {
+ $user_html = '
' . esc_html($user->display_name) . '';
+ $user_html .= '
';
+ } else {
+ $user_html = esc_html__('未登录用户', 'aksrt');
+ }
+
+ $desc_html = $kind === 'report' ? $this->ticket_report_desc_html($row['payload']) : $this->ticket_appeal_desc_html($row['payload'], (int) $row['id']);
+
+ echo '
';
+ echo '| ' . $status_html . ' | ';
+ echo '' . $type_html . ' | ';
+ echo '' . $user_html . ' | ';
+ echo '' . $desc_html . ' | ';
+ echo '' . esc_html((string) $row['time']) . ' | ';
+ echo '' . esc_html((string) ($row['updated'] !== '' ? $row['updated'] : '—')) . ' | ';
+ echo '
';
+ }
+
+ /** 举报行详情(对齐参考主题:举报目标红字 + 原因 + 分类) */
+ private function ticket_report_desc_html(array $r): string {
+ $target_type = sanitize_key((string) ($r['target_type'] ?? ''));
+ $target_id = (int) ($r['target_id'] ?? 0);
+ $link = $this->ticket_target_link($target_type, $target_id);
+
+ $html = '
' . esc_html(sprintf(__('举报目标:%s', 'aksrt'), $link !== '' ? $link : $target_type . ' #' . $target_id)) . '
';
+ $html .= '
' . esc_html(sprintf(__('举报原因:%s', 'aksrt'), (string) ($r['reason'] ?? ''))) . '
';
+ if (!empty($r['category'])) {
+ $html .= '
' . esc_html(sprintf(__('分类:%s', 'aksrt'), (string) $r['category'])) . '
';
+ }
+ return $html;
+ }
+
+ /** 申诉行详情(对齐参考主题:禁封状态/原因/起止时间,或已解封) */
+ private function ticket_appeal_desc_html(array $payload, int $user_id): string {
+ $ban_html = $this->ticket_ban_info_html($user_id);
+ $html = $ban_html;
+ if (!empty($payload['reason'])) {
+ $html .= '
' . esc_html(sprintf(__('申诉说明:%s', 'aksrt'), (string) $payload['reason'])) . '
';
+ }
+ return $html;
+ }
+
+ /** 用户当前限制状态摘要(对齐参考主题禁封信息展示) */
+ private function ticket_ban_info_html(int $user_id): string {
+ if ($user_id <= 0 || !class_exists('\AKSRT\User\Restriction')) {
+ return '';
+ }
+ $svc = \AKSRT\User\Restriction::get_instance();
+ $ban = $svc->get_active_ban($user_id);
+
+ if ($ban) {
+ $end = !empty($ban['expires_at']) ? (string) $ban['expires_at'] : __('永久', 'aksrt');
+ $html = '
' . esc_html(sprintf(__('禁封状态:%s', 'aksrt'), __('封禁中(禁止登录)', 'aksrt'))) . '
';
+ $html .= '
' . esc_html(sprintf(__('禁封原因:%s', 'aksrt'), (string) ($ban['reason'] ?? ''))) . '
';
+ $html .= '
' . esc_html(sprintf(__('开始时间:%s', 'aksrt'), (string) ($ban['created_at'] ?? ''))) . '
';
+ $html .= '
' . esc_html(sprintf(__('结束时间:%s', 'aksrt'), $end)) . '
';
+ return $html;
+ }
+
+ if ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_SUSPEND)) {
+ return '
' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('停权中', 'aksrt'))) . '
';
+ }
+ if ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_MUTE)) {
+ return '
' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('禁言中', 'aksrt'))) . '
';
+ }
+
+ return '
' . esc_html__('用户状态正常', 'aksrt') . '
';
+ }
+
+ private function ticket_target_link(string $target_type, int $target_id): string {
+ if ($target_type === 'post') {
+ $post = get_post($target_id);
+ if ($post instanceof \WP_Post) {
+ return '
' . esc_html(get_the_title($post)) . '';
+ }
+ } elseif ($target_type === 'comment') {
+ $comment = get_comment($target_id);
+ if ($comment instanceof \WP_Comment) {
+ return '
#' . $target_id . '';
+ }
+ } elseif ($target_type === 'user') {
+ $url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($target_id) : '';
+ $user = get_userdata($target_id);
+ if ($url !== '' && $user) {
+ return '
' . esc_html($user->display_name) . '';
+ }
+ }
+ return '';
+ }
+
+ /** 行内处理表单(对齐参考主题 form-table 结构) */
+ private function render_ticket_process_form(array $row, string $action_url): void {
+ $kind = $row['kind'];
+
+ echo '
';
+ }
+}
diff --git a/src/Admin/UserControllerReportOperations.php b/src/Admin/UserControllerReportOperations.php
index 045e94f0..668bdb18 100644
--- a/src/Admin/UserControllerReportOperations.php
+++ b/src/Admin/UserControllerReportOperations.php
@@ -354,61 +354,10 @@ trait UserControllerReportOperations {
}
private function apply_report_action(string $target_type, int $target_id, string $action_taken, string $reason = ''): string {
- if ($target_id <= 0 || $action_taken === 'none') {
+ if (!class_exists('\AKSRT\Messages\ReportCenter')) {
return '';
}
-
- if ($target_type === 'user') {
- if (!class_exists('\AKSRT\User\Restriction')) {
- return '';
- }
- $svc = \AKSRT\User\Restriction::get_instance();
-
- if ($action_taken === 'clear') {
- $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 '';
- }
-
- if ($action_taken === 'content_removed' && $target_type === 'post' && get_post($target_id)) {
- wp_update_post([
- 'ID' => $target_id,
- 'post_status' => 'pending',
- ]);
- return __('已将内容改为待审状态', 'aksrt');
- }
-
- if ($action_taken === 'content_removed' && $target_type === 'comment' && get_comment($target_id)) {
- wp_set_comment_status($target_id, 'hold', true);
- return __('已将评论改为待审状态', 'aksrt');
- }
-
- return '';
+ return \AKSRT\Messages\ReportCenter::get_instance()->apply_action($target_type, $target_id, $action_taken, $reason);
}
private function get_report_status_options(): array {
diff --git a/src/Messages/ReportCenter.php b/src/Messages/ReportCenter.php
index c32db01c..9185e35c 100644
--- a/src/Messages/ReportCenter.php
+++ b/src/Messages/ReportCenter.php
@@ -159,6 +159,87 @@ class ReportCenter {
return $result !== false;
}
+ /**
+ * 获取单条举报原始行(ARRAY_A),不存在返回 null。
+ */
+ public function get_report_row(int $report_id): ?array {
+ $table = $this->table();
+ if ($table === '' || $report_id <= 0) {
+ return null;
+ }
+
+ global $wpdb;
+ $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $report_id), ARRAY_A);
+ return $row ?: null;
+ }
+
+ /**
+ * 对举报目标执行处罚动作(REST 与原生后台共用入口)。
+ *
+ * @param string $target_type 目标类型 post/comment/user
+ * @param int $target_id 目标 ID
+ * @param string $action_taken none/warn/mute/suspend/ban/clear/content_removed
+ * @param string $reason 处罚原因(写入限制记录)
+ * @return string 执行结果描述,未执行返回空串
+ */
+ public function apply_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 (!class_exists('\AKSRT\User\Restriction')) {
+ return '';
+ }
+ $svc = \AKSRT\User\Restriction::get_instance();
+
+ if ($action_taken === 'clear') {
+ $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 '';
+ }
+
+ if ($action_taken === 'content_removed' && $target_type === 'post' && get_post($target_id)) {
+ wp_update_post([
+ 'ID' => $target_id,
+ 'post_status' => 'pending',
+ ]);
+ return __('已将内容改为待审状态', 'aksrt');
+ }
+
+ if ($action_taken === 'content_removed' && $target_type === 'comment' && get_comment($target_id)) {
+ wp_set_comment_status($target_id, 'hold', true);
+ return __('已将评论改为待审状态', 'aksrt');
+ }
+
+ return '';
+ }
+
/**
* Get status counts for the admin dashboard.
*
diff --git a/src/Settings/MailNotifications.php b/src/Settings/MailNotifications.php
index 68b22318..c34b0424 100644
--- a/src/Settings/MailNotifications.php
+++ b/src/Settings/MailNotifications.php
@@ -176,7 +176,7 @@ final class MailNotifications {
public static function report_submitted(int $report_id, array $report): void {
$reporter = get_userdata((int) ($report['reporter_id'] ?? 0));
- self::send_admin('report_admin', ['username' => $reporter instanceof \WP_User ? $reporter->display_name : __('用户', 'aksrt'), 'title' => (string) ($report['target_type'] ?? ''), 'url' => admin_url('users.php?page=aksrt_user_ban&tab=reports')]);
+ self::send_admin('report_admin', ['username' => $reporter instanceof \WP_User ? $reporter->display_name : __('用户', 'aksrt'), 'title' => (string) ($report['target_type'] ?? ''), 'url' => admin_url('users.php?page=aksrt_user_ban')]);
}
public static function report_status_changed(int $report_id, int $reporter_id, string $status): void {
diff --git a/tests/widget-system/report-ban-contract.test.cjs b/tests/widget-system/report-ban-contract.test.cjs
index 2c1022df..bec279d7 100644
--- a/tests/widget-system/report-ban-contract.test.cjs
+++ b/tests/widget-system/report-ban-contract.test.cjs
@@ -43,11 +43,13 @@ test('report center has no legacy ajax handlers or option migration', () => {
assert.match(reportOps, /has_pending_report\(\$reporter_id, \$target_type, \$target_id\)/)
})
-test('report processing writes real restrictions, not a display-only meta', () => {
- assert.match(reportOps, /->restrict\(\s*\$target_id,\s*\$action_taken,/)
- assert.match(reportOps, /lift_user_restriction\(\$target_id\)/)
- assert.match(reportOps, /in_array\(\$action_taken, \\AKSRT\\User\\Restriction::TYPES, true\)/)
- assert.ok(!reportOps.includes('aksrt_user_ban_state'), 'dual-track ban state meta must not be written or read')
+test('report processing delegates actions to the shared ReportCenter entry', () => {
+ assert.match(reportOps, /ReportCenter::get_instance\(\)->apply_action\(\$target_type, \$target_id, \$action_taken, \$reason\)/)
+ assert.match(reportCenter, /public function apply_action\(/)
+ assert.match(reportCenter, /->restrict\(\s*\$target_id,\s*\$action_taken,/)
+ assert.match(reportCenter, /lift_user_restriction\(\$target_id\)/)
+ assert.match(reportCenter, /in_array\(\$action_taken, \\AKSRT\\User\\Restriction::TYPES, true\)/)
+ assert.ok(!reportCenter.includes('aksrt_user_ban_state'), 'dual-track ban state meta must not be written or read')
for (const deadAction of ["'value' => 'warned'", "'value' => 'muted'", "'value' => 'restricted'", "'value' => 'banned'"]) {
assert.ok(!reportOps.includes(deadAction), `action value must map to Restriction types: ${deadAction}`)
}
@@ -55,6 +57,8 @@ test('report processing writes real restrictions, not a display-only meta', () =
assert.ok(reportOps.includes(action), `missing action option: ${action}`)
}
assert.match(reportOps, /get_user_restriction_state/)
+ assert.match(reportCenter, /public function get_report_row\(/)
+ assert.match(usersMenus, /\$svc->apply_action\(/)
})
test('restriction class supports lifting and kicks sessions on ban', () => {
@@ -93,8 +97,46 @@ test('profile governance notice reads from the restriction table', () => {
})
test('admin report notification links to the real page', () => {
- assert.match(mailNotifications, /users\.php\?page=aksrt_user_ban&tab=reports/)
+ assert.match(mailNotifications, /users\.php\?page=aksrt_user_ban'/)
assert.ok(!mailNotifications.includes('page=aksrt_reports&'), 'dead admin page link')
+ assert.ok(!mailNotifications.includes('tab=reports'), 'merged ticket page has no reports tab')
+})
+
+test('native ban page mirrors the reference theme layout', () => {
+ const tickets = read('src/Admin/Native/UsersMenusTickets.php')
+
+ // 菜单与页面标题对齐参考主题
+ assert.ok(usersMenus.includes(`__('举报&禁封申诉', 'aksrt'),\n __('举报&禁封申诉', 'aksrt')`), 'submenu title should be 举报&禁封申诉')
+ assert.ok(usersMenus.includes('用户禁封申诉及用户举报管理'), 'page heading should match the reference theme')
+
+ // 类型筛选按钮组 + 状态 subsubsub + 搜索
+ for (const label of [`__('禁封申诉', 'aksrt')`, `__('用户举报', 'aksrt')`, `__('待处理', 'aksrt')`, `__('已批准', 'aksrt')`, `__('已拒绝', 'aksrt')`, `__('搜索记录', 'aksrt')`]) {
+ assert.ok(tickets.includes(label), `missing reference-theme label: ${label}`)
+ }
+
+ // 表头列与参考主题一致:状态/类型/提交用户/详情/提交时间/更新时间(带排序)
+ for (const head of ['状态', '类型', '提交用户', '详情', '提交时间', '更新时间']) {
+ assert.ok(tickets.includes(`__('${head}', 'aksrt')`), `missing table head: ${head}`)
+ }
+ assert.match(tickets, /orderby-but/)
+ assert.match(tickets, /dashicons-arrow-up/)
+
+ // 行内处理表单对齐参考主题
+ for (const label of [`__('立即处理', 'aksrt')`, `__('申诉通过(解封该用户)', 'aksrt')`, `__('申诉拒绝(保持原禁封状态)', 'aksrt')`, `__('此处内容会反馈给举报人', 'aksrt')`, `__('确认提交', 'aksrt')`, `__('用户状态正常', 'aksrt')`, `__('暂无申请记录', 'aksrt')`]) {
+ assert.ok(tickets.includes(label), `missing process form label: ${label}`)
+ }
+
+ // 分页(每页 20 条,与参考主题一致)
+ assert.match(tickets, /TICKETS_PER_PAGE = 20/)
+ assert.match(usersMenus, /private function pagination\(int \$pages, int \$page_num\): void/)
+ assert.match(usersMenus, /paginate_links\(/)
+
+ // 旧的三 tab 结构已收敛
+ assert.ok(!usersMenus.includes('tab=reports'), 'old reports tab is gone')
+ assert.ok(!usersMenus.includes('tab=appeals'), 'old appeals tab is gone')
+ assert.ok(!usersMenus.includes('render_reports_tab'))
+ assert.ok(!usersMenus.includes('render_appeals_tab'))
+ assert.match(usersMenus, /use UsersMenusTickets;/)
})
test('comments expose a report entry for other users when reporting is enabled', () => {