feat: 改版阶段 3:老师按班级 × 任务批改,学生看本班同学作品

老师
- 新的“批改”页(教学默认页):左边班级 + 任务列表和完成进度,中间作品墙
  (每人最新一版、未交 / 我未评 / 分数),右侧详情面板:各版本、提示词链、打分、
  加入课上展示、加入作品展;J/K 切换学生,1–5 打分,P 课上展示
- “开始讲评”全屏依次播放课上展示的作品;成绩册点分数打开同一个详情面板
- 四色标记和“一键清除全站标记”去掉,flag 只用 present(课上展示);全部提交只给超管

学生
- “同学作品”:只看本班、自己交过这道题才开放,打分前匿名(服务端不返回作者)
- 学生的提交列表只有自己的;别人的提交和提示词链只有本班同学交过同一题、或进了作品展才能看
- 随机互评不再返回作者,打完分由 PUT /:id/score 返回

服务端新增 routes/review.ts(/submission/review:options、progress、class、peers),
查询走 slim 覆盖索引

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-27 00:10:18 +08:00
1 parent 9c6c01eba1
commit df3f842dac
18 files changed
+1616 -275

No files matched your search

+71
View File
@@ -493,3 +493,74 @@ describe("任务素材", () => {
}
})
})
describe("同学作品与批改", () => {
test("学生只能看自己的、作品展里的、本班同学在自己也交过的题上的提交", async () => {
const me = makeUser("me", "normal", "c1")
const mate = makeUser("mate", "normal", "c1")
const other = makeUser("other", "normal", "c2")
const task = makeTask("T")
const mateSub = makeSubmission(mate, task)
const otherSub = makeSubmission(other, task)
const stu = await client("me")
expect((await stu("GET", `/api/submission/${uuid(mateSub)}`)).status).toBe(403)
expect((await stu("GET", `/api/submission/${uuid(mateSub)}/prompt-chain`)).status).toBe(403)
const locked = await stu("GET", `/api/submission/review/peers/${task}`)
expect(locked.json).toEqual({ locked: true, items: [] })
makeSubmission(me, task)
expect((await stu("GET", `/api/submission/${uuid(mateSub)}`)).status).toBe(200)
// 外班的仍然看不到
expect((await stu("GET", `/api/submission/${uuid(otherSub)}`)).status).toBe(403)
// 列表里只有自己的
const list = await stu("GET", "/api/submission/")
expect(list.json.items.map((s: any) => s.username)).toEqual(["me"])
expect((await stu("GET", `/api/submission/by-user-task?user_id=${mate}&task_id=${task}`)).status).toBe(403)
})
test("同学作品打分前匿名,打完分才显示作者", async () => {
const me = makeUser("me", "normal", "c1")
const mate = makeUser("mate", "normal", "c1")
makeUser("other", "normal", "c2")
const task = makeTask("T")
makeSubmission(me, task)
const mateSub = makeSubmission(mate, task)
const stu = await client("me")
let peers = await stu("GET", `/api/submission/review/peers/${task}`)
expect(peers.json.items).toHaveLength(1)
expect(peers.json.items[0]).toMatchObject({ id: uuid(mateSub), username: null, score: null, my_score: 0 })
const rated = await stu("PUT", `/api/submission/${uuid(mateSub)}/score`, { score: 4 })
expect(rated.json.username).toBe("mate")
peers = await stu("GET", `/api/submission/review/peers/${task}`)
expect(peers.json.items[0]).toMatchObject({ username: "mate", my_score: 4 })
const random = await (await client("other"))("GET", "/api/submission/random-for-rating")
expect(random.json.username).toBeUndefined()
})
test("老师按班级 × 任务看作品墙;课上展示用 flag", async () => {
makeUser("t", "admin")
const a = makeUser("a", "normal", "c1")
makeUser("b", "normal", "c1")
const task = makeTask("T")
makeSubmission(a, task, 0, "2026-01-01 00:00:00.000000")
const latest = makeSubmission(a, task, 0, "2026-01-02 00:00:00.000000")
const teacher = await client("t")
expect((await (await client("a"))("GET", `/api/submission/review/class?classname=c1&task_id=${task}`)).status).toBe(403)
const wall = await teacher("GET", `/api/submission/review/class?classname=c1&task_id=${task}`)
expect(wall.json.map((s: any) => [s.username, s.submission?.id ?? null, s.submission?.submit_count])).toEqual([
["a", uuid(latest), 2],
["b", null, undefined],
])
const progress = await teacher("GET", "/api/submission/review/progress?classname=c1")
expect(progress.json).toEqual({ student_count: 2, submitted: { [task]: 1 } })
const options = await teacher("GET", "/api/submission/review/options")
expect(options.json.classes).toEqual(["c1"])
expect((await teacher("PUT", `/api/submission/${uuid(latest)}/flag`, { flag: "present" })).json.flag).toBe("present")
})
})