学生从 A 班转到 B 班之后,昨天在 A 班完成的作业应该出现在谁的班级轨迹里?如果系统每次都拿“当前班级”去关联历史作答,答案会变成 B 班。这在查询上很方便,在事实语义上却是错的。
TeachFlow 遇到的不是一个报表筛选问题,而是两个时间问题:
- 现在谁属于哪个班;
- 一条学习事件发生时属于哪个教学语境。
Current 不能倒推 History
Enrollment 表达当前和历史成员关系;Learning Episode、Publication、Assignment 与 Evidence 则保存一次教学活动发生时的上下文。学生转班只会关闭旧关系并打开新关系,不应该搬运已经发生的课堂和作业事实。
图中的关键点是:T2 之后学生的当前班级变成 B,但 E1 和 E2 仍然属于 A 班当时的课程与作业;B 班只能拥有转班后发生的 E3。教师查看历史轨迹时,人数、覆盖率和掌握分布也应使用那个时间点的成员与事件,而不是今天的 Current。
为什么只保存 classroomId 还不够
一个 ID 能说明“属于哪个班”,却不能独立回答:
- 当时的 Enrollment 是否有效;
- 使用的是哪个课程 Publication 和图版本;
- 这是课堂、作业、诊断还是迁移练习;
- 后来是否发生纠正、撤销或重算;
- 查询者现在是否仍有权限查看历史聚合。
因此,TeachFlow 把班级归属放进事件和 Episode 语境,并保留成员时间线。Current 只服务“现在该看什么”,历史查询则按发生时间和冻结引用重建。
外部标准为什么也保留开始与结束
1EdTech OneRoster 的 Enrollment 数据模型包含 user、class、school、role,并提供 beginDate 与 endDate;CSV Binding 1.2.1 又明确 endDate 是排他的结束日期。这个标准并不规定 TeachFlow 的全部实现,但证明“成员关系有有效期”不是项目特例。
类似地,事件溯源模式强调以不可变、按时间排序的事件保留状态变化历史,从而支持审计和过去状态分析。TeachFlow 不是把所有业务都实现成纯 Event Sourcing,但在学习事实与成员变化上采用了相同原则:后来的 Current 不覆盖早先发生的事实。
三种容易出错的实现
只在学生表保存 currentClassId。
转班更新一列后,所有历史查询都会跟着变化。
把转班做成删除旧 Enrollment。
当前查询变简单,但无法解释过去谁有权限、班级人数为何变化。
回填历史 Evidence 的新班级。
看似“数据统一”,实际破坏了原课程、教师和作业责任关系。
TeachFlow 的约束是:导入花名册是增量操作,文件中缺失的人不会被静默退班;退出或转班必须显式写入 inactive。班级轨迹再从成员事件与学习事实构建,不从当前列表倒推。
权限与归属不是一回事
历史证据属于 A 班,不代表所有现在的 A 班教师都天然有权查看学生个人详情。归属回答事实发生在哪里,授权回答当前请求者能看到什么。TeachFlow 的教师班级轨迹只返回聚合,学生个人轨迹保留自己的原始事实;跨班访问由服务端验证并拒绝。
当前证据边界
项目记录和源码包含转班、unknown 保留、班级历史、聚合隔离和轨迹回放检查;但本文固定快照未附带新的 PostgreSQL 与浏览器执行回执。现有范围也没有覆盖学期归档、跨校转学、合班拆班和法规留存,因此只能声明当前单校教学班模型下的时间归属设计。
常见问题与设计边界
学生转到 B 班后,B 班教师需要了解历史怎么办?
可以通过经过授权的学生纵向档案或摘要查看,但不能把历史事件改写成 B 班事实。共享视图和事实归属应分开。
为什么不把每次查询都按时间 join Enrollment?
可以,但要控制复杂度与一致性。冻结 Episode 上下文能减少歧义,成员时间线用于校验和历史聚合,两者职责不同。
学期结束后 Current 怎么办?
Current 可以归档或重建,但历史事件、冻结引用与当时成员关系不能丢失;具体保留周期仍需学校制度确定。
参考资料
- 1EdTech OneRoster 1.2.1 CSV Binding:Enrollments
- 1EdTech OneRoster v1.1 Final Specification
- AWS Prescriptive Guidance:Event sourcing pattern
- TeachFlow
lib/learning-state/classroom-trajectory.ts、scripts/classroom-trajectory-check.mjs、README.md
结论
转班改变的是“从现在开始在哪里学习”,不是“过去在哪里发生”。把 Current 和 History 分开,把成员有效期与事件语境一起保存,系统才能在班级变化之后继续解释同一条学习证据,而不是让今天的一次更新改写昨天的课堂。