fix: 花名册↔报读双向同步并发幂等加固与索引迁移 #85

Merged
wangziqi merged 3 commits from fix/roster-enrollment-sync-hardening into main 2026-08-12 08:20:01 +00:00
Owner

背景

承接 #83(班级花名册与档案报读双向同步)。对合并后的代码做多轮 OCR review + 修复循环(12 轮迭代,最后一轮本任务范围 0 评论),主要修复并发幂等/死锁/数据一致性问题。

改动

fix(server): 花名册报读同步并发幂等加固

  • ensureEnrollmentsForRoster:普通读 + 分块批量插入(50/块),批内撞 active 部分唯一索引时逐行回退(1062 跳过);missing 去重并按 studentId 排序,避免多行 INSERT 行序不同互相等待死锁
  • courseCategory 仅映射 culture/professional/comprehensive(与前端 handleClassChange 一致,bootcamp/sprint 不再写入非法值);角色类型改用 TeacherRoleType
  • 导入路径恢复全量同步(不剔除 alreadyAdded):并发事务(如钉钉导入/建班)可能不建报读,剔除会导致成员永久缺报读;重复由唯一索引 + 1062 捕获兜底
  • archive.addEnrollment/updateEnrollment 保存撞唯一索引转 400(新增 saveEnrollmentWithDupGuard),并补 2 个单测
  • isDuplicateEntryError 提取到 common/db-errors.ts 单一来源(queries re-export 保持兼容)
  • 修复 classes.membership.spec(mock dataSource.transaction + manager,断言新返回结构);addStudents 空输入早返回补 enrollmentsCreated: 0

feat(server): 报读表索引迁移与存量重复清洗

  • 1786515492285-AddIndexToStudentEnrollments(class_id, student_id) 普通索引(NON_UNIQUE=1 守卫 + 1061/1091 并发幂等)
  • 1786517000000-AddActiveUniqueToStudentEnrollmentsactive 部分唯一函数索引(MySQL 8.0.13+ 版本守卫;仅 active 且 class_id 非空的行唯一,archived 多条/无班报读不受影响)
    • up:GET_LOCK 串行化 → 清洗存量重复 active(保留最早、其余置 archived,keep_enrollment_id 备份到 student_enrollments_active_cleanup 可回滚)→ 有界 3 次重试
    • down:三级恢复守卫(仍 archived + 花名册 active + 同班无 up 后新建报读);未恢复行保留备份表供人工排查

fix(admin): 报读操作统一失效档案与班级查询缓存

  • 4 个 mutation(增/改/归档/永久删)统一失效 ['archive', studentId] + queryKeys.classes.all(抽 ENROLLMENT_INVALIDATE 常量),避免班级花名册读到陈旧数据;操作列拍平嵌套三元

验证

  • server:classes + archive.enrollment-roster 共 98 个 spec 全过;typecheck / eslint 干净
  • admin:typecheck / oxlint 干净
  • 迁移本地实跑:up → down → up 幂等循环;重复 active 插入被拒(1062);恢复守卫 6 个场景(标准 revert / A→A2 / 离班 / >2 行重复组 / keep_id=0 旧表升级 / 重新激活非重复行)实测通过
  • 数据库:MySQL 8.4.10(函数索引需生产 ≥ 8.0.13)
