feat(messages): redesign native message admin page

- Rebuild users.php?page=aksrt_messages: overview stats, collapsible
  compose card (users list / role recipients), read-state views,
  type/recipient/keyword filters, bulk mark-read/delete, shared
  AdminListPagination
- Extract NotificationAdminQuery / AdminBroadcast / MessageTypes under
  src/Messages; page logic lives in UsersMenusMessages trait
- Page-scoped assets/css/admin/messages.css
- Remove dead aksrt_notification_type_label() and
  Message::get_all_notifications()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-27 22:51:11 +08:00
1 parent bbb41547e2
commit 6681b0c306
9 files changed
+1081 -110

No files matched your search

+174
View File
@@ -0,0 +1,174 @@
<?php
/**
* 后台「消息管理」页的通知表查询:分页列表、计数、类型枚举与批量写操作。
*
* 全站视角(不按当前用户过滤),仅供 manage_options 的原生后台页使用;
* 用户自身收件箱读写仍走 Message / inc/modules/user-notifications.php。
*
* @package AKSRT
*/
namespace AKSRT\Messages;
defined('ABSPATH') || exit;
final class NotificationAdminQuery {
public const PER_PAGE = 20;
public const MAX_BULK = 200;
public const STATUSES = ['unread', 'read'];
private const COLUMNS = 'id, user_id, type, title, description, target_url, is_read, created_at, read_at';
public static function table(): string {
return class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('notification') : '';
}
/**
* @param array{status:string,type:string,user_id:int,s:string} $filters user_id:0=不限,<0=无匹配用户
* @return array{items:list<array<string,mixed>>,total:int,page:int}
*/
public static function paginate(array $filters, int $page, int $per_page = self::PER_PAGE): array {
$table = self::table();
if ($table === '') {
return ['items' => [], 'total' => 0, 'page' => 1];
}
global $wpdb;
[$where, $params] = self::where($filters, true);
$total = (int) $wpdb->get_var(self::prepare("SELECT COUNT(*) FROM {$table} WHERE {$where}", $params));
// 先夹页码再查数据:越界页(如删光最后一页)回落到末页而不是空表。
$page = min(max(1, $page), max(1, (int) ceil($total / $per_page)));
$rows = $wpdb->get_results($wpdb->prepare(
'SELECT ' . self::COLUMNS . " FROM {$table} WHERE {$where} ORDER BY created_at DESC, id DESC LIMIT %d OFFSET %d",
array_merge($params, [$per_page, ($page - 1) * $per_page])
), ARRAY_A);
return ['items' => is_array($rows) ? $rows : [], 'total' => $total, 'page' => $page];
}
/**
* 状态视图计数(忽略状态筛选,保留其它筛选)。
*
* @return array{all:int,unread:int,read:int}
*/
public static function status_counts(array $filters): array {
$table = self::table();
if ($table === '') {
return ['all' => 0, 'unread' => 0, 'read' => 0];
}
global $wpdb;
[$where, $params] = self::where($filters, false);
$row = $wpdb->get_row(self::prepare(
"SELECT COUNT(*) AS total, COALESCE(SUM(is_read = 0), 0) AS unread FROM {$table} WHERE {$where}",
$params
), ARRAY_A);
$all = (int) ($row['total'] ?? 0);
$unread = (int) ($row['unread'] ?? 0);
return ['all' => $all, 'unread' => $unread, 'read' => max(0, $all - $unread)];
}
/** @return array{total:int,unread:int,today:int,recipients:int} */
public static function overview(): array {
$table = self::table();
if ($table === '') {
return ['total' => 0, 'unread' => 0, 'today' => 0, 'recipients' => 0];
}
global $wpdb;
// created_at 存 GMT;「今日」按站点时区零点换算。
$today_gmt = get_gmt_from_date(wp_date('Y-m-d 00:00:00'));
$row = $wpdb->get_row($wpdb->prepare(
"SELECT COUNT(*) AS total,
COALESCE(SUM(is_read = 0), 0) AS unread,
COALESCE(SUM(created_at >= %s), 0) AS today,
COUNT(DISTINCT user_id) AS recipients
FROM {$table}",
$today_gmt
), ARRAY_A);
return [
'total' => (int) ($row['total'] ?? 0),
'unread' => (int) ($row['unread'] ?? 0),
'today' => (int) ($row['today'] ?? 0),
'recipients' => (int) ($row['recipients'] ?? 0),
];
}
/** @return list<string> 表中实际出现过的类型 */
public static function types(): array {
$table = self::table();
if ($table === '') {
return [];
}
global $wpdb;
$types = $wpdb->get_col("SELECT DISTINCT type FROM {$table} ORDER BY type ASC LIMIT 100");
return array_values(array_filter(array_map('strval', (array) $types)));
}
/** @return int 实际删除条数 */
public static function delete(array $ids): int {
$ids = self::ids($ids);
$table = self::table();
if ($ids === [] || $table === '') {
return 0;
}
global $wpdb;
$in = implode(',', array_fill(0, count($ids), '%d'));
return (int) $wpdb->query($wpdb->prepare("DELETE FROM {$table} WHERE id IN ({$in})", $ids));
}
/** @return int 由未读变为已读的条数 */
public static function mark_read(array $ids): int {
$ids = self::ids($ids);
$table = self::table();
if ($ids === [] || $table === '') {
return 0;
}
global $wpdb;
$in = implode(',', array_fill(0, count($ids), '%d'));
return (int) $wpdb->query($wpdb->prepare(
"UPDATE {$table} SET is_read = 1, read_at = %s WHERE is_read = 0 AND id IN ({$in})",
array_merge([current_time('mysql', true)], $ids)
));
}
/** @return array{0:string,1:list<mixed>} */
private static function where(array $filters, bool $with_status): array {
global $wpdb;
$where = ['1=1'];
$params = [];
$status = (string) ($filters['status'] ?? '');
if ($with_status && in_array($status, self::STATUSES, true)) {
$where[] = $status === 'unread' ? 'is_read = 0' : 'is_read = 1';
}
$type = (string) ($filters['type'] ?? '');
if ($type !== '') {
$where[] = 'type = %s';
$params[] = $type;
}
$user_id = (int) ($filters['user_id'] ?? 0);
if ($user_id !== 0) {
$where[] = 'user_id = %d';
$params[] = $user_id;
}
$s = (string) ($filters['s'] ?? '');
if ($s !== '') {
$like = '%' . $wpdb->esc_like($s) . '%';
$where[] = '(title LIKE %s OR description LIKE %s)';
$params[] = $like;
$params[] = $like;
}
return [implode(' AND ', $where), $params];
}
private static function prepare(string $sql, array $params): string {
global $wpdb;
return $params === [] ? $sql : (string) $wpdb->prepare($sql, $params);
}
/** @return list<int> */
private static function ids(array $ids): array {
$ids = array_values(array_unique(array_filter(array_map('absint', $ids))));
return array_slice($ids, 0, self::MAX_BULK);
}
}