feat: 完善服务监控与下游重投

This commit is contained in:
hectorzhao
2026-08-14 17:21:29 +08:00
parent d30d9ea4d0
commit 1ef4380422
50 changed files with 1279 additions and 82 deletions
+11 -3
View File
@@ -48,6 +48,7 @@ export function AdminLayout() {
function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
const [pendingAudits, setPendingAudits] = useState(EMPTY_PENDING_AUDITS);
const [retirementUnreadCount, setRetirementUnreadCount] = useState(0);
const [securityAlertSummary, setSecurityAlertSummary] = useState({ count: 0, criticalCount: 0 });
const [sessionLocked, setSessionLocked] = useState(Boolean(session.locked));
const loadPendingAuditCount = useCallback(() => {
const currentSession = readSession('admin');
@@ -56,14 +57,16 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
return;
}
// This runs globally and on a timer, so it must not fan out through the full dashboard aggregation.
Promise.allSettled([adminApi.getPendingAudits(), adminApi.getSignatureRetirementUnreadCount()])
.then(([audits, retirement]) => {
Promise.allSettled([adminApi.getPendingAudits(), adminApi.getSignatureRetirementUnreadCount(), adminApi.getSecurityNotificationSummary()])
.then(([audits, retirement, security]) => {
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 });
})
.catch(() => {
setPendingAudits(EMPTY_PENDING_AUDITS);
setRetirementUnreadCount(0);
setSecurityAlertSummary({ count: 0, criticalCount: 0 });
});
}, []);
@@ -78,11 +81,13 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
window.addEventListener('focus', onFocus);
window.addEventListener('cmpp-audit-count-refresh', onAuditRefresh);
window.addEventListener('cmpp-retirement-count-refresh', onAuditRefresh);
window.addEventListener('cmpp-security-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-security-alert-count-refresh', onAuditRefresh);
};
}, [loadPendingAuditCount, session.portal, sessionLocked]);
@@ -96,7 +101,10 @@ function AdminAuthenticatedLayout({ session }: { session: LoginSession }) {
userName={session.user.displayName}
userRole="平台管理员"
onSessionLockedChange={setSessionLocked}
retirementAlert={{ count: retirementUnreadCount, to: '/admin/signature-retirement' }}
alertNotifications={[
{ label: '签名清退预警', count: retirementUnreadCount, description: '今日未读且未抑制', to: '/admin/signature-retirement' },
{ label: '安全检测与封禁', count: securityAlertSummary.count, description: securityAlertSummary.criticalCount > 0 ? `${securityAlertSummary.criticalCount} 条严重告警待处置` : '待处置安全告警', to: '/admin/security-detection' },
]}
auditNotifications={[
{ label: '企业认证待审', count: pendingAudits.enterpriseCertifications, to: '/admin/enterprise-audit' },
{ label: '短信审核待审', count: pendingAudits.smsAudits, to: '/admin/sms-audit' },
+40 -13
View File
@@ -50,6 +50,10 @@ export type AuditNotificationItem = {
to: string;
};
export type AlertNotificationItem = AuditNotificationItem & {
description: string;
};
type AppShellProps = {
title: string;
subtitle: string;
@@ -60,7 +64,7 @@ type AppShellProps = {
userRole: string;
navSections: ShellNavSection[];
auditNotifications?: AuditNotificationItem[];
retirementAlert?: { count: number; to: string };
alertNotifications?: AlertNotificationItem[];
onSessionLockedChange?: (locked: boolean) => void;
};
@@ -73,7 +77,7 @@ export function AppShell({
userRole,
navSections,
auditNotifications = [],
retirementAlert,
alertNotifications = [],
onSessionLockedChange,
}: AppShellProps) {
const [collapsed, setCollapsed] = useState(false);
@@ -81,6 +85,7 @@ export function AppShell({
const [closedSections, setClosedSections] = useState<Record<string, boolean>>({});
const [userMenuOpen, setUserMenuOpen] = useState(false);
const [noticeOpen, setNoticeOpen] = useState(false);
const [alertNoticeOpen, setAlertNoticeOpen] = useState(false);
const [passwordModalOpen, setPasswordModalOpen] = useState(false);
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
@@ -107,6 +112,10 @@ export function AppShell({
() => auditNotifications.reduce((sum, item) => sum + item.count, 0),
[auditNotifications],
);
const alertTotal = useMemo(
() => alertNotifications.reduce((sum, item) => sum + item.count, 0),
[alertNotifications],
);
async function changeOwnPassword() {
if (!currentPassword || newPassword.length < 6) {
@@ -417,23 +426,41 @@ export function AppShell({
<button className="icon-button topbar-help" type="button" aria-label="帮助中心">
<CircleHelp size={18} />
</button>
{retirementAlert ? (
<Link
aria-label={`今日未读且未抑制签名清退预警 ${retirementAlert.count}`}
className={['icon-button', retirementAlert.count > 0 ? 'has-dot' : ''].filter(Boolean).join(' ')}
title="今日未读且未抑制签名清退预警"
to={retirementAlert.to}
>
<Bell size={18} />
{retirementAlert.count > 0 ? <span className="notice-count">{retirementAlert.count}</span> : null}
</Link>
{alertNotifications.length ? (
<div className="notice-menu-wrap">
<button
aria-expanded={alertNoticeOpen}
aria-haspopup="menu"
aria-label="预警通知"
className={['icon-button', alertTotal > 0 ? 'has-dot' : ''].filter(Boolean).join(' ')}
onClick={() => { setAlertNoticeOpen((open) => !open); setNoticeOpen(false); }}
type="button"
>
<Bell size={18} />
{alertTotal > 0 ? <span className="notice-count">{alertTotal}</span> : null}
</button>
{alertNoticeOpen ? (
<div className="notice-popover notice-popover--alerts" role="menu">
<div className="notice-popover__header">
<strong></strong>
<span className={alertTotal === 0 ? 'is-zero' : ''}>{alertTotal} </span>
</div>
{alertNotifications.map((item) => (
<NavLink key={item.to} onClick={() => setAlertNoticeOpen(false)} role="menuitem" to={item.to}>
<span className="notice-popover__copy"><b>{item.label}</b><small>{item.description}</small></span>
<strong className={item.count === 0 ? 'is-zero' : ''}>{item.count}</strong>
</NavLink>
))}
</div>
) : null}
</div>
) : null}
<div className="notice-menu-wrap">
<button
aria-expanded={noticeOpen}
aria-haspopup="menu"
className={['icon-button', auditTotal > 0 ? 'has-dot' : ''].filter(Boolean).join(' ')}
onClick={() => setNoticeOpen((open) => !open)}
onClick={() => { setNoticeOpen((open) => !open); setAlertNoticeOpen(false); }}
type="button"
aria-label="通知"
>