## 背景 承接 #83(班级花名册与档案报读双向同步)。对合并后的代码做多轮 OCR review + 修复循环(12 轮迭代,最后一轮本任务范围 0 评论),主要修复并发幂等/死锁/数据一致性问题。 ## 改动 ### fix(server): 花名册报读同步并发幂等加固 - `ensureEnrollmentsForRoster`:普通读 + 分块批量插入(50/块),批内撞 active 部分唯一索引时逐行回退(1062 跳过);`missing` 去重并按 studentId 排序,避免多行 INSERT 行序不同互相等待死锁 - `courseCategory` 仅映射 culture/professional/comprehensive(与前端 handleClassChange 一致,bootcamp/sprint 不再写入非法值);角色类型改用 `TeacherRoleType` - 导入路径恢复**全量同步**(不剔除 alreadyAdded):并发事务(如钉钉导入/建班)可能不建报读,剔除会导致成员永久缺报读;重复由唯一索引 + 1062 捕获兜底 - `archive.addEnrollment/updateEnrollment` 保存撞唯一索引转 400(新增 `saveEnrollmentWithDupGuard`),并补 2 个单测 - `isDuplicateEntryError` 提取到 `common/db-errors.ts` 单一来源(queries re-export 保持兼容) - 修复 `classes.membership.spec`(mock dataSource.transaction + manager,断言新返回结构);`addStudents` 空输入早返回补 `enrollmentsCreated: 0` ### feat(server): 报读表索引迁移与存量重复清洗 - `1786515492285-AddIndexToStudentEnrollments`:`(class_id, student_id)` 普通索引(NON_UNIQUE=1 守卫 + 1061/1091 并发幂等) - `1786517000000-AddActiveUniqueToStudentEnrollments`:**active 部分唯一函数索引**(MySQL 8.0.13+ 版本守卫;仅 active 且 class_id 非空的行唯一,archived 多条/无班报读不受影响) - up:GET_LOCK 串行化 → 清洗存量重复 active(保留最早、其余置 archived,`keep_enrollment_id` 备份到 `student_enrollments_active_cleanup` 可回滚)→ 有界 3 次重试 - down:三级恢复守卫(仍 archived + 花名册 active + 同班无 up 后新建报读);未恢复行保留备份表供人工排查 ### fix(admin): 报读操作统一失效档案与班级查询缓存 - 4 个 mutation(增/改/归档/永久删)统一失效 `['archive', studentId] + queryKeys.classes.all`(抽 `ENROLLMENT_INVALIDATE` 常量),避免班级花名册读到陈旧数据;操作列拍平嵌套三元 ## 验证 - server:classes + archive.enrollment-roster 共 98 个 spec 全过;typecheck / eslint 干净 - admin:typecheck / oxlint 干净 - 迁移本地实跑:up → down → up 幂等循环;重复 active 插入被拒(1062);恢复守卫 6 个场景(标准 revert / A→A2 / 离班 / >2 行重复组 / keep_id=0 旧表升级 / 重新激活非重复行)实测通过 - 数据库:MySQL 8.4.10(函数索引需生产 ≥ 8.0.13)
wangziqi added 3 commits 2026-08-12 08:19:31 +00:00
- ensureEnrollmentsForRoster:普通读 + 分块批量插入,批内撞 active 部分唯一索引
  时逐行回退(1062 跳过),missing 去重并按 studentId 排序防死锁;
  courseCategory 仅映射 culture/professional/comprehensive,角色类型用枚举
- 导入路径恢复全量同步(不剔除 alreadyAdded,避免钉钉导入/建班等不同步
  路径导致漏建报读)
- archive add/updateEnrollment 保存 1062 转 400(saveEnrollmentWithDupGuard),
  并补 2 个单测
- isDuplicateEntryError 提取到 common/db-errors.ts 单一来源(queries re-export)
- 修复 classes.membership.spec:mock dataSource.transaction + manager,
  断言更新为新返回结构;addStudents 空输入早返回补 enrollmentsCreated: 0
- AddIndexToStudentEnrollments:student_enrollments 补 (class_id, student_id)
  普通索引,加速幂等查找(NON_UNIQUE=1 守卫 + 1061/1091 并发幂等)
- AddActiveUniqueToStudentEnrollments:active 部分唯一函数索引(MySQL 8.0.13+
  版本守卫),闭合并发重复报读;up 前 GET_LOCK 串行化 + 清洗存量重复 active
  (保留最早、其余置 archived,keep_enrollment_id 备份可回滚),有界重试;
  down 三级恢复守卫(archived + 花名册 active + 无 up 后新建报读),
  未恢复行保留备份表供人工排查
fix(admin): 报读操作统一失效档案与班级查询缓存
CI / check (pull_request) Failing after 40s
Dependency Check / check (pull_request) Failing after 34s
74fb09bdba
报读增删改/归档都会经 syncRosterMembership 变更班级花名册,4 个 mutation
统一失效 ['archive', studentId] + queryKeys.classes.all(抽 ENROLLMENT_INVALIDATE
常量),避免班级详情/花名册读到陈旧数据;操作列拍平嵌套三元。
wangziqi merged commit 74fb09bdba into main 2026-08-12 08:20:01 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wangziqi/gongxue-base#85