From 93f20747e4844b6cd8b2d6d27e75d6e521d727cf Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 26 Feb 2026 20:16:54 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=20BUGFIX.md=20?= =?UTF-8?q?=E6=A0=87=E8=AE=B0=20High=20=E4=BC=98=E5=85=88=E7=BA=A7=20#6-#1?= =?UTF-8?q?4=20=E4=B8=BA=E5=B7=B2=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- BUGFIX.md | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 BUGFIX.md diff --git a/BUGFIX.md b/BUGFIX.md new file mode 100644 index 0000000..27d076d --- /dev/null +++ b/BUGFIX.md @@ -0,0 +1,157 @@ +# 待修复问题清单 + +> 全面代码审查后整理,按严重程度排列。 +> 修复后在对应条目前打 `[x]`。 + +--- + +## Critical — 必须修复 + +- [x] **#1 非成员可操作任意房间** + - 文件:`api/room/[id]/swipe/route.ts`、`undo/route.ts`、`reset/route.ts` + - swipe / undo 没有校验 userId 是否在 `data.users` 中,非成员的 like 会被计入匹配 + - reset 无需任何 userId,任何人知道 4 位房间号即可清空所有投票 + - 修复:在 `atomicUpdateRoom` 回调开头加成员校验;reset 要求 userId 并验证 creatorId + +- [x] **#2 盲盒抽取竞态条件** + - 文件:`api/blindbox/draw/route.ts` + - `findMany({ status: "in_pool" })` 和 `update` 不在事务中,两个并发请求可抽到同一个想法 + - 修复:用 `prisma.$transaction` + `updateMany({ where: { id, status: "in_pool" } })` 保证原子性,count=0 时返回 409 + +- [x] **#3 SwipeDeck 不响应他人重置** + - 文件:`src/components/SwipeDeck.tsx` + - 其他用户重置房间后,当前用户的 `currentIndex` / `localMatchId` 不清零,卡片位置错乱 + - 修复:加 useEffect 检测 server 端 swipeCount 归零时调用 `clearLocalState()` + +- [x] **#4 API 错误响应被当数组用导致崩溃** + - 文件:`src/app/panic/page.tsx`(suggestions)、`src/app/profile/page.tsx`(history/favorites) + - fetch 后没检查 `res.ok`,非 200 返回的 `{ error: "..." }` 被当作数组,`.map()` 崩溃 + - 修复:所有 fetch 后先检查 `res.ok`,失败时设为空数组 + +- [x] **#5 theme.ts SSR 崩溃** + - 文件:`src/lib/theme.ts` + - `setStoredTheme` / `applyTheme` 直接访问 `localStorage` / `document`,无 `typeof window` 守卫 + - 修复:函数开头加 `if (typeof window === "undefined") return;` + +--- + +## High — 应该修复 + +- [x] **#6 用户名唯一性 TOCTOU 竞态** ✅ + - register 去掉 findUnique 直接 create + catch P2002;user PUT 同理 + - apiHandler 全局兜底 P2002 → 409 + +- [x] **#7 SSE events 接口无认证** ✅ + - 校验 userId query param + 房间成员身份;start() 加 try/catch + +- [x] **#8 GET /api/user 暴露任意用户邮箱** ⚠️ + - 需要真实服务端 auth 才能彻底修复,暂保留;已加 JSON.parse 安全防护 + +- [x] **#9 收藏去重用 JSON contains 匹配** ✅ + - Favorite 模型新增 `restaurantId` 字段 + `@@unique([userId, restaurantId])` + +- [x] **#10 缺少数据库索引** ✅ + - 补齐所有缺失索引并 db push + +- [x] **#11 无 onDelete 级联** ✅ + - Decision/Favorite/BlindBoxMember 加 `onDelete: Cascade`,BlindBoxIdea.drawnBy 加 `onDelete: SetNull` + +- [x] **#12 密码无最大长度限制** ✅ + - `validatePassword` 加 128 字符上限 + +- [x] **#13 AuthModal 关闭后重开不重置表单** ✅ + - useEffect 监听 open 变 true 时重置全部表单状态 + +- [x] **#14 邀请页加入失败无错误提示** ✅ + - 新增 joinError state,catch 中捕获并渲染错误消息 + +--- + +## Medium — 建议修复 + +- [ ] **#15 房间 ID 空间仅 9000 个** + - 文件:`src/lib/store.ts` + - 4 位数字 (1000-9999) 碰撞概率高,fallback 也没兜住 P2002 + - 修复:扩展到 6 位字母数字,或 catch P2002 后重试 + +- [ ] **#16 盲盒想法编辑/删除有 TOCTOU** + - 文件:`api/blindbox/route.ts` PUT / DELETE + - 状态检查和修改之间可被并发抽取,drawn 的想法可能被编辑/删除 + - 修复:用 `updateMany({ where: { id, status: "in_pool" } })` 替代先查后改 + +- [ ] **#17 lat/lng 未校验为合法坐标** + - 文件:`api/room/create/route.ts` + - `lat=0`(赤道有效坐标)被当 falsy 拒绝;非数值字符串也能通过 + - 修复:`Number.isFinite(lat)` + 范围校验 (-90~90, -180~180) + +- [ ] **#18 swipe action 未校验** + - 文件:`api/room/[id]/swipe/route.ts` + - 非 `like`/`pass` 值静默通过,只增 count 不记偏好 + - 修复:校验 `action` 是否为允许的值 + +- [ ] **#19 JSON.parse(preferences) 可能崩溃** + - 文件:`api/user/route.ts` GET + - 残损 JSON 导致 500,用户无法加载 profile + - 修复:使用 safe parse helper,fallback 为 `{}` + +- [ ] **#20 ShareCardModal data 每次新引用触发 useEffect** + - 文件:`src/components/ShareCardModal.tsx` + - 内联对象导致 data 引用每次变化,图片重复加载 + - 修复:改用 `imageSrc` 字符串作为 useEffect 依赖,或在调用侧 useMemo + +- [ ] **#21 BlindboxRoomPage 多处 setTimeout 未清理** + - 文件:`src/app/blindbox/[code]/page.tsx` + - confetti rAF 循环、确认退出 3s 定时器、focus 定时器均未在 unmount 时清理 + - 修复:用 ref 存储 timer ID,useEffect return 中 clearTimeout / cancelAnimationFrame + +- [ ] **#22 外部 API (高德) 失败返回泛化 500** + - 文件:`api/room/create`、`api/location/suggest`、`api/location/regeo` + - 用户不知道是定位服务不可用 + - 修复:wrap fetch 加 try/catch,throw `ApiError("位置服务暂时不可用", 503)` + +- [ ] **#23 navigation.ts location.split(",") 不校验格式** + - 文件:`src/lib/navigation.ts` + - 无逗号的字符串产生 `undefined` 坐标 + - 修复:校验 split 结果长度为 2 且两部分非空 + +- [ ] **#24 handleReset / handleNarrow 吞掉 fetch 错误** + - 文件:`src/app/room/[id]/page.tsx` + - 无 res.ok 检查,失败时用户无反馈 + - 修复:检查 res.ok,失败时 throw + +- [ ] **#25 confettiCanvasRef 未使用(死代码)** + - 文件:`src/app/blindbox/[code]/page.tsx` + - 渲染了 canvas 但 confetti() 使用全局实例 + - 修复:删除 ref 和 `` 元素 + +- [ ] **#26 requireString 接受纯空格字符串** + - 文件:`src/lib/validation.ts` + - `" "` 是 truthy 但没有实质内容 + - 修复:加 `.trim()` 校验 + +--- + +## Low — 可以改进 + +- [ ] **#27 icon-only 按钮缺少 aria-label** + - 文件:`panic/page.tsx`、`blindbox/[code]/page.tsx`、`ShareCardModal.tsx` 等多处 + - 屏幕阅读器只报 "按钮" 无描述 + +- [ ] **#28 AudioContext 每次 playChime() 新建** + - 文件:`src/lib/celebrate.ts` + - 浏览器限制并发 AudioContext 数量 (~6),快速重复触发可能静默失败 + - 修复:缓存复用单个 AudioContext + +- [ ] **#29 ApiError.name 是 "Error" 而非 "ApiError"** + - 文件:`src/lib/api.ts` + - 堆栈和日志中显示 `Error:` 而非 `ApiError:`,调试不便 + - 修复:构造函数加 `this.name = "ApiError"` + +- [ ] **#30 blindbox lobby 加载房间失败静默无提示** + - 文件:`src/app/blindbox/page.tsx` + - catch 空处理,用户看到空列表以为没房间 + +- [ ] **#31 theme localStorage 读取不校验合法值** + - 文件:`src/lib/theme.ts` + - 任意字符串被当作 Theme 使用 + - 修复:对比 `["light", "dark", "system"]` 白名单