ensure_cap(); $filters = $this->message_filters(); $paged = max(1, absint($_GET['paged'] ?? 1)); echo '
'; echo '

' . esc_html__('消息管理', 'aksrt') . '

'; printf( '%s', esc_url(admin_url('admin.php?page=aksrt-settings#/users/notifications')), esc_html__('通知配置', 'aksrt') ); echo '
'; $this->render_message_notices(); if (NotificationAdminQuery::table() === '') { echo '

' . esc_html__('通知表不可用。', 'aksrt') . '

'; return; } $query = $this->message_query_filters($filters); $result = NotificationAdminQuery::paginate($query, $paged); $this->render_message_overview(); $this->render_message_compose(); $this->render_message_views($filters, NotificationAdminQuery::status_counts($query)); $this->render_message_filter_form($filters); $this->render_message_table($result['items'], $filters, $result['page'], $query['user_id'] < 0); AdminListPagination::render( self::MESSAGE_PAGE_SLUG, $result['total'], $result['page'], NotificationAdminQuery::PER_PAGE, $this->message_filter_args($filters), 'users.php' ); echo ''; } /* ------------------------------------------------------------------ * 写操作 * ----------------------------------------------------------------*/ public function handle_send_message(): void { $this->ensure_cap(); check_admin_referer('aksrt_users_send_message'); $mode = sanitize_key((string) wp_unslash($_POST['recipient_mode'] ?? 'users')); $title = trim(sanitize_text_field((string) wp_unslash($_POST['title'] ?? ''))); $content = trim(sanitize_textarea_field((string) wp_unslash($_POST['content'] ?? ''))); $target_url = esc_url_raw(trim((string) wp_unslash($_POST['target_url'] ?? ''))); if ($content === '') { $this->message_fail(__('请填写消息内容。', 'aksrt')); } if (mb_strlen($content) > AdminBroadcast::CONTENT_MAX) { $this->message_fail(sprintf(__('消息内容不能超过 %d 字。', 'aksrt'), AdminBroadcast::CONTENT_MAX)); } $title = mb_substr($title !== '' ? $title : $content, 0, AdminBroadcast::TITLE_MAX); $unknown = 0; if ($mode === 'role') { $ids = AdminBroadcast::resolve_role(sanitize_key((string) wp_unslash($_POST['role'] ?? ''))); } else { $resolved = AdminBroadcast::resolve_users(sanitize_textarea_field((string) wp_unslash($_POST['users'] ?? ''))); $ids = is_wp_error($resolved) ? $resolved : $resolved['ids']; $unknown = is_wp_error($resolved) ? 0 : count($resolved['unknown']); } if (is_wp_error($ids)) { $this->message_fail($ids->get_error_message()); } if ($ids === []) { $this->message_fail(__('没有匹配到任何有效收件人。', 'aksrt')); } $sent = AdminBroadcast::send($ids, $title, $content, $target_url); $this->message_redirect([ 'aksrt_msg' => $sent['sent'] > 0 ? 'sent' : 'send_failed', 'aksrt_msg_n' => $sent['sent'], 'aksrt_msg_f' => $sent['failed'], 'aksrt_msg_u' => $unknown, ]); } public function handle_bulk_messages(): void { $this->ensure_cap(); check_admin_referer('aksrt_users_bulk_messages'); $op = sanitize_key((string) wp_unslash($_POST['bulk_action'] ?? '')); $ids = isset($_POST['ids']) && is_array($_POST['ids']) ? array_map('absint', wp_unslash($_POST['ids'])) : []; $back = $this->message_filter_args($this->message_filters($_POST)); $paged = absint($_POST['paged'] ?? 0); if ($paged > 1) { $back['paged'] = $paged; } if (!in_array($op, ['delete', 'mark_read'], true) || array_filter($ids) === []) { $this->message_redirect(array_merge($back, ['aksrt_msg' => 'bulk_none'])); } $count = $op === 'delete' ? NotificationAdminQuery::delete($ids) : NotificationAdminQuery::mark_read($ids); $this->message_redirect(array_merge($back, [ 'aksrt_msg' => $op === 'delete' ? 'deleted' : 'marked', 'aksrt_msg_n' => $count, ])); } /* ------------------------------------------------------------------ * 区块渲染 * ----------------------------------------------------------------*/ private function render_message_notices(): void { $code = sanitize_key((string) wp_unslash($_GET['aksrt_msg'] ?? '')); if ($code === '') { return; } $n = absint($_GET['aksrt_msg_n'] ?? 0); $error = false; switch ($code) { case 'sent': $text = sprintf(__('已发送 %d 条消息。', 'aksrt'), $n); $failed = absint($_GET['aksrt_msg_f'] ?? 0); $unknown = absint($_GET['aksrt_msg_u'] ?? 0); if ($failed > 0) { $text .= ' ' . sprintf(__('投递失败 %d 条。', 'aksrt'), $failed); } if ($unknown > 0) { $text .= ' ' . sprintf(__('%d 个收件人未找到,已跳过。', 'aksrt'), $unknown); } break; case 'send_failed': $error = true; $text = isset($_GET['aksrt_msg_error']) ? sanitize_text_field((string) wp_unslash($_GET['aksrt_msg_error'])) : __('消息发送失败。', 'aksrt'); break; case 'deleted': $text = sprintf(__('已删除 %d 条消息。', 'aksrt'), $n); break; case 'marked': $text = sprintf(__('已将 %d 条消息标记为已读。', 'aksrt'), $n); break; case 'bulk_none': $error = true; $text = __('请先勾选消息并选择操作。', 'aksrt'); break; default: return; } printf( '

