fix(qa): 第五轮系统性质量保障缺陷修复 + 单元/集成测试

安全/鉴权:登出清服务端 httpOnly 令牌、登录 IP 限流前置 bcrypt、停用账号
令牌在 6 个内联 role handler 失效、admin/items 存在性 oracle 认证前置、
同意偏好 sanitize fail-closed、api-crypto 按 HTTP 方法判体。

CMS:workflow 状态更新+审计收进交互式 $transaction(失败即回滚)、编辑器
richtext 走 textarea 防换行损毁、array/object 子字段按作用域读写防污染、
about/contact/erp-upgrade 补 ISR revalidate 与路由映射。

UI/a11y:AnimatedCounter 入视口前显起点、CLS 累计上报、MetricCard 动画入
effect 并卸载取消 rAF、后台状态四态映射/搜索防抖+序号守卫/媒体单 input/
吞错改横幅/表单 label-aria 关联。

新增对应判别性单测与 pagination/rate-limit/client-ip/sanitize/validate-content-data
等模块及 tests-integration 真库集成层。
This commit is contained in:
2026-09-28 10:48:07 +08:00
parent 040951c0a3
commit a366bd1400
224 changed files with 8938 additions and 5579 deletions
+8 -3
View File
@@ -1,6 +1,6 @@
'use client';
import { useEffect, useState, useCallback } from 'react';
import { useEffect, useState, useCallback, useRef } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/components/admin/auth-context';
import { adminApi } from '@/lib/admin-api';
@@ -63,8 +63,11 @@ export default function NotificationsPage() {
const [unreadOnly, setUnreadOnly] = useState(false);
const [unreadCount, setUnreadCount] = useState(0);
const pageSize = 20;
// 单调递增的请求序号:只有最新一次请求的响应可以写进 state
const fetchSeqRef = useRef(0);
const fetchNotifications = useCallback(async () => {
const seq = ++fetchSeqRef.current;
setLoading(true);
setError('');
try {
@@ -80,12 +83,14 @@ export default function NotificationsPage() {
pageSize: number;
totalPages: number;
}>(`/api/admin/notifications?${query.toString()}`);
if (seq !== fetchSeqRef.current) return;
setNotifications(data.items || []);
setTotal(data.total || 0);
} catch (err) {
if (seq !== fetchSeqRef.current) return;
setError(err instanceof Error ? err.message : '加载失败');
} finally {
setLoading(false);
if (seq === fetchSeqRef.current) setLoading(false);
}
}, [page, unreadOnly]);
@@ -164,7 +169,7 @@ export default function NotificationsPage() {
</div>
<div className="flex items-center gap-2">
<button
onClick={() => { setUnreadOnly(!unreadOnly); setPage(1); }}
onClick={() => { setUnreadOnly((prev) => !prev); setPage(1); }}
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
unreadOnly
? 'bg-dark-bg text-white border-border-dark'