Files
ClassHelper/docs/app/imports.md
T
LoveChengke 9d5ac7adf9 feat(shared,server,web,client,plugin)!: 角色与权限重构 + 学期周次 + 课表方向反转(1.2.0)
学生、任课关系、周次口径三处长期靠约定维持的事情,改为由数据模型保证。

学生不是账号
- 拆出独立 Student 表,没有 passwordHash / role / 任何登录字段
- 「学生不能登录」由表结构保证,不再靠「登录前判 role 再 403」的代码分支
- 三张个人数据表外键 userId → studentId;删掉 Enrollment 与 ClassTeacher
- 学生一律称「学号」、教师一律称「工号」,「学生账号」这个概念消失

任课关系唯一来源 = Course(班级 + 科目 + 教师)
- 班主任 = Class.teacherId,科任 = Course.teacherId,同一人两处指向同一账号、不重复建号
- 作业与成绩按科目再收一道:挂科目只由该科任课老师改,没挂科目归班主任 / 管理员
- 新增「任课教师分配」与「班级教师批量导入」(带「使用工号匹配」开关)

成绩与名单
- 成绩等级:百分制 / 等级制 A·B·C·D / 自定义,支持单个与批量改等级
- 按学号查成绩明细(教师端 + 班级端 + 设备令牌),受按班开关控制
- 调班 / 转出(单个与批量)+ 可查历史;学号不变、历史作业成绩留原归属
- 毕业归档页(按年度记录毕业班级 / 毕业生 / 转出学生 / 该届通知与作业)
- 学年升级(未毕业不归档、数据沿用);班级称呼改为「2026级1班」

ClassHelper 班级端在线状态
- Web 端显示 班级 / 设备名 / 设备号 / 在线离线 / 最后在线 / 最后心跳
- lastHeartbeatAt 驱动 60 秒在线窗口,与只表示「带状态上报」的 lastSeenAt 分开

学期周次
- 新增 TermWeek(classId 空串 = 全校默认),管理员可用日期范围选择器逐周配置
- 一键按开学日期生成整学期;联网拉取调休建议(只给建议、不改数据,无外网失败属正常)
- 改周次后随课表下发给教室的 ClassHelper 班级端

课表方向反转
- 主方向改为「服务端 → 教室」自动下发;教室不再自动回传
- 取教室课表改为手动动作(点击前弹警告),插件按待办标记在下一次心跳补报

改名
- 班级小助手 → ClassHelper:界面、窗口标题、托盘、灵动岛、EXE、安装包、
  安装目录、systemd 描述、插件清单与设置页、官网、文档站、README

BREAKING CHANGE: 产品名与 EXE 名变更(班级小助手.exe → ClassHelper.exe),
安装目录随之改变,不支持覆盖安装,需先卸载旧版。
服务端首次启动会自动应用 6 个数据库迁移(学生行按 id 原样搬迁,历史数据不丢),
升级前请先备份。
2026-10-06 19:41:18 +08:00