%s

', $error ? 'notice-error' : 'notice-success', esc_html($text) ); } private function render_message_overview(): void { $stats = NotificationAdminQuery::overview(); $cards = [ ['total', __('消息总数', 'aksrt'), 'dashicons-email-alt'], ['unread', __('未读', 'aksrt'), 'dashicons-marker'], ['today', __('今日新增', 'aksrt'), 'dashicons-calendar-alt'], ['recipients', __('涉及用户', 'aksrt'), 'dashicons-groups'], ]; echo ''; } private function render_message_compose(): void { $open = sanitize_key((string) wp_unslash($_GET['aksrt_msg'] ?? '')) === 'send_failed'; echo '
'; echo '' . esc_html__('发送系统消息', 'aksrt') . ''; echo '
'; echo ''; wp_nonce_field('aksrt_users_send_message'); echo '
' . esc_html__('收件人', 'aksrt') . ''; printf( '', esc_html__('指定用户', 'aksrt') ); printf( '', esc_html__('按角色', 'aksrt') ); echo '
'; echo '
'; printf( '', esc_html__('用户', 'aksrt'), esc_attr__('用户 ID / 用户名 / 邮箱,逗号或换行分隔', 'aksrt'), esc_html(sprintf(__('单次最多 %d 位。', 'aksrt'), AdminBroadcast::MAX_USERS)) ); echo ''; printf( '', esc_html__('标题', 'aksrt'), (int) AdminBroadcast::TITLE_MAX, esc_attr__('留空则取内容开头', 'aksrt') ); printf( '', esc_html__('跳转链接(可选)', 'aksrt') ); printf( '', esc_html__('内容', 'aksrt'), (int) AdminBroadcast::CONTENT_MAX ); echo '
'; printf( '

%s

', esc_html__('以「系统通知」类型逐人投递;邮件 / 公众号提醒按通知配置同步触发。', 'aksrt'), esc_html__('发送', 'aksrt') ); echo '
'; } /** @param array{all:int,unread:int,read:int} $counts */ private function render_message_views(array $filters, array $counts): void { $views = [ '' => [__('全部', 'aksrt'), $counts['all']], 'unread' => [__('未读', 'aksrt'), $counts['unread']], 'read' => [__('已读', 'aksrt'), $counts['read']], ]; $args = $this->message_filter_args($filters); unset($args['status']); echo ''; } private function render_message_filter_form(array $filters): void { echo '
'; echo ''; if ($filters['status'] !== '') { echo ''; } echo ''; echo ''; printf( '', esc_html__('按收件人筛选', 'aksrt'), esc_attr($filters['user']), esc_attr__('收件人 ID / 用户名 / 邮箱', 'aksrt') ); printf( '', esc_html__('搜索消息', 'aksrt'), esc_attr($filters['s']), esc_attr__('标题 / 内容', 'aksrt') ); submit_button(__('筛选', 'aksrt'), '', '', false); if ($filters['type'] !== '' || $filters['user'] !== '' || $filters['s'] !== '') { printf( '%s', esc_url($this->message_page_url($filters['status'] === '' ? [] : ['status' => $filters['status']])), esc_html__('清除筛选', 'aksrt') ); } echo '
'; } /** @param list> $items */ private function render_message_table(array $items, array $filters, int $page, bool $user_missing): void { $user_ids = array_values(array_unique(array_filter(array_map(static fn($row): int => (int) ($row['user_id'] ?? 0), $items)))); if ($user_ids !== []) { cache_users($user_ids); } echo '
'; echo ''; wp_nonce_field('aksrt_users_bulk_messages'); foreach ($this->message_filter_args($filters) as $key => $value) { printf('', esc_attr($key), esc_attr((string) $value)); } echo ''; echo '
'; printf( ' ', esc_html__('标记已读', 'aksrt') ); printf( '', esc_js(__('确定删除所选消息?删除后用户收件箱中也将不再显示。', 'aksrt')), esc_html__('删除', 'aksrt') ); echo '

