fix: harden notification polling and simplify pagination controls

This commit is contained in:
hectorzhao
2026-09-08 21:44:08 +08:00
parent ebb185b22b
commit 6816f56178
15 changed files with 897 additions and 141 deletions
+108 -96
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useState } from 'react';
import {
Activity,
AlertTriangle,
@@ -35,6 +35,7 @@ import { getLastUserActivityAt, readSession, type LoginSession } from '@/api/ses
import { AppShell } from '@/layouts/AppShell';
import { PortalSessionBoundary } from '@/layouts/PortalSessionBoundary';
import { request } from '@/api/core/httpClient';
import { settleNotificationRequest, useNotificationPolling } from './useNotificationPolling';
const EMPTY_PENDING_AUDITS: Omit<PendingAuditCounts, 'total'> = {
enterpriseCertifications: 0,
@@ -44,92 +45,95 @@ const EMPTY_PENDING_AUDITS: Omit<PendingAuditCounts, 'total'> = {
drainageInfos: 0,
};
type NotificationValue<T> = { value: T; status: 'loading' | 'ready' | 'unavailable'; hasValue: boolean };
function initialNotification<T>(value: T): NotificationValue<T> {
return { value, status: 'loading', hasValue: false };
}
function updateNotification<T>(
previous: NotificationValue<T>,
result: PromiseSettledResult<T>,
available = true,
): NotificationValue<T> {
return result.status === 'fulfilled' && available
? { value: result.value, status: 'ready', hasValue: true }
: { ...previous, status: 'unavailable' };
}
function notificationDescription<T>(notification: NotificationValue<T>, description: string) {
if (notification.status === 'loading') return '计数加载中';
if (notification.status === 'unavailable') {
return notification.hasValue ? '计数暂不可用,显示上次结果' : '计数暂不可用,尚未取得结果';
}
return description;
}
function canPollNotifications(userId: string) {
const session = readSession('admin');
return Boolean(
session &&
session.user.id === userId &&
!session.locked &&
Date.now() - getLastUserActivityAt() < session.idleTimeoutSeconds * 1000,
);
}
export function AdminLayout() {
return (
<PortalSessionBoundary portal="admin">
{(session) => <AdminAuthenticatedLayout session={session} />}
{(session) => <AdminAuthenticatedLayout key={session.user.id} session={session} />}
</PortalSessionBoundary>
);
}
function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
const [pendingAudits, setPendingAudits] = useState(EMPTY_PENDING_AUDITS);
const [retirementUnreadCount, setRetirementUnreadCount] = useState(0);
const [reportSummary, setReportSummary] = useState({ count: 0, unavailable: false });
const [monitorSummary, setMonitorSummary] = useState({ count: 0, unavailable: false });
const [securityAlertSummary, setSecurityAlertSummary] = useState({ count: 0, criticalCount: 0 });
const [infrastructureAlertSummary, setInfrastructureAlertSummary] = useState({ count: 0, criticalCount: 0 });
const [pendingAudits, setPendingAudits] = useState(() => initialNotification(EMPTY_PENDING_AUDITS));
const [retirementSummary, setRetirementSummary] = useState(() => initialNotification({ count: 0 }));
const [reportSummary, setReportSummary] = useState(() => initialNotification({ count: 0 }));
const [monitorSummary, setMonitorSummary] = useState(() => initialNotification({ count: 0 }));
const [securityAlertSummary, setSecurityAlertSummary] = useState(() =>
initialNotification({ count: 0, criticalCount: 0 }),
);
const [infrastructureAlertSummary, setInfrastructureAlertSummary] = useState(() =>
initialNotification({ count: 0, criticalCount: 0 }),
);
const [sessionLocked, setSessionLocked] = useState(Boolean(session.locked));
const loadPendingAuditCount = useCallback(() => {
const currentSession = readSession('admin');
if (
!currentSession ||
currentSession.locked ||
Date.now() - getLastUserActivityAt() >= currentSession.idleTimeoutSeconds * 1000
) {
return;
}
const canPoll = useCallback(() => canPollNotifications(session.user.id), [session.user.id]);
const loadPendingAuditCount = useCallback(async (signal: AbortSignal, isCurrent: () => boolean) => {
// This runs globally and on a timer, so it must not fan out through the full dashboard aggregation.
Promise.allSettled([
adminApi.getPendingAudits(),
adminApi.getSignatureRetirementUnreadCount(),
adminApi.getSecurityNotificationSummary(),
adminApi.getInfrastructureMonitoringNotificationSummary(),
request<{ count: number }>('/admin/report-notifications/summary'),
request<{ count: number; unavailable?: boolean }>('/admin/sending-monitor/notification-summary'),
])
.then(([audits, retirement, security, infrastructure, reporting, monitor]) => {
setMonitorSummary((previous) =>
monitor.status === 'fulfilled'
? { count: monitor.value.count, unavailable: Boolean(monitor.value.unavailable) }
: { ...previous, unavailable: true },
);
setReportSummary((previous) =>
reporting.status === 'fulfilled'
? { count: reporting.value.count, unavailable: false }
: { ...previous, unavailable: true },
);
setPendingAudits(audits.status === 'fulfilled' ? audits.value : EMPTY_PENDING_AUDITS);
setRetirementUnreadCount(retirement.status === 'fulfilled' ? retirement.value.count : 0);
setSecurityAlertSummary(security.status === 'fulfilled' ? security.value : { count: 0, criticalCount: 0 });
setInfrastructureAlertSummary(
infrastructure.status === 'fulfilled' ? infrastructure.value : { count: 0, criticalCount: 0 },
);
})
.catch(() => {
setPendingAudits(EMPTY_PENDING_AUDITS);
setRetirementUnreadCount(0);
setSecurityAlertSummary({ count: 0, criticalCount: 0 });
setInfrastructureAlertSummary({ count: 0, criticalCount: 0 });
});
const results = await Promise.allSettled([
settleNotificationRequest(adminApi.getPendingAudits(undefined, signal), signal),
settleNotificationRequest(adminApi.getSignatureRetirementUnreadCount(signal), signal),
settleNotificationRequest(adminApi.getSecurityNotificationSummary(signal), signal),
settleNotificationRequest(adminApi.getInfrastructureMonitoringNotificationSummary(signal), signal),
settleNotificationRequest(request<{ count: number }>('/admin/report-notifications/summary', { signal }), signal),
settleNotificationRequest(
request<{ count: number; unavailable?: boolean }>('/admin/sending-monitor/notification-summary', { signal }),
signal,
),
]);
if (!isCurrent()) return false;
const [audits, retirement, security, infrastructure, reporting, monitor] = results;
const monitorAvailable = monitor.status === 'fulfilled' && !monitor.value.unavailable;
setPendingAudits((previous) => updateNotification(previous, audits));
setRetirementSummary((previous) => updateNotification(previous, retirement));
setSecurityAlertSummary((previous) => updateNotification(previous, security));
setInfrastructureAlertSummary((previous) => updateNotification(previous, infrastructure));
setReportSummary((previous) => updateNotification(previous, reporting));
setMonitorSummary((previous) => updateNotification(previous, monitor, monitorAvailable));
return results.every((result) => result.status === 'fulfilled') && monitorAvailable;
}, []);
useEffect(() => {
if (session.portal !== 'admin' || sessionLocked) {
return;
}
loadPendingAuditCount();
const timer = window.setInterval(loadPendingAuditCount, 30000);
const onFocus = () => loadPendingAuditCount();
const onAuditRefresh = () => loadPendingAuditCount();
window.addEventListener('focus', onFocus);
window.addEventListener('cmpp-audit-count-refresh', onAuditRefresh);
window.addEventListener('cmpp-retirement-count-refresh', onAuditRefresh);
window.addEventListener('cmpp-report-notification-refresh', onAuditRefresh);
window.addEventListener('cmpp-monitor-alert-refresh', onAuditRefresh);
window.addEventListener('cmpp-security-alert-count-refresh', onAuditRefresh);
window.addEventListener('cmpp-infrastructure-alert-count-refresh', onAuditRefresh);
return () => {
window.clearInterval(timer);
window.removeEventListener('focus', onFocus);
window.removeEventListener('cmpp-audit-count-refresh', onAuditRefresh);
window.removeEventListener('cmpp-retirement-count-refresh', onAuditRefresh);
window.removeEventListener('cmpp-report-notification-refresh', onAuditRefresh);
window.removeEventListener('cmpp-monitor-alert-refresh', onAuditRefresh);
window.removeEventListener('cmpp-security-alert-count-refresh', onAuditRefresh);
window.removeEventListener('cmpp-infrastructure-alert-count-refresh', onAuditRefresh);
};
}, [loadPendingAuditCount, session.portal, sessionLocked]);
useNotificationPolling({
enabled: session.portal === 'admin' && !sessionLocked,
canPoll,
poll: loadPendingAuditCount,
});
const auditLabel = (label: string) =>
pendingAudits.status === 'ready'
? label
: `${label}${pendingAudits.status === 'loading' ? '计数加载中' : '计数暂不可用'}`;
return (
<AppShell
@@ -144,14 +148,14 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
reportingNotifications={[
{
label: '报备状态变化通知',
count: reportSummary.count,
description: reportSummary.unavailable ? '消息计数暂不可用,请重试' : '按企业与小时汇总的未读消息',
count: reportSummary.value.count,
description: notificationDescription(reportSummary, '按企业与小时汇总的未读消息'),
to: '/admin/report-records?tab=readiness',
},
{
label: '签名清退预警',
count: retirementUnreadCount,
description: '今日未读且未抑制',
count: retirementSummary.value.count,
description: notificationDescription(retirementSummary, '今日未读且未抑制'),
to: '/admin/signature-retirement',
},
{
@@ -163,40 +167,48 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
alertNotifications={[
{
label: '发送质量告警',
count: monitorSummary.count,
description: monitorSummary.unavailable ? '告警计数暂不可用,请重试' : '未读活动发送质量告警',
count: monitorSummary.value.count,
description: notificationDescription(monitorSummary, '未读活动发送质量告警'),
to: '/admin/monitor?tab=alerts',
},
{
label: '安全检测与封禁',
count: securityAlertSummary.count,
description:
securityAlertSummary.criticalCount > 0
? `${securityAlertSummary.criticalCount} 条严重告警待处置`
count: securityAlertSummary.value.count,
description: notificationDescription(
securityAlertSummary,
securityAlertSummary.value.criticalCount > 0
? `${securityAlertSummary.value.criticalCount} 条严重告警待处置`
: '待处置安全告警',
),
to: '/admin/security-detection',
},
{
label: '系统监控告警',
count: infrastructureAlertSummary.count,
description:
infrastructureAlertSummary.criticalCount > 0
? `${infrastructureAlertSummary.criticalCount} 条 Prometheus 严重告警`
count: infrastructureAlertSummary.value.count,
description: notificationDescription(
infrastructureAlertSummary,
infrastructureAlertSummary.value.criticalCount > 0
? `${infrastructureAlertSummary.value.criticalCount} 条 Prometheus 严重告警`
: 'Prometheus 活动告警',
),
to: '/admin/system-monitoring#active-alerts',
},
]}
auditNotifications={[
{ label: '企业认证待审', count: pendingAudits.enterpriseCertifications, to: '/admin/enterprise-audit' },
{ label: '短信审核待审', count: pendingAudits.smsAudits, to: '/admin/sms-audit' },
{ label: '模板待审', count: pendingAudits.templates, to: '/admin/templates' },
{ label: '签名待审', count: pendingAudits.signatures, to: '/admin/signatures' },
{
label: '签名导入待审',
count: pendingAudits.signatureImports ?? 0,
label: auditLabel('企业认证待审'),
count: pendingAudits.value.enterpriseCertifications,
to: '/admin/enterprise-audit',
},
{ label: auditLabel('短信审核待审'), count: pendingAudits.value.smsAudits, to: '/admin/sms-audit' },
{ label: auditLabel('模板待审'), count: pendingAudits.value.templates, to: '/admin/templates' },
{ label: auditLabel('签名待审'), count: pendingAudits.value.signatures, to: '/admin/signatures' },
{
label: auditLabel('签名导入待审'),
count: pendingAudits.value.signatureImports ?? 0,
to: '/admin/signatures?tab=import',
},
{ label: '引流信息待审', count: pendingAudits.drainageInfos, to: '/admin/drainage-audits' },
{ label: auditLabel('引流信息待审'), count: pendingAudits.value.drainageInfos, to: '/admin/drainage-audits' },
]}
navSections={[
{