>,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_start, $today_end] = self::today_range(); $row = $wpdb->get_row($wpdb->prepare( "SELECT COUNT(*) AS total, COALESCE(SUM(is_read = 0), 0) AS unread, COALESCE(SUM(created_at >= %s AND created_at < %s), 0) AS today, COUNT(DISTINCT user_id) AS recipients FROM {$table}", $today_start, $today_end ), 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 表中实际出现过的类型 */ 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} */ 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; } if (($filters['period'] ?? '') === 'today') { $where[] = 'created_at >= %s AND created_at < %s'; array_push($params, ...self::today_range()); } return [implode(' AND ', $where), $params]; } /** 先在站点时区计算自然日,再转换成 GMT;正确处理夏令时的 23 / 25 小时日。 */ private static function today_range(): array { $start = current_datetime()->setTime(0, 0, 0); return [ get_gmt_from_date($start->format('Y-m-d H:i:s')), get_gmt_from_date($start->modify('+1 day')->format('Y-m-d H:i:s')), ]; } private static function prepare(string $sql, array $params): string { global $wpdb; return $params === [] ? $sql : (string) $wpdb->prepare($sql, $params); } /** @return list */ 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); } }