'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; if ($items === []) { $hint = $user_missing ? __('没有找到该收件人。', 'aksrt') : ($this->message_filter_args($filters) === [] ? __('站内还没有任何通知。', 'aksrt') : __('当前筛选条件下没有消息。', 'aksrt')); printf( '', esc_html__('暂无消息', 'aksrt'), esc_html($hint) ); } foreach ($items as $row) { $this->render_message_row($row, $filters); } echo '
' . esc_html__('消息', 'aksrt') . '' . esc_html__('类型', 'aksrt') . '' . esc_html__('收件人', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('发送时间', 'aksrt') . '
%s%s
'; } /** @param array $row */ private function render_message_row(array $row, array $filters): void { $id = (int) ($row['id'] ?? 0); $type = (string) ($row['type'] ?? ''); $title = (string) ($row['title'] ?? ''); $desc = (string) ($row['description'] ?? ''); $url = (string) ($row['target_url'] ?? ''); $unread = empty($row['is_read']); echo ''; printf( '', $id, esc_html(sprintf(__('选择消息 #%d', 'aksrt'), $id)) ); echo '' . esc_html($title) . ''; if ($desc !== '' && $desc !== $title) { echo '
' . esc_html(mb_strimwidth($desc, 0, 160, '…')) . '
'; } echo '
#' . esc_html((string) $id) . ''; if ($url !== '') { printf( ' %s%s', esc_url($url), esc_html__('跳转链接', 'aksrt'), esc_html__('(在新窗口打开)', 'aksrt') ); } echo '
'; printf( '%s', esc_attr(MessageTypes::tone($type)), esc_url($this->message_page_url(array_merge($this->message_filter_args($filters), ['type' => $type]))), esc_html(MessageTypes::label($type)) ); echo '' . $this->message_user_cell((int) ($row['user_id'] ?? 0), $filters) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped in builder. $read_at = (string) ($row['read_at'] ?? ''); printf( '%s', $unread ? 'is-unread' : 'is-read', !$unread && $read_at !== '' ? ' title="' . esc_attr(sprintf(__('阅读于 %s', 'aksrt'), $this->message_time($read_at))) . '"' : '', $unread ? esc_html__('未读', 'aksrt') : esc_html__('已读', 'aksrt') ); $created = (string) ($row['created_at'] ?? ''); $ts = $created !== '' ? strtotime($created . ' UTC') : false; printf( '%s', esc_attr($ts ? gmdate('c', $ts) : ''), esc_html($this->message_time($created)), $ts ? '
' . esc_html(sprintf(__('%s前', 'aksrt'), human_time_diff($ts))) . '
' : '' ); echo ''; } private function message_user_cell(int $user_id, array $filters): string { $user = $user_id > 0 ? get_userdata($user_id) : false; if (!$user) { return '' . esc_html(sprintf(__('已删除用户 #%d', 'aksrt'), $user_id)) . ''; } $avatar = function_exists('aksrt_get_avatar_url') ? aksrt_get_avatar_url($user_id, 48) : get_avatar_url($user_id, ['size' => 48]); $filter_url = $this->message_page_url(array_merge($this->message_filter_args($filters), ['user' => (string) $user_id])); return '' . '' . '' . esc_html($user->display_name) . '' . '
#' . esc_html((string) $user_id) . ' · ' . esc_html__('编辑', 'aksrt') . '
'; } /* ------------------------------------------------------------------ * 参数与 URL * ----------------------------------------------------------------*/ /** @return array{status:string,type:string,user:string,s:string} */ private function message_filters(?array $source = null): array { $source = $source ?? $_GET; $status = sanitize_key((string) wp_unslash($source['status'] ?? '')); return [ 'status' => in_array($status, NotificationAdminQuery::STATUSES, true) ? $status : '', 'type' => sanitize_key((string) wp_unslash($source['type'] ?? '')), 'user' => trim(sanitize_text_field((string) wp_unslash($source['user'] ?? ''))), 's' => trim(sanitize_text_field((string) wp_unslash($source['s'] ?? ''))), ]; } /** 收件人筛选解析为 user_id:0=不限,-1=未找到(查询结果为空)。 */ private function message_query_filters(array $filters): array { $user_id = 0; $needle = $filters['user']; if ($needle !== '') { if (ctype_digit($needle)) { $user = get_userdata((int) $needle); } elseif (is_email($needle)) { $user = get_user_by('email', $needle); } else { $user = get_user_by('login', sanitize_user($needle)); } $user_id = $user instanceof \WP_User ? (int) $user->ID : -1; } return [ 'status' => $filters['status'], 'type' => $filters['type'], 'user_id' => $user_id, 's' => $filters['s'], ]; } /** @return array 非空筛选参数(用于 URL / 隐藏域) */ private function message_filter_args(array $filters): array { return array_filter($filters, static fn($value): bool => $value !== ''); } private function message_page_url(array $args = []): string { return add_query_arg(array_merge(['page' => self::MESSAGE_PAGE_SLUG], $args), admin_url('users.php')); } /** 通知表时间统一存 GMT,按站点时区展示。 */ private function message_time(string $gmt): string { return $gmt !== '' ? get_date_from_gmt($gmt, 'Y-m-d H:i') : ''; } private function message_fail(string $error): void { $this->message_redirect(['aksrt_msg' => 'send_failed', 'aksrt_msg_error' => $error]); } private function message_redirect(array $args): void { wp_safe_redirect($this->message_page_url($args)); exit; } }