导读(3行收益):作业系统最常见的三个乱象:学生重复交、老师批改被覆盖、成绩和成绩单对不上,每个都在消耗老师的时间。根源是状态靠业务字段推断,没有一张状态机。本文给作业实例的状态机设计与提交、批改、回写三处的幂等实现,附迁移表与 SQL。看完能直接理顺你的作业流程。
一、作业系统为什么乱
作业系统常见的「乱」:
- 重复提交:学生提交按钮多点几次,或断网重传,生成多条作业记录;
- 批改覆盖:两位老师同时批同一份作业,后写的把先写的盖掉;
- 成绩对不上:作业成绩和期末成绩单口径不一致,回写时又重复累加。
三个问题的根源:作业状态散落在业务字段里(is_submitted、score 是不是 null……),谁都能改,改了没有约束。正确的做法是把作业做成一条带状态机的记录,任何状态变化都走固定迁移。
二、作业实例状态机设计
先定义状态与迁移,再写代码:
CREATE TABLE homework_instance (
id BIGINT PRIMARY KEY,
student_id BIGINT NOT NULL,
homework_id BIGINT NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
-- 0草稿 1已提交 2已批改 3已退回 4成绩已回写
file_url VARCHAR(255),
score DECIMAL(5,2),
comment VARCHAR(500),
submit_at DATETIME,
graded_at DATETIME
);
CREATE UNIQUE INDEX uk_student_hw ON homework_instance(student_id, homework_id);
状态迁移规则:
| 当前状态 | 事件 | 目标状态 |
|---|---|---|
| 草稿 | 学生提交 | 已提交 |
| 已提交 | 老师批改打分 | 已批改 |
| 已提交 | 老师退回 | 已退回 |
| 已退回 | 学生重交 | 已提交 |
| 已批改 | 成绩写入成绩单 | 成绩已回写 |
状态落库后,页面按钮的显隐一律按状态渲染,而不是按角色判断,避免「老师能改学生已提交的作业」这类越权操作。
三、提交:防重复提交与补交
提交接口必须幂等:同一学生同一作业只有一条记录,重复提交只更新内容不新增记录,并且只在「草稿/已退回」状态才能提交。
def submit_homework(student_id: int, homework_id: int, file_url: str) -> bool:
affected = db.execute("""
UPDATE homework_instance
SET status = 1, file_url = :url, submit_at = NOW()
WHERE student_id = :sid AND homework_id = :hid
AND status IN (0, 3) -- 只有草稿或已退回可提交
""", url=file_url, sid=student_id, hid=homework_id).rowcount
return affected == 1
避坑:补交要保留提交次数与历史版本,submit_count 单独计数,状态迁移不受补交影响;防重靠条件更新而不是先查后写。
四、批改:并发保护
两位老师同时批同一份作业,后提交的会覆盖先提交的。用「乐观锁 + 批改人标记」解决:
UPDATE homework_instance
SET status = 2, score = :score, comment = :comment,
graded_at = NOW(), version = version + 1
WHERE id = :id AND status = 1 AND version = :expect_version;
# 乐观锁:version 不匹配说明已被别人批改,返回冲突让老师重载
row = db.execute(
"SELECT version FROM homework_instance WHERE id=? AND status=1", hw_id
)
if not row:
return "作业不在可批改状态"
affected = db.execute(
"UPDATE homework_instance SET status=2, score=?, comment=?, version=version+1 "
"WHERE id=? AND status=1 AND version=?",
score, comment, hw_id, row.version,
).rowcount
return "批改成功" if affected == 1 else "已被其他老师批改,请刷新"
避坑:成绩单回写前先标记「已批改」但不下发,等对账完成再置「成绩已回写」,避免学生看到半截成绩。
五、成绩回写:幂等与对账
成绩从作业表回写到成绩单,必须幂等:按 (student_id, homework_id) 唯一键 upsert,重复回写不重复累加。
INSERT INTO grade_sheet (student_id, homework_id, score, synced_at)
VALUES (:sid, :hid, :score, NOW())
ON DUPLICATE KEY UPDATE score = VALUES(score), synced_at = NOW();
对账思路:回写完成后,比对「已批改但未回写」的数量,应趋近 0;成绩单中该作业的分数与作业表一致。回写建议做成独立任务或消息异步执行,失败重试天然幂等;学生端成绩展示只读「成绩已回写」状态,避免看到半截数据。
六、踩坑清单
- 状态字段与业务字段混用:别用 score IS NULL 推断「未批改」,状态只认 status;
- 提交接口不幂等:重复调用生成多条记录,成绩统计双份;
- 批改无乐观锁:多人协作时后写覆盖先写,丢批改;
- 成绩回写非 upsert:重复任务执行两次,成绩翻倍;
- 退回后学生重交,老师批改入口没解锁:批改界面永远显示旧版本;
- 状态机缺分支:退回、补交、重批改等分支没有对应处理。
七、工程落地建议
作业模块建议先画状态迁移图再写接口,提交/批改/回写三处都做幂等,成绩回写走唯一键 upsert 并每日对账。若自建成本高,可基于成型平台(如乔拓云教育系统)的作业与考试组件起步,重点验证状态流转与成绩单联动。
八、复盘清单(可直接抄走)
- [ ] 提交接口:条件更新 + 唯一索引是否都在;
- [ ] 批改接口:乐观锁 version 是否生效,冲突提示是否清晰;
- [ ] 成绩回写:upsert 唯一键是否覆盖所有入口;
- [ ] 状态机:草稿/已提交/已批改/已退回/成绩已回写五个状态是否都有页面入口;
- [ ] 对账:每日「已批改未回写」数是否为 0。
结语
作业系统的乱,多数不是功能缺失,而是状态没有模型化。一张状态机、三处幂等,就能把重复提交、覆盖批改、成绩翻倍这三个常见事故按下去。本文仅作技术分享,具体功能以各平台官方实时信息为准。