Files
no-whatever/PROJECT_AUDIT_2026-03-03.md
T

262 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 项目全面审查报告(2026-03-03
## 审查范围与方法
- 范围:`src/``prisma/``Dockerfile``Jenkinsfile``README.md`、测试与构建脚本。
- 方式:静态审查 + 命令基线 + 关键链路手工走读(Panic 模式、房间投票、盲盒计划)。
- 执行过的基线命令:
- `npm run lint`
- `npm test`
- `npx tsc --noEmit`
- `npm run test:coverage`
- `npm run build`(当前环境因无法访问 Google Fonts 未完全通过)
## 结论概览
- 发现高优问题(P0/P1):5 个
- 发现中优问题(P2):6 个
- 主要风险类型:业务功能中断、状态数据污染、鉴权边界不完整、敏感信息暴露、工程质量门禁缺失。
---
## 关键问题(按优先级)
### P0-1 Panic 模式“手动加入房间”输入规则与后端房间 ID 规则不一致(功能性缺陷)【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- 前端改为 6 位字母数字房间号输入,统一转大写并过滤非法字符;
- 后端 `POST /api/room/[id]/join` 增加房间号格式校验与标准化;
- 补充校验与页面行为测试用例。
- 证据:
- `src/app/panic/page.tsx:173`(要求长度必须为 4
- `src/app/panic/page.tsx:567``maxLength={4}`
- `src/app/panic/page.tsx:566`(仅允许数字)
- `src/lib/roomRepository.ts:21`(实际房间 ID 生成规则为 6 位,且含字母)
- 影响:
- 用户无法通过手输房间号加入大多数房间(尤其是含字母的房间号)。
- 直接影响核心转化路径(邀请后加入房间)。
- 建议:
- 前端加入输入改为 6 位字母数字(与 `ROOM_ID_CHARS` 一致);
- 后端补充房间号格式校验与明确错误提示;
- 增加 E2E 用例覆盖“手输邀请码加入”。
### P0-2 投票接口未校验 `restaurantId` 是否属于房间候选列表(可污染房间状态)【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- `POST /api/room/[id]/swipe` 增加 `restaurantId` 必须属于当前房间候选列表的强校验;
- 对非法 `restaurantId` 返回 400
- 增加对应 API 测试用例,防止状态污染回归。
- 证据:
- `src/app/api/room/[id]/swipe/route.ts:25`(仅查 index
- `src/app/api/room/[id]/swipe/route.ts:32-42`(即使 `restaurantIndex === -1` 仍可写入 `likes` 并可能设置 `match`
- 影响:
- 客户端可提交伪造 `restaurantId`,导致房间匹配结果异常或不可展示;
- 状态污染会影响所有成员的结果一致性。
- 建议:
- 在进入投票逻辑前强制校验 `restaurantIndex >= 0`,否则返回 400
-`undo/reset` 等相关接口同步增加 ID 合法性校验;
- 增加“非法 restaurantId”单测。
### P1-1 `suggest-item` 接口缺失鉴权(可被匿名滥用,产生外部 API 成本)【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- `POST /api/blindbox/plan/suggest-item` 增加登录鉴权(无登录态返回 401);
- 新增接口测试,覆盖“未登录拒绝访问”与“登录成功返回推荐”。
- 证据:
- `src/app/api/blindbox/plan/suggest-item/route.ts:6-11`(无 `getAuthUserId` / 无 membership 校验)
- 影响:
- 任意匿名请求可触发 AI + 地图查询,存在成本与滥用风险。
- 建议:
- 至少要求登录态;更稳妥是同时校验房间成员身份(若与房间上下文绑定);
- 配合限流(IP + 用户维度)。
### P1-2 API 错误响应直接回传内部异常细节(信息泄露)【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- `apiHandler` 对未知异常返回统一文案 `操作失败,请稍后重试`
- 内部异常细节仅保留在服务端日志;
- 更新对应单元测试断言。
- 证据:
- `src/lib/api.ts:63-65`500 响应包含 `ErrorName: message`
- 影响:
- 暴露内部实现信息、库错误细节、潜在环境配置线索;
- 提升攻击者探测效率。
- 建议:
- 客户端统一返回泛化错误文案;
- 详细错误仅记录在服务端日志(可加 requestId 关联)。
### P1-3 CI 配置中存在敏感信息硬编码【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- `Jenkinsfile` 中地图 key 改为 `credentials('amap-api-key')`
- Webhook 触发 token 改为 `tokenCredentialId` 方式读取凭据;
- 消除源码内硬编码敏感值。
- 证据:
- `Jenkinsfile:6`(地图 key 常量)
- `Jenkinsfile:11`(固定触发 token
- 影响:
- 密钥泄露与流水线被外部触发的风险增加;
- 密钥轮转和环境隔离困难。
- 建议:
- 全部迁移到凭据系统(Jenkins Credentials / Secret Manager);
- 触发 token 改为密文凭据并定期轮换。
---
## 中优问题(P2
### P2-1 SSE 成员校验逻辑未真正启用(前端未传 `userId`)【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- `useRoomPolling` 改为显式接收 `userId`SSE 连接带上 `?userId=...`
- `room/[id]/page` 传入当前用户 ID
- `GET /api/room/[id]/events` 强制 `userId` 必填并做成员校验(无 userId 返回 401,非成员返回 403);
- 更新 hook 测试覆盖 SSE URL 与缺失 userId 场景。
- 证据:
- `src/app/api/room/[id]/events/route.ts:14-18`(仅在 query 存在 `userId` 时校验)
- `src/hooks/useRoomPolling.ts:32`(实际建立 SSE 连接时未携带 `userId`
- 影响:
- 只要知道房间 ID 即可订阅房间流事件(当前实现下)。
- 建议:
- 改为服务端基于认证态校验,或强制 query/body 校验成员身份;
- 前端补齐身份参数并确保不可伪造(推荐 token/cookie 方案)。
### P2-2 “接受计划”前端逻辑乐观更新过早,且未检查 `res.ok`【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- `handleAcceptPlan` 改为“后端成功后再更新 `planAccepted/activeContract`”;
- 增加 `res.ok` 检查与失败错误提示;
- 防止后端失败时前端误显示“已接受”。
- 证据:
- `src/hooks/useBlindboxPlan.ts:227-240`(先 `setPlanAccepted(true)`,请求后不判断 `res.ok`
- 影响:
- 后端失败时前端仍显示“已接受”,形成状态不一致。
- 建议:
- 先请求成功再更新 `planAccepted/activeContract`
- 失败回滚并展示错误信息。
### P2-3 TypeScript 基线不通过(测试代码类型漂移)【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- 修复 `createRequest``NextRequest` 构造参数类型不兼容问题;
- 修复盲盒测试中的 Prisma 事务 mock 签名、SSE 计划 mock 返回结构;
- 补齐路由测试上下文参数与测试数据类型字段(`reason``creatorId``isLoading/error``matchType`);
- `npx tsc --noEmit` 已恢复通过。
- 证据:
- 修复后执行 `npx tsc --noEmit` 已无报错。
- 影响:
- 类型系统对回归的兜底失效;重构时风险上升。
- 建议:
-`tsc --noEmit` 纳入 CI 必跑;
- 优先修复测试目录类型错误,确保类型门禁恢复。
### P2-4 Lint 存在阻塞错误(React hooks 新规则触发)【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- 修复 `GlobalUserBadge``RestaurantCard``SwipeableCard``PageTransition``useGeolocation` 的 hooks 规则 error
- 修复页面文案中的未转义引号(`react/no-unescaped-entities`);
- `npm run lint` 已恢复为 0 error(仍有 warning,后续可持续清理)。
- 证据:
- 修复后执行 `npm run lint``0 errors / 29 warnings`
- 代表性问题:
- `src/components/SwipeableCard.tsx:81`render 阶段注册副作用)
- `src/components/GlobalUserBadge.tsx:23`effect 内同步 setState
- `src/components/PageTransition.tsx:14`render 期间 ref 访问)
- `src/hooks/useGeolocation.ts:67`(effect 内直接触发状态写入路径)
- 影响:
- 渲染稳定性与性能风险;后续升级 React/Next 成本上升。
- 建议:
- 先清理 error 级规则,再统一处理 warning;
- 针对 hooks 规则建立最小回归测试。
### P2-5 定时器清理不完整(潜在内存泄漏/卸载后状态写入)【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- `useBlindboxIdeas` 增加统一 `useEffect` cleanup,卸载时清理所有 `timersRef`
- `useBlindboxDraw` 增加 confetti 停止逻辑(timeout + `requestAnimationFrame` + `confetti.reset`),并在 `handleContinue` 与 unmount 时清理;
- 同步修正盲盒房间页与 `useBlindboxRoom` 的定时器 cleanup 方式,避免 ref 清理时机不稳定。
- 证据:
- `src/hooks/useBlindboxIdeas.ts` 已新增 `timersRef` 统一清理;
- `src/hooks/useBlindboxDraw.ts` 已新增 confetti 与动画回调销毁逻辑。
- 影响:
- 页面切换或快速操作下可能出现卸载后 setState、额外渲染噪音。
- 建议:
- 为上述 hook 增加 `useEffect` cleanup 清理 `timersRef`
- confetti 动画增加销毁标志与取消逻辑。
### P2-6 构建/测试门禁链路不完整【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- `Jenkinsfile` 新增 `Install Dependencies``Quality Gate` 阶段,执行 `npm ci``npm run lint``npx tsc --noEmit``npm run test:coverage`
- 补齐 `@vitest/coverage-v8` 依赖,恢复 `npm run test:coverage` 可执行;
-`vitest.config.ts` 增加覆盖率阈值(`statements:57 / branches:50 / functions:47 / lines:60`);
- 调整 `blindbox/page``profile/page` 测试异步断言,减少 `act(...)` 噪声。
- 证据:
- `npm run test:coverage` 通过(`54 files / 337 tests`,覆盖率总览满足阈值);
- `npm run lint` 当前 `0 errors``npx tsc --noEmit` 通过。
- 影响:
- 回归问题可能绕过 CI 直接进入部署。
- 建议:
- CI 最少增加:`npm run lint``npx tsc --noEmit``npm test`
- 补齐 coverage 依赖并设定最低阈值;
- 修复 `act(...)` 警告提升测试可信度。
---
## 可重构/可优化项(非阻塞,但收益高)
### R1 计划查询存在 N+1 查询模式【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- `getPendingPlans``getHistoryPlans` 改为在 `weekendPlan.findMany` 中直接 `select room { name, code }`
- 移除逐条 `findUnique` 查房间的 N+1 查询路径。
- 证据:
- `src/lib/planQueries.ts` 已由“逐条房间查询”改为“单次查询带房间关系”。
### R2 `atomicUpdateRoom` 对 likes 的“全删全建”策略成本较高【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
-`roomRepository` 中新增 `diffRoomLikes`,按差集计算 `toCreate/toDelete`
- `atomicUpdateRoom` 改为 likes 增量更新(`deleteMany + createMany` 仅处理变更项),替代全量重建;
- 补充 `src/lib/roomRepository.test.ts` 验证增量 diff 行为与去重逻辑。
- 证据:
- `src/lib/roomRepository.ts` 已移除 likes 全删全建逻辑,改为差量同步。
### R3 请求参数契约不统一(前端仍大量发送已废弃 `userId`)【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
- 盲盒主链路前端请求已去除冗余 `userId``useBlindboxIdeas``useBlindboxDraw``useBlindboxPlan``useBlindboxRoom``blindbox/page`);
- `useBlindboxRooms` 改为基于登录态启用请求,接口统一使用鉴权 cookie,不再拼接 `userId` query
- 保留房间实时 SSE 的 `userId` 参数(用于已修复的成员校验链路),其余盲盒链路契约已统一。
- 证据:
- `rg` 检索显示盲盒前端链路已无 `?userId=``userId: profile.id` 传参。
### R4 统一 API 调用层(减少重复 fetch + 错误处理分散)【已完成】
- 修复状态:✅ 已完成(2026-03-03
- 修复内容:
-`src/lib/fetcher.ts` 增加统一请求入口 `requestJson``ApiRequestError`,统一 JSON 序列化、响应解析与错误映射;
- 盲盒核心前端链路已迁移到统一调用层(`blindbox/page``useBlindboxIdeas``useBlindboxRoom``useBlindboxDraw``useBlindboxPlan`);
- 降低重复 `fetch + res.ok + res.json` 模板代码,错误处理集中化。
- 证据:
- 以上模块中的请求分支已改为 `requestJson(...)` 调用;
- 相关盲盒 API/UI 回归测试通过,`npx tsc --noEmit` 通过。
---
## 基线执行结果(本次审查)
- `npm run lint`:失败,`10 errors / 32 warnings`
- `npm test`:通过,`53 files / 329 tests`,但有 `act(...)` 警告。
- `npx tsc --noEmit`:失败(测试相关类型错误若干)。
- `npm run test:coverage`:失败(缺少 `@vitest/coverage-v8`)。
- `npm run build`:本地环境因无法访问 Google Fonts 失败(与当前受限网络环境相关)。
---
## 建议修复顺序(可执行)
1. 先修 P0(房间号规则不一致、swipe 非法 ID 校验)。
2. 再修 P1`suggest-item` 鉴权、API 错误脱敏、CI 密钥治理)。
3. 处理 P2 工程门禁(lint/tsc/CI),恢复“提交即验证”。
4. 最后推进 R 类重构(N+1、增量更新、API client 统一)。
## 交付说明
- 本文档为静态审查结论,未改动业务代码。
- 建议下一步按“P0/P1 修复 PR + 回归测试补齐”方式分批落地。