# 系统性 Code Review / 全面测试 / 验收报告 — 2026-09-21 - 分支:`refactor/optimize-ui`(领先 `origin/main` 30 个提交;`src/` 改动 120 文件,+3248 / −3959) - 环境:Next.js 16.3.0 (Turbopack) / React 18.3 / TypeScript 5 strict / Prisma 6.19 + SQLite / Tailwind 3.4 - 方法:4 路并行模块审查(API+鉴权 / lib+hooks / 组件+页面 / 数据+配置+测试基建),所有结论由主代理逐条复核证据后定级;无法证实的假设已撤回(见 §7) - 实测门禁:type-check / lint / 单测+覆盖率 / build / **E2E 三浏览器全量** / check:contrast / check:headings / 安全响应头 / **Lighthouse 7 URL** / **变异测试(quick 作用域)** —— 全部由本次亲跑取回原始输出,未引用任何历史报告(详见 §2) ## 1. 验收结论 **不通过验收(REJECTED)。** 阻断项 5 类:①生产级安全漏洞(权限提升 ×2、存储型 XSS ×2);②E2E 92 例失败(3 浏览器一致复现,含 6 例 `@critical`);③CI 用 `|| echo` 吞掉全部功能性测试,绿色徽章不证明任何行为正确性;④本分支引入 2 处用户可见 UI 回归;⑤**可访问性门禁双重失效**(A-11)导致一个**全站 62 页**的 WCAG AA 对比度失败(A-12 Cookie 条隐私政策链接 3.31:1)在 `check:contrast` 与 Lighthouse 两道门禁下同时报绿。 > 关键判据不是「失败数」而是「门禁是否可信」。当前 `test:all` 与 Jenkins 绿灯所覆盖的范围,与 AGENTS.md §5 声称的质量门禁之间存在实质落差;本次实测中**两道 a11y 门禁双双通过,却被两个独立引擎各测出 critical 级失败**,即为该落差的最强证据。 ## 2. 门禁执行结果(本次实测,非引用) | 门禁 | 命令 | 结果 | 判定 | |---|---|---|---| | 类型检查 | `npm run type-check` | 0 error,EXIT=0 | 通过 | | Lint | `npm run lint` | 0 error / **128 warning** | **不达 AGENTS.md「无警告」** | | 单元测试 | `npm run test:unit` | **132 套件 / 1594 例全通过**,23.9s | 通过 | | 覆盖率 | `npm run test:coverage` | stmts 75.9 / branch 84.28 / func 75.46 / line 75.9 | 通过(但门禁形同虚设,见 §3.3) | | 生产构建 | `npm run build` | EXIT=0,62 静态页生成 | 通过 | | E2E 三浏览器 | `playwright test --project=chromium,firefox,webkit` | **710 通过 / 92 失败 / 8 跳过**,17.2min,EXIT=1 | **失败** | | 色彩对比度 | `npm run check:contrast` | 7/7 通过 | 通过(**但仅 7 组硬编码浅色对,盲区见 §3.4**) | | 标题层级 | `npm run check:headings` | 10/10 页 0 问题 | 通过 | | 安全响应头 | `npm run test:security:headers` | 6 通过 / 2 警告 | 通过(**但打的是线上 novalon.cn,不验证本分支**) | | Lighthouse | `npx lighthouse@13`(对 `lighthouserc.json` 全部 7 个 URL,desktop preset) | **性能全绿**(perf 99-100 / FCP 246-250ms / LCP 790-896ms / CLS **0.000** / TBT 0ms / SI 248-414ms);**a11y 92-97、7 页全部含 contrast 失败节点,共 10 节点** | **断言 0 违规 → 门禁"通过",但门禁太松看不见真实 a11y 缺陷,见 §3.4** | | 变异测试 | `npx stryker run --inPlace --mutate 'src/lib/utils.ts'`(`test:mutation:quick`) | **91.18%**(31 killed / 3 survived / 0 no-cov / 0 error),≥ `break:50`;**全量作用域未跑**(外推需数小时) | 通过(阈值达标),但暴露 2 处真实断言缺陷 + 作用域排除「零编造」核心,见 §3.5 | | 压测 | `npm run test:performance` | **不可运行**:`k6` 是 v0.0.0 占位包(`node_modules/k6/package.json`:"Dummy package for autocompleting k6 scripts"),无 `node_modules/.bin/k6` | **失效** | Lint 128 warning 构成:`no-explicit-any` 49 · `react-hooks/set-state-in-effect` 12 · `next/no-img-element` 10 · 其余 57。 ## 3. 阻断级问题(P0) ### 3.1 权限提升与账号接管(已逐行复核) **A-1 `content_admin` → `super_admin` 自主提权** — `src/app/api/admin/users/route.ts:90`(POST)/ `:166`(PUT)放行 `content_admin`;`:135` `const rolesToAssign = body.roleCodes?.length ? body.roleCodes : ['readonly']` 直接取请求体,`:139`/`:214` 仅校验「角色是否存在于 DB」,不校验「调用者是否有权授予」。`:201-208` 只阻止移除**自己**的 super_admin,授予方向无任何 allowlist。 攻击:`POST /api/admin/users {username,password,roleCodes:["super_admin"]}`。 修复:两个 handler 均将 `body.roleCodes` 与调用者角色集求交,非 `super_admin` 不得授予 `super_admin`。 **A-2 `content_admin` 可重置任意用户密码 → 接管超管** — `users/route.ts:189-191` `if (body.password) updateData.password = await hashPassword(body.password)` → `:193` `prisma.user.update({ where: { id: userId } })`,`userId` 来自 `:172` 攻击者可控的 query 参数,未校验目标是否持有 `super_admin`。同理 `:188` 可将超管 `status` 置 0 致其失联。 攻击:`PUT /api/admin/users?id= {password:"x"}` 后登录。 修复:改密/停用他人须 `super_admin`;目标持有 `super_admin` 时同样要求调用者为 `super_admin`。 架构成因:`src/proxy.ts:104` `matcher: ['/admin/:path*']` 且 `:81` 显式 `!pathname.startsWith('/api/')` —— **proxy 完全不覆盖 `/api/*`**,每个 route 必须自守卫。(Next 16 已将 `middleware.ts` 更名 `proxy.ts`,此处用法正确。) ### 3.2 存储型 XSS ×2(已验证数据通路) **A-3 CMS 富文本未净化直出公开页** — `src/app/terms/page.tsx:212` 与 `src/app/privacy/page.tsx:267`:`dangerouslySetInnerHTML={{ __html: cmsContent }}`,`cmsContent` 来自 `getPublishedItems('legal-page')` → `item.data.content`(`terms/page.tsx:181`)。字段类型 `richtext`,描述即「支持 HTML 标签」(`content-types.ts:1563`)。**全仓无净化器**(`DOMPurify` / `sanitize-html` 检索 0 命中);`RichTextEditor.tsx:38-46` 的 TipTap `Link` 未配置 `protocols`。 影响:任何可编辑 legal-page 的账号(或经 A-1 提权者)即可在 `/terms`、`/privacy` 对**全部访客**注入脚本。 修复:渲染端统一 `sanitize-html`(白名单标签/属性/协议),并在 `uploadMedia`/items 写入侧再校验一次。 **A-4 上传文件同源分发且安全头丢失** — `src/app/api/admin/media/route.ts:79-84` 将客户端可控的 `file.type`/`file.name` 原样传入;`src/lib/media/media-service.ts:36-87` `uploadMedia` **无扩展名/MIME 白名单**(`:44` `isImage()` 只门控缩略图,不门控落盘),`generateUniqueFileName` 保留原扩展名 → `evil.html` 存入 `public/uploads/`。`nginx-static-production.conf:163-168` 的 `location /uploads/` 自带 `add_header`,按 nginx 语义**不再继承** server 级 `X-Content-Type-Options: nosniff`(`:42`)与 CSP(`:128`)→ 以 `text/html` 同源渲染。叠加 CSP 含 `script-src 'unsafe-inline' 'unsafe-eval'`(`next.config.mjs:40`),失去兜底。 修复:`uploadMedia` 增白名单 + magic-byte 校验;`/uploads/` 内重新 `add_header nosniff/CSP` 并对非图片强制 `Content-Disposition: attachment`;移除 `unsafe-eval`。 ### 3.3 测试基建不可信(最高价值发现) **A-5 CI 功能性测试全部被 `|| echo` 吞掉** — `Jenkinsfile:179` `playwright test --grep "@smoke|@critical" || echo "⚠️ …继续执行"`;`:181` journey、`:214` 视觉回归、`:236` `npm audit`、`:238` 安全头 同法吞没。真门禁仅 lint(:129)/type-check(:136)/coverage(:148)/build(:174,254)。**AGENTS.md §5 列为必须的 lighthouse、`check:contrast`、`check:headings`、mutation、k6、integration 在流水线中完全缺席。** 结论:「CI 绿」当前只证明 lint + tsc + 「覆盖率≥30%」+ build 成功,不证明任何用户行为。 修复:去掉功能性 stage 的 `|| echo`,把缺失门禁纳入流水线。 **A-6 `@critical` GA4 测试是自证式空测** — `e2e/ga4-event-tracking.spec.ts:47,75,165,235`(4 例,标签 `@critical`):`beforeEach:24-31` 注入 mock `window.gtag`,测试体 `:56-62` **自行调用** `gtag('config','G-TEST123',…)`,再于 `:65-70` 断言 `__gtagCalls` 中存在该调用。TC-GA4-003 `:184-205` 甚至不点击按钮,只判可见性后自调用;且 `if (isCtaVisible) {} else {}` 两分支均自调用 → **CTA 不存在也通过**。断言的 `G-TEST123` 由测试自己提供,与应用无关。 影响:`npm run test:critical` 对分析埋点的绿灯为 0 信息量。 修复:改为拦截真实网络请求(`gtag/js` 的 `page_path`/事件参数),或让 `trackEvent` 走可注入 sink 并断言应用调用。 **A-7 覆盖率门禁形同虚设 + 三处文档口径互斥** — 实际生效门禁用 `jest.config.js` global `branches 30 / functions 25 / lines 32 / statements 30`,而实测为 `84.28 / 75.46 / 75.9 / 75.9`,**低于实测约 45 个百分点**;目录级阈值多数不可约束:`seo` branches 门限 0(实测 100)、`content` branches 门限 4(实测 100)、`ui` functions 门限 10(实测 81)。文档互斥:`CLAUDE.md:39,196` 称阈值 80% 且路径写作 `config/test/jest.config.js`(**路径错误**,真实配置在仓库根;`config/test/jest.config.js` 仅 Stryker 使用);`docs/development/quality-gates.md:82-85` 称四项均 ≥70%。 修复:阈值上调至「实测 −5pp」的棘轮值;统一三处文档并修正配置路径。 **A-8 单测不接触真实数据层** — `jest.setup.js:12-27` 全局 mock `PrismaClient`(`findMany → []`),`:29-50` 整体 mock `@/lib/cms/data-server`。因此 §4 所有数据层缺陷(无事务、TOCTOU、JSON.parse 崩溃)**没有任何测试能发现**。`npm run test:integration`(`--testPathPatterns='src/app/api/'`)跑的仍是 mock 版 `route.test.ts`(如 `items/route.test.ts:14` mock `@/lib/db`),命名误导。 另有空洞断言:`src/lib/db.test.ts:6-9` `expect(prisma).toBeDefined()` 对全局 mock 永真;`colors.test.ts`(≈18)、`constants.test.ts`(≈24)、`design-system.test.ts`(21) 大量 `toBeDefined()` 静态常量。 **A-9 视觉基线已「追认现状」,且 9 张近空白** — 提交 `f543e47` 自述「87 例失败…均为尺寸级不匹配」后重生成 86/115 基线 ⇒ 重生时点已存在的回归**被固化为参照**,该套件此后无法再发现它。基线总数 107(5 project × 21,齐全),但 `visual-{chromium,firefox,webkit}-desktop/…/button-{default,hover,focus}-*.png` 为 **912 / 1111 / 1961–2030 字节**,日期 Jul 26 与 Jul 6(**在 9 月重生成之外**)⇒ 近空白区域仍算「比对通过」。容差偏松:`playwright.config.ts:38-41` `maxDiffPixels:200`、`maxDiffPixelRatio:0.005`、`threshold:0.3`。部分断言条件化(`visual-regression.spec.ts:73,91,101,111,130,182` 元素缺失即 0 断言通过)或为永真(`:199` `color||fontFamily`、`:217` `brand||ink||bg` `toBeTruthy()`)。 **A-10 E2E 从不验证交付物** — `playwright.config.ts:130` `command: 'npm run dev'` + `:132 reuseExistingServer: true`,全部规格跑在 **dev server**(甚至可能是上一轮残留进程)而非 `output:'standalone'` 构建产物。构建期才暴露的问题(预渲染、ISR、production header)永不被测。 修复:新增 `--project=production` 指向 `npm run start` 的 webServer。 ### 3.4 可访问性门禁的双重失效与已证实的全站缺陷 **A-11 两个对比度门禁同时看不见同一个真实违规** — 两条独立通路各自漏检: 1. **`check:contrast` 静态漏检** — `scripts/utils/check-color-contrast.ts:10-18` 的 `criticalColorPairs` 是 **7 组硬编码十六进制**(全部 `#FFFFFF` 底),既不读 `tailwind.config.js`/`globals.css`,也**不含任何暗色模式配对**,更不覆盖 alpha 修饰类。而本分支主题提交(`4b8500a`、`846585a`)恰好把暗色模式与 `--color-brand` 双通道拆分作为主战场 —— 门禁与其要保护的对象完全脱节,令牌一旦改动它仍对着陈旧色值报绿。 2. **Lighthouse 阈值漏检** — `lighthouserc.json` 对 accessibility 断言 **≥0.9**,而含 axe critical 失败的实际得分是 **92-97**:`products` 92 分(7 个 `aria-required-parent` critical 节点)、其余 6 页 96-97 分(每页 1-4 个 `color-contrast` 节点)。**critical 级 WCAG 失败被折算成分数后落在门禁线之上**,故 `npm run lighthouse` 会显示全绿。 **A-12 Cookie 同意条的「隐私政策」链接在全部 62 页对比度不达标(已双引擎证实)** — `src/components/analytics/CookieConsent.tsx:152` `text-[var(--color-brand)]`(#C41E3A)落在同文件 `:141` 的 `bg-[var(--color-bg-primary)]`(暗色 #0A0E14)上: - Lighthouse/axe 实测 **3.3:1**(15.75px normal,要求 4.5:1);我按 `globals.css:23/416` 令牌值独立算得 **3.31:1**,两法吻合。 - 该组件挂在**根布局** `src/app/layout.tsx:223` ⇒ 7/7 被测页各命中 1 次,即**全站每一页**都失败(10 个失败节点中的 7 个来自此处,余 3 个见 §4.3)。 - 根因是**违反已写明的设计契约**:DESIGN.md:154「**The Two-Channel Red Rule.** 底色用 `--color-brand`(暗黑不翻),文字用 `--color-brand-ink`(暗黑翻至 #F87171)。合并成一条是 bug 的源头」、DESIGN.md:232「用 `text-brand-ink` 写红色文字…**永不混用**」。此处的合规写法应为 `text-brand-ink`。 - 讽刺点:这是**隐私/Cookie 同意 UI**里指向隐私政策的链接,属合规可见路径。 **同类面**:全仓 **67 处** `text-[var(--color-brand)]`(23 个文件,含 `CookieConsent`、`error.tsx`、`not-found-content.tsx`、`cta-section`、`hero-section-v2`、`product-card`、`service-card` 等)对比 233 处合规的 `text-brand-ink`。这些站点在浅色面(#FFFFFF 上 **5.84:1**)偶然达标,一旦位于暗色面即跌到 **3.31:1**(`--color-brand-bg` #2A1418 上更仅 **2.97:1**)—— 而本分支暗色为默认。**修复应整族收敛而非逐点打补丁**:以 `text-brand-ink` 替换全部 67 处,并加一条 grep 门禁禁止 `text-[var(--color-brand)]`。 > 本项由 Playwright+axe(移动)与 Lighthouse+axe(桌面)**两个独立引擎**分别复现,非源码推断 —— 这是本次验收中证据强度最高的一类结论。 ### 3.5 变异测试(本次实跑,唯一真正量化「测试有没有断言」的门禁) `npm run test:mutation:quick`(作用域 `src/lib/utils.ts`):**91.18%**(31 killed / 3 survived / 0 no-coverage / 0 error,均值 14.97 tests/mutant,74s)≥ `stryker.config.json` 的 `break: 50` ⇒ 阈值达标。但 3 个存活变异体经逐个复核后,**2 个是本项目的真实测试缺陷**: **A-13 `lerp` 的全部测试用例都以 `start = 0` 输入 ⇒ `start` 偏移量从未被检验** — `src/lib/utils.ts:49` `start + (end - start) * t` 被改为 `start + (end + start) * t` 后仍全绿。原因(`src/lib/utils.test.ts:120-128` 四例逐一验算):`lerp(0,10,0.5)`、`lerp(0,100,0.25)`、`lerp(0,10,0)`、`lerp(0,10,1)` —— **`start` 恒为 0**,而 `end - 0` 与 `end + 0` 数值相同,故该变异在数学上不可观测。这是**测试数据选择缺陷**:函数唯一独有的参数(`start`)恰好是唯一没被非零值覆盖的那个。 **A-14 `randomBetween` 只断言边界,检不出算子错误** — `utils.ts:45` `Math.random() * (max - min) + min` 改为 `/` 后仍全绿。`test.ts:106-115` 只做 `toBeGreaterThanOrEqual(1)` / `toBeLessThanOrEqual(10)`;变异实现给出 `rand/9 + 1 ∈ [1, 1.89]`,负数例给出 `∈ [-1, -0.89]` —— 均落在断言区间内。分布被压到区间一端 11% 的长度而测试无法察觉,因为**没有任何一例检验取值是否覆盖全区或分布是否均匀**。 **(反向校准)第 3 个存活体不是缺陷** — `utils.ts:25` 的 `if (timeout)` → `if (true)`:`clearTimeout(null)` 在 Node/浏览器均为合法 no-op,故该变异体与原实现**语义等价**,存活属正常,不计入测试质量问题。列出以示本次定级未把噪声当发现。 **A-15 变异门禁的作用域把「零编造」核心排除在外** — `stryker.config.json:20` 的 `mutate` 含排除项 `"!src/lib/constants/**"`,而 `src/lib/constants/metrics-basis.ts`(`resolveMetricBasis` / `weakestBasis` / `FORBIDDEN_PROOF_PHRASES`,即项目 AGENTS.md §3「零编造」原则的唯一机械载体)**正在该目录内**。后果:那个「未声明口径必须保守回落到 `target`」的回落逻辑,若被改坏(例如回落到 `verified`)**不会有任何变异测试发现** —— 与 B-3「`basis` 仅类型约定、写入侧无校验」构成同一处治理缺口的两半。 **门禁自身的两点风险(实测记录)**: 1. `npm run test:mutation` 与 `:quick` 均带 `--inPlace`,Stryker 会**直接改写工作树**(其日志自述 "In place mode is enabled, Stryker will be overriding YOUR files")。本次运行期间 `git status` 一度显示 10+ 个文件为 ` M`(Stryker 为注入覆盖率而临时修补 jest/babel 配置),结束后由 `.stryker-tmp/backup-*` 复原,**我已核实工作树恢复到运行前状态**(仅本报告与 `deliverables/` 两个未跟踪项)。但这意味着 CI 中一旦该 job 中途崩溃,仓库将留下**被变异过的源码**且无 `git checkout` 提示 —— 建议 CI 改用沙箱模式(去掉 `--inPlace`)。 2. 顺带证伪了一个可疑点:我曾怀疑 Stryker 用的 `config/test/jest.config.js` 与根 `jest.config.js` 存在测试发现差异(其 dry-run 报 "Ran 668 tests" 而 `test:unit` 为 1594)。实测 `npx jest --config config/test/jest.config.js` ⇒ **132 套件 / 1594 例全通过**,两配置的 `testMatch`/`roots`/`setupFilesAfterEnv` 一致,无结果分歧,故**不列为缺陷**(668 与 ✘ 标记是 Stryker perTest 覆盖分析的呈现方式,✘ = 该测试未覆盖当前变异体,**不是失败**)。唯一真实差异仍是 A-7 已记的**阈值不同**(此配置 global 为 70/55/55/55,根配置为 30/25/32/30)—— 即两份 jest 配置近重复却配着互不相同的门禁值。 ## 4. 高危问题(P1) ### 4.1 本分支引入的 UI 回归(验收主要风险) **R-1 双重移动端安全区留白 ≈190px** — `globals.css:1321-1323`(本分支审计修复 `152eef9` 新增)`footer { padding-bottom: calc(64px + env(safe-area-inset-bottom,0px)) }` 叠加 `src/components/layout/footer.tsx:171` 的 `pb-[calc(8rem+env(safe-area-inset-bottom,0px))]` ⇒ iPhone SE/15 上备案行下方 128+64+2×inset ≈ **192–226px** 空深色带,**全部 31 页**。 修复:二选一(建议只保留 CSS 侧,并删除组件 `pb-[…]`)。 **R-2 动效时长收敛到错误基准** — `CONTEXT.md:76` 与 `DESIGN.md:233` 规定入场 **180–280ms**,`--transition-normal: 280ms`(`globals.css:252`);分支却收敛到 300ms:`src/components/ui/scroll-reveal.tsx:73` `duration = 0.3` 且注释**错误引用契约为「200-300ms」**;另有 `duration-300` ×108、`duration: 0.3` ×179 未令牌化。 修复:以 `duration-normal/fast` 令牌替换字面量,修正注释;或正式修订 CONTEXT.md。同类:`--stagger-*` 令牌(`globals.css:272-276`)**零采用**(`var(--stagger` 检索 0 命中),实散为 `detail-trust-section.tsx:108` `index*0.06`、`header.tsx:230` `index*0.05`、`contact-content-v3.tsx:262` `delay: 1.2`(**1200ms**,远超 150ms 段间上限)。 **R-3 暗色模式新闻页对比度不达标** — `src/app/(marketing)/news/[slug]/NewsDetailClient.tsx:47` `bg-[var(--color-brand-bg)] text-[var(--color-brand)]`(另 `:34`、`:124`)误用**设计上不翻转**的 `--color-brand` #C41E3A 作文字色,违反 DESIGN.md:154 双通道红规则。暗色实测 **3.31:1**(`--color-bg-primary` #0A0E14 上)与 **2.97:1**(`--color-brand-bg` #2A1418 上)。`news-detail-content-v3.tsx:29,105`、`news-content-v3.tsx:37` 同病。同处 `hover:bg-[var(--color-brand)]/20`、`text-[var(--color-brand)]/30` 在构建产物中**不生成任何 CSS**(对照 `border-brand/30` 可正常编译)。 **本项是 §3.4 A-12 全站缺陷的一个局部实例** —— 同一契约违反在全仓共 67 处,故修复须按 A-12 整族收敛,只改新闻页会留下 Cookie 条等仍在失败。 修复:文字改用 `text-brand-ink`。同族缺陷另见 `content-unavailable.tsx:38`(本分支新增,暗色 3.31:1)。 **R-4 审计 §10.1 修复 #4 只落一半** — `src/components/detail/solution-service-card.tsx:128` `shadow-blue-500/20 → shadow-brand` 已做,同行保留 `bg-gradient-to-br from-[var(--color-accent-blue)] to-[#1d4ed8]`(硬编码 hex + 双色渐变),`:127` `hover:border-blue-300`、`:134` `group-hover:text-blue-600` 绕过 `accent-blue` 令牌且暗色不翻转 ⇒ **蓝色块配品牌红光晕**,违反 DESIGN.md:150/153「One Voice」与 CONTEXT.md:45「红与强调色不同卡」。 **R-5 `prefers-reduced-motion` 未被 framer-motion 尊重** — 全仓 **0 处 `MotionConfig`**(`grep MotionConfig src` 无命中),`globals.css:755-764` 的 CSS 守卫(`animation/transition-duration: 0.01ms !important`)**管不到 framer-motion 的 JS-rAF 内联样式**。仅部分文件单独调用 `useReducedMotion()`,未接线者(如 `detail-trust-section.tsx:80-83` 的 `y:24→0`、`detail-hero.tsx`、`product-detail-content-v3.tsx`)在暗色+动效默认开启的本分支上,对前庭敏感用户仍产生大位移。与 DESIGN.md:122「reduced-motion 全链路守卫」不符。 修复:根布局包 ``,一处收口。 ### 4.2 逻辑与安全(非本分支引入,但在验收范围内) **B-1 创建接口绕过发布工作流** — `src/app/api/admin/items/route.ts:130` `status: status || 'draft'` + `:135` `publishedAt: status==='published' ? new Date() : null`,仅 `:107` `requirePermission(…,'create')` 守卫;PUT 侧 `:184-186` 明确拒改 status 并要求走 workflow 接口 —— 即 `POST {status:'published'}` 可跳过 submit/approve 与 `publish` 权限直接上线。且任意 status 字符串可入库,之后 `workflow.ts:42` 对所有动作返回 `false`,条目永久卡死且无反馈。 **B-2 登录接口用户枚举 + 零限流** — `src/app/api/auth/login/route.ts:20-22` 在 `:24` `verifyPassword` **之前**返回 `'账号已被禁用,请联系管理员'`,未知用户则为 `:17` `'用户名或密码错误'`;即便消息相同,`user` 不存在时不跑 bcrypt 也留下可靠的时间侧信道。全仓唯一限流在 `src/app/api/contact/route.ts:14`,登录裸奔(`nginx-static-production.conf:177` `rate=100r/s` 仍允许约 860 万次/日)。修复:禁用检查移到验密之后;对 `/api/auth/login` 加 per-IP+per-username 计数或上游独立 `limit_req`。 **B-3 `basis` 仅类型约定,非结构强制**(直接对应「零编造」原则)— 类型侧全部可选:`products.ts:15`、`services.ts:40`、`solutions.ts:27`、`sections.tsx:323`、`about-content-v4.tsx:39` 皆 `basis?: MetricBasis`;运行侧 `items/route.ts:131` `JSON.stringify(data || {})` **完全不按 `FieldDefinition` 校验**(`cms/types.ts:50-55` 的 `validation {min,max,pattern}` 全仓从未执行,zod 只在 `api/contact/route.ts` 使用)。故 `POST {data:{metrics:[{value:'99.9%',basis:'verified'}]}}` 会渲染「已有可核验的实测出处」。机械门禁 `metrics-basis.test.ts:53-64,170-182` 读的是**导入的 seed 字面量**、只扫 `src/app`+`src/components`,`prisma/` 与 `/admin` 编辑后的库内容从不复检。 缓解事实:`resolveMetricBasis`/`weakestBasis`(`metrics-basis.ts:34-47`)保守回落 + `content-types.ts:8-18` 的 `metricBasisField` 明示「留空按目标口径处理」—— 兜底方向正确,`FORBIDDEN_PROOF_PHRASES` 扫描也确属严格(含 `length>40` 防空跑)。真正的漏洞在**写入侧无校验**,以及 `home-content-v15.tsx:510,552,555,562` 用 `Record` / `as any` 把 CMS 载荷从类型系统里放行。 **B-4 `data:null` 写库即打挂整页** — `items/route.ts:197` `if (data !== undefined) updateData.data = JSON.stringify(data)`(PUT 缺 POST 那样的 `|| {}` 兜底)→ `data-server.ts:20` `JSON.parse(item.data)` 得 `null` → `terms/page.tsx:180` `items.find(i => i.data.pageType === 'terms')` 抛 `TypeError`。同类:`data-server.ts:20,75-77,89-91,106` 的 `JSON.parse` **无 try/catch**(对照 `media-service.ts:133-139 parseDerivatives` 已正确守卫),单个脏列即可中断页面/SSG。 **B-5 角色权限重写无事务** — `src/app/api/admin/roles/route.ts:81` 先 `deleteMany` 全部权限再 `:85-97` 循环 `create`,**全仓应用代码零 `$transaction`**。中途失败即留下**权限为空/半权限**的角色(提权/降权双向风险)。同因:`cms/workflow.ts:59→66→80` `version: item.version+1` 读-改-写在事务外(`items/route.ts:192` 却用了正确的 `{ increment: 1 }`,同规则两实现)→ 并发审批丢更新 + status TOCTOU;`workflow.ts:80→90→102` update/audit/notify 三连 await 无原子性,通知失败会把成功审批变成 500。SQLite 侧 `src/lib/db.ts:7` 未配 `journal_mode=WAL`/`busy_timeout`(`DATABASE_URL` 无查询参数),默认 rollback journal + `busy_timeout=0` 下并发写直接 `SQLITE_BUSY`。 **B-6 `/api/cms/draft/enable` 密钥缺失即放行 + 开放重定向** — `route.ts:15` `if (expectedSecret && secret !== expectedSecret)` 为 **fail-open**;`CMS_PREVIEW_SECRET` 经全仓检索**不在 `.env` / `.env.local` / `.env.production` / `.env.example` 任何一处**,故线上恒为未配置,任意请求可 `draft.enable()`,并直达 `:25-27` `NextResponse.redirect(new URL(redirect, request.url))`,`redirect` 取请求体、协议相对形式 `//evil.com` 即跳出站外。(内容面影响当前为零:`draftMode()` 除这两个路由外无人读取,`data-server.ts:38,49,195,219` 硬过滤 `status:'published'`。对照 `/api/cms/revalidate/route.ts:40-45` 未配置即 500,写法正确 —— 应统一为 fail-closed。) **B-7 刷新令牌无轮换/吊销** — `auth/refresh/route.ts:16` 仅验签名,`:21-25` 直接用 `payload` 重签,不加载用户 ⇒ 被禁用/删除的用户 7 天内持续换取访问令牌,绕过 `login/route.ts:20` 的禁用拦截;`:24` `role: payload.role` 沿用旧 claim,降权用户保留高权标识(`permissions.ts:38-56` 按 `UserRole` 实查故服务端不破,但 `admin-layout.tsx:241`、`auth/me/route.ts:15` 会显示陈旧角色)。`logout/route.ts:5-10` 只清 cookie,`prisma/schema.prisma` 无 jti/tokenVersion 表 ⇒ 失窃 refresh token 登出后仍有效。 **B-8 Bento 网格 ARIA 角色无父(3 页 × 3 浏览器 = 9 例失败的真实根因)** — `src/components/sections/bento-grid.tsx:23` 把 `role="list"` 放在 `BentoGrid`、`:47` 子项 `role="listitem"`;但 `src/app/(marketing)/products/products-content-v3.tsx:9` **只 import 了 `BentoItem`**,卡片直接落在无 `role="list"` 的 `div.grid lg:grid-cols-2 gap-px`(`:~193`)上 ⇒ axe `aria-required-parent`(critical)。 **已由 Lighthouse 桌面端独立复现并逐节点确认**:`/products` 命中 **7 个** critical 节点,`data-testid` 分别为 `bento-product-card-{erp,crm,cms,bi,sds,oa,novavis}` —— 与我按源码推断的「7 张 BentoItem 卡」精确一致,故根因不是假设。该页 a11y 得分因此降至 **92**(仍高于 0.9 门禁,见 A-11)。 修复:改用 `BentoGrid` 包裹,或去掉 `BentoItem` 的 `role="listitem"`。 **B-9 `tracking-tightest` 是空类名,10 个 H1 丢失展示级字距** — `tailwind.config.js:157-165` 的 `letterSpacing` 仅 `tighter/tight/normal/wide/wider/widest/eyebrow`,**无 `tightest`**;构建产物 `dist/static/chunks/0o3c09ni2y1ao.css` 内只有 `tracking-tighter`/`tracking-tight` 两条规则,**不存在 `.tracking-tightest`** ⇒ 该类在 10 处标题(`contact-content-v3.tsx:249`、`about-content-v4.tsx:70`、`cases-content-v3.tsx:146`、`team-content-v3.tsx:168` 等)**静默失效**,全部大字标题以 0em 字距呈现,与 DESIGN.md:28 的 −0.03em 相悖。与 config 自述(`:130-133`)已发生过的 `font-calligraphy` 同族缺陷。 **B-10 mega 下拉键盘不可关闭(WCAG 1.4.13 / 2.1.2)** — `src/components/layout/mega-dropdown.tsx:37` `onMouseEnter` 开、`:28-30` 仅 `onMouseLeave` 关,无 Escape 路径;`header.tsx:28-32` 的全局 Escape 只看 `isOpen`(移动抽屉),从不看 `openDropdown`。键盘用户在「产品」上回车后必须动鼠标才能关闭。 ### 4.3 E2E 真实失败分类(92 = 约 30 例 × 3 浏览器,非抖动) | 失败簇 | 例数 | 定性 | |---|---|---| | `ga4-event-tracking` TC-GA4-001..004 `@critical` | 12 | **测试自身缺陷**(A-6 空测)+ 断言环境不成立 | | `mobile-accessibility` axe `aria-required-parent` `/products` | 3 | **真实缺陷** B-8(Lighthouse 桌面端独立复现同样 7 节点,见 B-8) | | `mobile-accessibility` axe `color-contrast` `/about`、`/team` | 6 | **需产品裁决**:节点为 `about-content-v4.tsx:217`、`team-content-v3.tsx:85,118` 的 `text-text-muted/30|/10` **`aria-hidden="true"` 装饰性序号**(Lighthouse 桌面端在 `/about` 复现同样 3 个 `aria-hidden` 节点,类名一致);axe 按视觉可见性仍会报。建议按 WCAG「incidental/decorative」显式豁免,或提高 alpha。真正该修的是**正文** `text-text-secondary/80`(`brand-content.tsx:129`、`team-content-v3.tsx:205`)。**注意:Lighthouse 另在 7/7 页各报 1 例装饰节点之外的真缺陷,即 A-12 Cookie 条链接 —— 移动套件反而没抓到它** | | 触摸目标 ≥44px(首页,`@accessibility`+`@performance`) | 6 | 真实移动可用性风险,需按元素定位 | | `p1-brand-visual-audit`「不应出现可见 novalon」× 首页/全站 | 6 | **测试过期**(见 §7),`CONTEXT.md:194` 已批准该文案 | | `uj-11b` 共创旅程 `@critical` | 6 | **真实可测性缺陷**:`uj-11-home-conversion.spec.ts:117` 依赖 `data-testid="early-access-banner"`,而首页 HTML 中该 testid **0 命中**;`:131` 期望文案「成为首批共创客户」HTML 亦 0 命中(「首批客户共创中」「共创进行时」均存在)。实现未落测试钩子 | ## 5. 中危(P2)摘要 - `header.tsx:187` `aria-controls="mobile-menu"` 指向 `{isOpen && …}` 内才存在的 `id`(`:219`)——关闭时 AT 解析落空。 - `contact-content-v3.tsx:111` `setErrors(prev => ({…prev,[field]:undefined}))` 不删键 ⇒ `:376` `Object.keys(errors).length` 与 `:379`「请修正以下 N 项」长期错误,字段全修完后仍残留带空 `
  • ` 的 `role="alert"` 红框。 - `header.tsx:167` `
    ` 使 `ThemeToggle` **桌面专属**,移动抽屉(`:224-261`)从不渲染 ⇒ 系统深色下的手机用户无法切浅色。本分支暗色默认开启,影响放大。 - `header.tsx:87` 高度 80/64 动画 vs `layout.tsx:18` 固定 `pt-16`(64px) ⇒ `scrollY===0` 时首屏顶部 16px 内容压在固定头下方。 - `api-crypto.ts:49-50` 以 `content-length` 判定有无请求体,chunked/H2 下**静默跳过解密**并把 `{data:""}` 交给 handler(`:93`),管理端保存即写入密文;`admin-api.ts:44-49` `catch {}` 在非安全上下文(`crypto.subtle` 不可用)**降级明文**;`:85` 将 `e.message` 回显客户端。密钥为 `NEXT_PUBLIC_*` 且盐硬编码 ⇒ 属混淆而非加密,无任何逻辑把它当鉴权用(此点正确)。 - `analytics.ts:178-188` `trackOutboundLink` 同时发 `outbound_click` 与 `click`,外跳统计**双计**;`:172` `value || 1` 吞掉 0;`:18-23` 默认 `analytics: true` 与 `:29-42` 无形状合并的 `JSON.parse` 可能违背用户实际同意。 - `use-focus-trap.ts:43-46` Escape 恒 `preventDefault` 并归还焦点却不通知持有者(焦点可逃出仍开启的 trap);`:13` 的 `[tabindex]:not([tabindex="-1"])` 只约束末项,`