# gongxue-base 修复记录（Phase 0-2 实施）

> 依据 2026-08-09 OCR 全库审查（`all-comments.jsonl`，1626 条精确去重）与用户确认的计划（P0+P1，直接实施，允许 schema 变更）。
> 执行：主 agent + 9 个并行 worker（按互不重叠的文件划分）。全部改动未提交，供人工 review。

## 验证基线（最终）
- server：`npm run typecheck` ✅ ｜ `npm run lint` ✅ 0 error 0 warning ｜ `jest` 148 套件 / 1087 用例 ✅
- admin：`npm run typecheck` ✅ ｜ `vitest run` 33 文件 / 175 用例 ✅ ｜ `oxlint` 0 error（any 为 warn）
- OCR 复查：全量改动 `ocr review` 1 轮（100 文件 / 84 条），定向 `ocr scan` 2 轮（12 文件 60 条 → 4 文件 18 条），发现的 critical/high 已逐条修复或明确降级为待办。

## Phase 0 — 部署/运维
- `serve-proxy.js`：API_TARGET 真正生效（new URL 解析）；SPA fallback 仅导航请求，缺失资源 404；fs.promises 异步读文件；剥离 hop-by-hop 头（含 Connection 提名头）；proxyRes/req error 监听 + 30s 超时（带 error 销毁 + 504）+ 同步异常 try/catch；路径穿越防护。
- `migrate.sh`：密码改用子 shell 内 `MYSQL_PWD`（不进 argv、不泄漏 pm2）；删除硬编码兜底密码，缺失即报错；SQL 包事务；`sleep 8` 改为轮询 users/tenants/operation_logs 就绪；`pm2 startOrReload`；`trap` ERR 停应用；`.env` 从脚本目录读取并去引号；导入计数不再被 `set -e` 掩盖。
- `deploy.sh` / `.gitea/workflows/deploy.yml`：rsync 排除 `.env*`/`data.sql`/`uploads/`；总是 `npm ci`（含 devDependencies，migration:run 依赖 ts-node）；远程块 `set -e`；workflow 加 concurrency。
- `ecosystem.config.cjs`：`kill_timeout: 5000`、`listen_timeout: 8000`、绝对日志路径。
- `oxlint.config.ts`：React 版本对齐实际安装（19.2.7）；`no-explicit-any` 改为 warn。

## Phase 1 — 安全
- JWT：`jwt-secret.ts` 统一取密钥，生产缺 `JWT_SECRET` 启动即失败，删除公开兜底密钥。
- 密码策略：登录最短 8 位、`@MaxLength(72)`；用户名 `@MaxLength(64)`；登录失败文案统一防枚举。
- 越权/IDOR：classes 全部变更接口补 `assertReadAccess`；dashboard 五个全局接口按班级范围收紧（非管理员返回空）；schedules.findAll 始终应用范围；students basic-lookups 范围化；attendance match 校验学生范围、auto-match 仅管理员；archive 全部路由按学生范围校验（新增 `resolveRecordStudentId` + `StudentsService.assertStudentAccess`）；exams 权限码尝试分离后因 `exam:create/edit/delete` 属废弃码**回滚**为 `exam:view`（服务层已有范围校验，见待办）。
- 全局 `ValidationPipe({ transform: true })`（不开 whitelist，避免裸 body 被剥空）。
- 凭据：集成配置 appSecret 复用 AES-256-GCM 静态加密（`secret-crypto.ts` + 回填脚本 `npm run encrypt:integration-secrets`）；对外输出脱敏。
- 注入：`common/like-escape.ts` + 13 处 LIKE 调用转义（含 rbac 教师搜索）。
- 无界输入：attendance records `@ArrayMaxSize(500)`、ai-chat values ≤200 字段、notes `@MaxLength(1000)`、sync limit ≤200、全部上传 10MB 上限。
- 上传/下载/XSS：`common/mime.ts` 扩展名白名单归一化 + `X-Content-Type-Options: nosniff` + 安全类型才 inline；上传判空 400；流 error/close 处理；`AttachmentsTab` 不再 window.open(blobURL)（img/sandbox iframe/<a download> + revoke）。
- 其他：`request-utils` 默认不信任代理头（`TRUST_PROXY=1` 才信任）；`updateTeacherProfile` 返回脱敏；`.env.example` 补充 TRUST_PROXY 说明。