116 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 表格与课表导入
description: 成绩、学生名单、教师名单的表格导入,以及从 ClassIsland 档案导入节次时间与课表。
---
# 表格与课表导入
五条导入链路**都在服务端完成解析、校验与去重**,前端只负责收集文件与展示结果。
所以 Web 端、客户端与直接调接口拿到的结果是同一套规则、同一份统计。
## 通用流程
表格类导入都是三段式(成绩 / 学生 / 教师都一样):
1. **下载模板** —— `GET /imports/template?kind=grades&format=xlsx|csv`。
CSV 带 BOM,Excel 直接双击打开不乱码。
2. **上传预览** —— 服务端返回列名、前 20 行、总行数与**建议的字段映射**,以及校验问题。
列名按同义词自动映射(「用户名→学号」「得分→分数」「满分→总分」「课程→科目」……),也可以手动改。
**缺必填列不会静默通过**,会直接给出「缺少必填列「…」」。
3. **确认导入** —— 按映射与写入模式提交:
| 模式 | 已存在的记录 |
| -------- | ------------ |
| `upsert` | 更新(默认) |
| `append` | 跳过 |
返回 `{ total, inserted, updated, skipped, failed, errors[], warnings[] }`。
`errors[].row` 是 **Excel 视角的行号(含表头)**,方便老师改完再导。
**体积限制**:文件上限 **8MB**,请求体上限 12MB(文件以 base64 放进 JSON,约 4/3 倍)。
超限返回 413 `IMPORT_TOO_LARGE` 与中文提示 —— 如果你看到一句英文的「request entity too large」,
那说明这个异常没被映射,属于 bug。
## 1. 成绩表格导入
- 入口:成绩录入 → 「导入表格」
- 模板列:`学生学号 / 学生姓名 / 考试名称 / 分数 / 总分 / 科目 / 等级`
- 支持 `.xlsx / .xls / .csv / .tsv`(自动识别分隔符)
- **重复判定**:同班级 + 同学生 + 同考试 + 同课程
- 学生按「用户名」优先、「姓名」兜底匹配
## 2. 学生名单导入
- 入口:学生管理 → 「导入名单」(仅管理员)
- 模板列:`学号 / 姓名 / 班级 / 性别 / 家长手机号` —— 学生**不是账号**,没有密码列
- 用户名必须匹配 `^[A-Za-z0-9_.-]{3,32}$`
- 用户名已经被某个教师 / 管理员账号占用时,**这一行进错误列表**,不会覆盖他人账号
## 3. 教师名单导入
- 入口:教师管理 → 「导入名单」(仅管理员)
- 模板列:`工号 / 姓名 / 手机号 / 初始密码 / 角色`
- 角色取 `ADMIN` / `TEACHER`,留空按 `TEACHER`
- 与新建教师同一套校验,导入完可以直接登录
## 4. 从 ClassIsland 导入课程表
**这是最省事的一条路**:如果教室里已经在用 ClassIsland,直接导入它的档案 JSON,
课表、节次时间、单双周会一起进来,不用排两遍。
- 入口:课表管理 → 「导入 ClassIsland 课程表」
- 输入:同一份档案 JSON(`ClassPlans` / `TimeLayouts` / `Subjects` 三个字典),
粘贴或选择 `.json` 文件
### 真实档案的形态
ClassIsland 2.x 的档案在 `Profiles/<档案名>.json`,字段是 PascalCase:
```jsonc
{
"TimeLayouts": {
"<guid>": { "Name": "…", "Layouts": [{ "StartTime": "08:00:00", "EndTime": "08:45:00", "TimeType": 0 }] },
},
"ClassPlans": { "<guid>": { "Name": "…", "Classes": [/* 与上课时间点逐位对齐 */] } },
"Subjects": { "<guid>": { "Name": "语文" } },
}
```
解析器同时兼容顶层数组、`{ TimeLayouts: [...] }`、`{ items | Items | Layouts | TimeLayout: [...] }`
与单个时间表对象。
| 细节 | 说明 |
| ------------ | ----------------------------------------------------------------------------------------------------------------------- |
| 时间字段 | `StartTime` / `EndTime`(`"08:00:00"`);旧版 `StartSecond` / `EndSecond`(ISO 字符串或秒数);`"8:0"` / `"08:00"` 都认 |
| `TimeType` | `0=上课`、`1=课间`、`2=分割线`、`3=行动`,与 ClassIsland 一致 |
| **节次对齐** | `ClassPlan.Classes[i]` 对应 `Layouts` 里**第 i 个 `TimeType === 0` 的时间点**(课间、分割线不占位) |
| 单双周 | `TimeRule.WeekCountDiv`(`0=每周`、`1=单周`、`2=双周`)+ `WeekCountDivTotal`(默认 2) |
| 星期换算 | ClassIsland 是 `0=周日 … 6=周六`,导入时换算成 `1=周一 … 7=周日` |
| 科目映射 | `Subjects[<guid>].Name`;该班还没有的科目**自动补建课程**(任课老师取该班班主任),预览里会提前列出 |
| 写入模式 | `replace` 清空该班课表后写入(事务内完成)/ `merge` 按「星期 + 开始时间 + 单双周」去重 |
**解析出 `errors` 时返回 400 且数据库完全不写入**(`IMPORT_INVALID` / `IMPORT_EMPTY`)——
先保证不写坏,再谈导入。
时间重叠、分割线没有时间这类问题只作 `warnings`,不阻断导入。
> **3 周及以上的轮换**没法用单双周表达,会给出 warning 并按"每周"处理。
## 5. 只导入节次时间(旧入口)
「导入时间配置」按钮**已经下线** —— 因为同一份档案 JSON 里的 `TimeLayouts` 已经跟着课表一起进来了。
组件与接口(`/imports/time-layout/*`)仍然保留给旧脚本调用。
它的写入模式也是 `replace`(整体替换该班同名配置)与 `merge`(以**开始时间**为槽位标识合并,
同槽位被导入项覆盖,其它旧节次保留,不静默丢数据)。
## 相关接口
| 路径 | 说明 |
| ------------------------------------------------------ | ------------------------------------------ |
| `/imports/template?kind=&format=` | 下载模板(`csv` 走 JSON,`xlsx` 走二进制) |
| `/imports/table/preview` | 上传解析预览(base64) |
| `/imports/table/commit` | 按映射与模式导入 |
| `/imports/class-plan/preview`、`/imports/class-plan` | ClassIsland 课程表预览 / 导入 |
| `/imports/time-layout/preview`、`/imports/time-layout` | 时间配置预览 / 导入(旧入口) |