chore: 合并为单仓库,统一部署流程

- 根目录加入 docker-compose、Caddyfile、文档等原本不在仓库里的文件
- 前后端两份 GitHub Actions 合并成根目录的一个 deploy workflow:
  构建前端上传到 build/,再在服务器上 git pull 并重建容器

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 16:02:12 +08:00
1 parent 70983a12a5
commit 47d4679984
40 files changed
+13723 -34

No files matched your search

@@ -0,0 +1,262 @@
# 平时成绩册设计
**日期**: 2026-05-01
**状态**: 待用户复核
## 背景
期末需要在管理后台查看单个班级学生的平时成绩。现有系统已有学生班级字段、教程/挑战任务、提交记录、提交评分和单题统计弹窗,但缺少跨教程和挑战的班级成绩册。
本功能在管理后台新增“平时成绩”页面,面向管理员按班级查看、排序、分级和导出学生平时成绩。
## 目标
- 管理员可以选择一个班级查看该班平时成绩。
- 教程和挑战都计入成绩,不只统计挑战。
- 同一学生同一任务按最高分提交计算。
- 任务是否计入成绩由该班提交覆盖率决定,不依赖 `Task.is_public`。
- 页面显示排名、等级、每个任务得分、教程合计、挑战合计、总分、平均分、已提交任务数和未提交任务数。
- 支持导出当前班级成绩 CSV。
## 不在本期范围
- 不提供“全部班级”视图。
- 不做教程和挑战权重配置。
- 不做人工调整等级。
- 不新增持久化成绩快照,成绩册按当前提交和评分实时计算。
- 不修改现有 `Submission.score` 或 `Rating` 计算逻辑。
## 成绩计算规则
### 学生范围
成绩册必须选择一个班级。学生范围为:
- `User.role = normal`
- `User.classname = classname`
如果未传班级,接口返回 `400`,提示“请选择班级”。
### 任务范围
教程和挑战都可以纳入成绩。页面提供任务类型筛选:
- 全部
- 教程
- 挑战
任务纳入成绩的默认规则:
- 只看当前班级学生。
- 某任务只要该班至少 50% 学生有过提交,就计入成绩。
- 覆盖率门槛人数为 `ceil(班级学生数 * 0.5)`。
- 不使用 `Task.is_public` 判断是否计入,因为它只表示公开状态。
页面提供“显示所有有提交任务”的开关:
- 默认关闭,只显示计入成绩的任务。
- 开启后显示当前班级有提交记录的所有任务,用于检查任务是否未达到覆盖率门槛。
- 开启后每个任务仍返回 `included` 标记;汇总字段只计算 `included = true` 的任务。
### 单元格分数
每个学生每个任务的单元格分数为:
- 该学生在该任务所有提交中最高的 `Submission.score`。
- 如果没有提交,显示缺交,汇总时按 `0` 计入。
如果多个提交同分,提交详情链接指向最近创建的一条最高分提交。
### 汇总字段
每个学生行包含:
- `tutorial_total`: 教程最高分合计。
- `challenge_total`: 挑战最高分合计。
- `total_score`: 教程合计 + 挑战合计。
- `average_score`: `total_score / included_task_count`,没有计入任务时为 `null`。
- `submitted_task_count`: 有提交的计入任务数。
- `missing_task_count`: 没有提交的计入任务数。
## 等级规则
等级只在当前班级内计算。按 `total_score` 降序排名,同分按 `username` 升序稳定排序。
设班级人数为 `N`,排名从 1 开始:
- `A`: `rank <= ceil(0.30 * N)`
- `B`: `rank <= ceil(0.70 * N)`
- `C`: `rank <= ceil(0.90 * N)`
- `D`: `rank <= ceil(0.95 * N)`
- `E`: 其余
返回字段包含:
- `rank`
- `grade`
## 后端设计
### API
新增管理员接口:
```http
GET /api/submission/gradebook/
```
参数:
- `classname`: 必填,班级名。
- `task_type`: 可选,`tutorial` / `challenge`。
- `username`: 可选,按用户名片段搜索学生。
- `include_all_tasks`: 可选布尔值,默认 `false`。
权限:
- 仅 `admin` 和 `super` 可以访问。
### 返回结构
```ts
interface GradebookTask {
id: number
display: number
title: string
task_type: "tutorial" | "challenge"
submitted_count: number
coverage: number
included: boolean
}
interface GradebookCell {
score: number
submitted: boolean
submission_id: string | null
}
interface GradebookRow {
user_id: number
username: string
classname: string
rank: number
grade: "A" | "B" | "C" | "D" | "E"
scores: Record<number, GradebookCell>
tutorial_total: number
challenge_total: number
total_score: number
average_score: number | null
submitted_task_count: number
missing_task_count: number
}
interface GradebookOut {
classname: string
classes: string[]
task_count: number
included_task_count: number
student_count: number
coverage_threshold_count: number
tasks: GradebookTask[]
rows: GradebookRow[]
}
```
### 查询策略
1. 查询班级列表,用于前端选择器。
2. 查询当前班级普通学生,得到 `student_ids` 和 `student_count`。
3. 查询这些学生所有提交涉及的任务,按 `task_type` 过滤。
4. 对每个任务统计有提交的学生数,判断是否达到 50% 覆盖率。
5. 根据 `include_all_tasks` 决定返回任务集合。
6. 查询 `(user_id, task_id)` 的最高分提交。
7. 组装每个学生的任务分数和汇总字段。
8. 按 `total_score desc, username asc` 排名并分配等级。
## 前端设计
### 路由和入口
在管理后台 `/dashboard` 下新增页面:
```text
/dashboard/gradebook
```
侧边栏或管理入口新增“平时成绩”。
### 页面布局
顶部筛选区:
- 班级选择,默认选择第一个班级,不提供“全部班级”。
- 任务类型:全部 / 教程 / 挑战。
- 学生搜索。
- “显示所有有提交任务”开关,默认关闭。
- 导出 CSV 按钮。
主体表格:
- 左侧固定列:排名、等级、学生、班级。
- 中间动态列:每个任务一列,显示最高分。
- 右侧固定汇总列:教程合计、挑战合计、总分、平均分、已提交、缺交。
显示规则:
- 缺交单元格在页面显示 `缺交`,使用轻微警示样式;CSV 中按 `0` 导出。
- 未计入成绩但被显示的任务列,用较弱样式并在表头标注未计入。
- 点击有提交的分数,打开该最高分提交详情页。
## 导出设计
新增导出接口:
```http
GET /api/submission/gradebook/export/
```
参数与成绩册接口一致。
CSV 列:
- 排名
- 等级
- 用户名
- 班级
- 每个任务得分
- 教程合计
- 挑战合计
- 总分
- 平均分
- 已提交任务数
- 未提交任务数
导出内容与当前页面筛选保持一致。
## 错误处理
- 未登录:沿用现有登录拦截。
- 非管理员访问:返回 `403`。
- 未选择班级:返回 `400`。
- 班级不存在或没有学生:返回空表格,并保留班级选择列表。
- 当前班级没有达到覆盖率门槛的任务:表格仍显示学生和汇总列,任务列为空,汇总为 0 或 `null`。
## 测试计划
后端测试:
- 任务覆盖率达到 50% 时被计入,低于 50% 时不计入。
- 教程和挑战都能参与计算。
- 同一学生同一任务取最高分。
- 缺交任务按 0 计入,并增加缺交数。
- A/B/C/D/E 按累计 `ceil` 边界分配。
- `classname` 缺失返回 `400`。
- 普通用户访问返回 `403`。
前端检查:
- 页面默认选择第一个班级并加载成绩。
- 任务类型筛选正确刷新表格。
- “显示所有有提交任务”能显示未计入任务。
- 点击分数能打开对应提交详情。
- CSV 导出与当前筛选一致。