## Phase 2 — 数据正确性
- 并发/事务：wallets 原子扣款/退款（`balance ± :amount` + 余额条件，杜绝 double-spend）；financial-operations 条件认领 + 支持 15 分钟 stale running 回收 + 结果 JSON 解析容错；ai-a2ui 唯一约束冲突幂等；deposits purge/create 排除归档；with-audit-log best-effort；sync-runner finally 不掩盖原错误；imports.commit/preview、students.import、rooms.create、exams 批量、attendance-generation 事务化；occupancies 用父行 FOR UPDATE 防重复入住 + 退宿按剩余在住置状态；attendance-import 用 MySQL advisory lock + 30 分钟看门狗。
- 校验：3 个 DTO 的矛盾校验器（@Matches vs @IsISO8601 strict）统一为仅日期；rental 跨字段 `endDate >= startDate`；expenses 上传空值 400。
- 实体：learning-record / student-profile 的 student_id 双映射移除；student-wallet / bill 金额 DECIMAL 加 number transformer；ding-leave/attendance-raw 可空 FK 类型 `| null`；class-student 外键 onDelete 统一 CASCADE；`app.module` synchronize 仅显式 true。
- 时区：`common/china-time.ts`（getChinaDate / addDaysToDateOnly / getWeekDayFromDateOnly / isConsecutiveDates + 单测）；attendance-calendar、ai-review.import-basic、attendance-generation、rental-schedule 的本地/UTC 混用修复。
- 状态/路由：bills batch/status 与 classrooms report 路由顺序；attendance-report 连续日真实判断；sync 空 mappedValues 跳过更新；考勤组名带 classId；rbac-seed 仅空表或 `SEED_ROLES=true` 播种 + 默认管理员仍会创建（生产缺 ADMIN_PASSWORD 报错、不打印密码、不再批量重激活禁用用户、删除死代码）；expense-types 唯一性检查回滚为与 DB 唯一索引一致；agent-tool.registry 重复注册守卫。
- 前端：Roles 多组勾选受控化；Schedules 月视图可新建；useDirtyGuard 返回稳定引用；JinshujuMatchModal success:false 分支；Attendance 订阅 store；late 计入出勤率/主状态；EditableCell Enter 不再与 select/date 编辑器冲突；DynamicChart 空数据短路；useSubmissionState ref 变更移到 layout effect。

## 追加修复（2026-08-09 第二轮）
- **exams 写权限分离**：`exam:create/exam:edit/exam:delete` 从 `DEPRECATED_PERMISSION_CODES` 移除并加入 `PRESET_PERMISSIONS`（seed 会创建）；控制器 create→`exam:create`，archive/restore/batch-*/updateScore→`exam:edit`，purge→`exam:purge`；spec 同步（新增 4 组权限断言）。
- **wallets refundBill 幂等**：退款前对账单做条件更新 `status <> 'cancelled'`（affected=0 直接返回），与 bills.cancel 的事务悲观锁双保险，杜绝并发重复冲正。
- **exams updateScore 防误清空**：`score` 缺失（undefined）直接 400，清空必须显式传 null。
- 验证：server typecheck/lint ✅，jest 148 套件 / 1091 用例 ✅；定向 OCR 复查 3 文件无 high/critical。

## 持续修复循环（Round 1-7，OCR 复查驱动）

> 按用户要求启动持续修复+review goal：每轮并行派发 3 个互不重叠的 worker → 全量验证 → OCR 定向复查 → 再修，共 7 轮。

- **R1（medium 清理）**：deploy SSH_HOST 校验/密钥 heredoc、jwt-secret 环境判定、encrypt 脚本防回退密钥+连接释放、archive 下载审计、bills 通知 catch；rbac 密码 8-72、attendance-import success/release、wallets 分页、attendance 日期边界/连续段语义、AttachmentsTab 乱序、serve-proxy 尾斜杠；migrate trap/防重入/就绪检查、rental ENDED、students.import 死注入、bills-generation 注释。
- **R2（high 修复）**：archive 死代码、rental 死循环×2、financial SQL 优先级、wallets :amount 绑定、exams 权限回滚+spec、rbac-seed admin 创建、attendance 看门狗语义、serve-proxy fallback MIME、migrate argv、deploy npm ci、expense-types 一致性。
- **R3（exams 权限 + wallets 幂等）**：exam:create/edit 从废弃列表恢复并 seed、控制器按写操作标注；refundBill 条件更新幂等；updateScore 空 PUT 防护。定向复查 3 文件 0 high。
- **R4（medium 批量）**：bills 通知/ids、jwt-secret、encrypt 脚本、archive 审计、attendance success/release/日期/连续段、AttachmentsTab 序号、serve-proxy、migrate trap/flock、rental ENDED、students.import 死注入、rbac.dto 校验。
- **R5（4 high + quick wins）**：migrate cd/TOCTOU、jwt SEED_DEV、students.import 空 updates、bills 首次生成并发（GET_LOCK）；serve-proxy 超时竞态/非法头/请求头 hop-by-hop/URL 回退；attendance-generation N+1、matchImport 事务、锁名常量、rbac.dto 校验、bills 批量学生+allSettled。
- **R6（high→1 + quick）**：attendance 看门狗只告警、runner.connect try/finally、fetchAllPages 并发、mapToEntity 批量预取；attendance-generation 标签/死代码/事务；bills 批量保存/四舍五入/未用注入；rental 多日展开+事务+枚举；students.import host org 缓存/日期日历校验；rbac.dto 锚定正则+长度上限。
- **R7（high→0）**：bills 路由顺序（export 在 :id 前）+ ids DTO；serve-proxy 失败分支 error 监听/写头抛错释放 socket/客户端断开；migrate DUMP 绝对路径/原子锁/文件预检/APP_STARTED/5 表就绪/DML-only 说明；rbac.dto 显示名允许内部空格+密码字节上限+subjects 唯一性；bills 通知幂等（仅新增 paid 才发）+ reason 审计转义（本轮新增）。

