# 待修复问题清单

> 全面代码审查后整理，按严重程度排列。
> 修复后在对应条目前打 `[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 — 建议修复

- [x] **#15 房间 ID 空间仅 9000 个** ✅
  - 扩展为 6 位字母数字 (30^6 ≈ 7.3 亿)，createRoom 用 P2002 重试

- [x] **#16 盲盒想法编辑/删除有 TOCTOU** ✅
  - PUT/DELETE 改用 updateMany/deleteMany 原子操作

- [x] **#17 lat/lng 未校验为合法坐标** ✅
  - Number.isFinite + 范围校验 (-90~90, -180~180)

- [x] **#18 swipe action 未校验** ✅
  - 校验 action 必须为 'like' 或 'pass'

- [x] **#19 JSON.parse(preferences) 可能崩溃** ✅
  - GET 和 PUT 响应均加 try/catch，fallback {}

- [x] **#20 ShareCardModal data 每次新引用触发 useEffect** ✅
  - 依赖改为 imageSrc 字符串而非整个 data 对象

- [x] **#21 BlindboxRoomPage 多处 setTimeout 未清理** ✅
  - timersRef 统一收集所有 setTimeout，unmount 时批量清理；confetti rAF 用 alive ref 控制

- [x] **#22 外部 API (高德) 失败返回泛化 500** ✅
  - 三个高德 API 路由 fetch 加 try/catch → 503

- [x] **#23 navigation.ts location.split(",") 不校验格式** ✅
  - 校验 split 结果长度为 2 且两部分非空

- [x] **#24 handleReset / handleNarrow 吞掉 fetch 错误** ✅
  - 检查 res.ok，失败时 toast 提示

- [x] **#25 confettiCanvasRef 未使用（死代码）** ✅
  - 删除 ref 和 canvas 元素

- [x] **#26 requireString 接受纯空格字符串** ✅
  - 加 .trim() 校验

---

## Low — 可以改进

- [x] **#27 icon-only 按钮缺少 aria-label** ✅
  - panic/blindbox/ShareCardModal/AuthModal 中所有 icon-only 按钮补 aria-label

- [x] **#28 AudioContext 每次 playChime() 新建** ✅
  - 缓存复用单个 AudioContext，state === "closed" 时才重建

- [x] **#29 ApiError.name 是 "Error" 而非 "ApiError"** ✅
  - 已在 High 批次修复

- [x] **#30 blindbox lobby 加载房间失败静默无提示** ✅
  - 新增 loadError state，失败时显示"加载失败 / 点击重试"

- [x] **#31 theme localStorage 读取不校验合法值** ✅
  - 已在 Critical 批次修复（VALID_THEMES 白名单校验）