### 收敛结论
- 连续 2 轮 OCR 定向复查（覆盖所有修复过的文件）：**high/critical = 0**。
- 每轮全量验证：server `typecheck` ✅ `lint` 0 error ✅ `jest` **152 套件 / 1170 用例** ✅；admin `typecheck` ✅ `vitest` 175 ✅。
- 剩余为 **P2 性能/重构/依赖 schema** 项（见下），非本轮目标。
### 收尾补充（复查核对后）
- `bills.controller`：取消账单的 `reason` 写入审计日志前做控制字符过滤（防日志注入）。
- `serve-proxy`：删除 serveStatic 未使用的 `req` 参数。
- `migrate.sh`：就绪检查超时时输出期间收集的 MySQL stderr（密码错误/容器未启动可诊断）。
- 验证：server lint/typecheck/jest 152 套件 / 1170 用例全绿。

## P2 纯代码项（并行 3 worker + 复查二修）
- **serve-proxy 静态流式化**：fs.stat + Content-Length + createReadStream pipe，消除全量缓冲；目录请求 404、realpath 防 symlink 穿越。
- **migrate.sh 库名变量化**：LEGACY_DB/TARGET_DB 抽变量；COMMIT 用 printf 防粘行；IMPORTED_COUNT 失败兜底；`.migrate.done` 改为数据提交后立即写。
- **attendance-import**：IN(dingIds) 按 1000 分块并发查询；finally 仅抢到锁才清理运行态。
- **students.import**：phone/idNumber 批量预取（2 条 IN 查询）替代逐行 findOne；新增双键冲突检测（phone/idNumber 命中不同学生 → 跳过并计 skippedConflict，防数据错乱）。
- **bills-generation**：per-room 入住查询改为一次 IN 批量 + 内存分组；日期统一 YYYY-MM-DD 字符串比较（toDateOnly/daysBetweenDateOnly），消除 Date hydrate 比较不可靠；eligibleDays 提出循环。
- **bills DTO**：BatchIdsBodyDto 移入 dto/bill.dto；batchPurge/batchRemove 复用 common BatchIdsDto；billingMonth 收紧为真实月份。
- **rbac.dto**：UpdateRoleDto/UpdateUserDto 用 PartialType/OmitType 去重；subjects 补非空白 each。
- 复查二修：serve-proxy 目录/realpath、matchImport 冲突、bills 日期、attendance finally、DTO/migrate 小项；spec 同步。
- 验证：server typecheck/lint 全绿，jest **152 套件 / 1178 用例** ✅。

## 剩余（已收敛）
- 数据库唯一约束（bills 周期、Student.phone/idNumber、expense-type code 放开软删复用）与迁移文件生成（class_student 外键 CASCADE）——需接库环境执行 `npm run migration:generate -- FixEntityMappings`。
- 少量 low（STATIC_DIR 可配置、%20 解码、X-Forwarded 链保留、登录时序侧信道权衡项等）按需处理。
- 迁移文件生成（class_student 外键 CASCADE 等）：`cd apps/server && npm run migration:generate -- FixEntityMappings`（需接库环境）。
- 低价值 low 项（如 STATIC_DIR 可配置、%20 解码、x-forwarded 链保留、attendance-import 无界 IN 分块等）按需后续处理。

## 待办/已知（未修）
1. ~~exams 写权限分离~~ ✅ 已修复（见追加修复）。
2. ~~wallets refund 的 bill 状态 check-then-act~~ ✅ 已修复（条件更新幂等）。
3. **迁移文件**：实体改动（class_student 外键 CASCADE 等）需在接库环境 `cd apps/server && npm run migration:generate -- FixEntityMappings` 生成并 review 后上线；本地无 MySQL 未生成。
4. **P2 backlog**：剩余 low/维护性 ~700 条（见 `all-comments.jsonl`），未在本轮实施。
5. OCR 复查仍提示的 medium/low（如 migrate.sh 无并发锁/命令注入面、rbac-seed N+1、serve-proxy 的 servedIndex 冗余等）可按后续迭代处理。

## 交付物
- 改动：`git status` 约 120+ 文件（未提交）。
- 报告：`ocr-reports/审查汇总.md`、`high-severity-comments.md`、`all-comments.jsonl`、`admin/server/misc-scan-comments.jsonl`、`修复记录.md`（本文件）。
