chore(sync): 同步当前工作区全部变更(学员学号可编辑并留空自动生成等)

This commit is contained in:
buliping committed 2026-11-10 13:34:14 +08:00
1 parent 01e949093a
commit ee629f2f3e
534 files changed
+72978 -2720

No files matched your search

@@ -0,0 +1,192 @@
# 班级成员 · 批量导入参训学员 —— 全流程逻辑说明
> 生成日期:2026-11-05
> 文档性质:逻辑梳理(不含代码修改、不含技术选型建议)
> 覆盖范围:班级/计划页「参训名单 → 批量导入」入口
> 前端:`build_ui/src/views/train/class/components/EnrollmentPanel.vue`、`build_ui/src/views/train/plan/components/EnrollmentPanel.vue`、`build_ui/src/api/train/enrollment.js`
> 后端:`ruoyi-admin/.../controller/train/TrainEnrollmentController#batchImportEnrollments` → `ruoyi-train/.../service/impl/EnrollmentAppServiceImpl#batchImportEnrollments / processBatchImport / parseExcelToDTOList`
---
## 阶段一:导入模板设计
### 1.1 模板形态与获取方式
| 项目 | 说明 |
|---|---|
| 模板来源 | **系统未提供"下载模板"入口**,参训导入没有模板下载接口;模板结构由后端解析器**隐式约定**(按列下标读取),需线下自行维护 |
| 文件类型 | 前端限制 `.xlsx / .xls`(`accept`),后端不校验扩展名,统一用 POI 按内容解析 |
| 文件大小 | ≤ 10 MB(后端 `MAX_FILE_SIZE = 10 * 1024 * 1024`) |
| 单批行数 | ≤ 500 行(后端 `MAX_BATCH_ROWS`;前端提示"单次最大支持 500 行") |
| 表头行 | 第 1 行视为表头,**直接跳过、不校验内容**;数据从第 2 行开始 |
| 工作表 | 只读取**第 1 个 sheet** |
### 1.2 字段设计(按列下标,而非表头名称)
| 列序 | 字段名 | 含义 | 必填 | 格式要求 | 校验规则 / 默认值 |
|---|---|---|---|---|---|
| A(第 1 列,index 0) | 学员学号 `s_id` | 关联 `train_student.s_id`,同时作为参训档案的 `student_id` | **是** | 纯文本,≤32 字符;须与学员主档学号**完全一致**(区分大小写/空格) | 为空 → 该行被**静默跳过**(不计入总数、不产生错误提示);非空时必须能在主档查到,否则按"自动建档"开关决定失败或建档 |
| B(第 2 列,index 1) | 参训来源 `source_type` | 1=管理员录入 / 2=Excel 批量导入 / 3=内部平台同步 | 否 | 整数 | 空或非数字 → **默认 2** |
| C(第 3 列,index 2) | 住宿需求 `accommodation_required` | 0=不住宿 / 1=需要住宿 | 否 | 整数 | 空或非数字 → **默认 0**;**未做 0/1 范围校验**(其他值原样落库) |
| D(第 4 列,index 3) | 备注 `remark` | 入班备注 | 否 | 文本 | 原样落库(表结构限长 500);**自动建档时会被借用为学员姓名** |
### 1.3 模板解析流程步骤
1. 用户在前端点击「批量导入」,打开对话框(仅"上传文件"+"学员不存在时自动建档"开关两项)。
2. 选择文件后前端**不立即上传**(`auto-upload=false`),只暂存 `file.raw`。
3. 点击「开始导入」→ 组装 `FormData{file, autoCreateStudent}`,POST `/train/plans/{planId}/enrollments/batch`。
4. 服务端校验文件非空、大小 ≤10MB。
5. 对计划加行锁(`SELECT … FOR UPDATE`)并校验计划存在/未删除/未锁定(已完成、已归档不可导入)。
6. POI 解析第 1 个 sheet;从第 2 行起逐行取 4 个单元格 → 组装 DTO。
7. 进入批量处理引擎(阶段二)。
> **关键判断节点**:解析阶段不校验表头文字、不校验列数量。列顺序被人为打乱时,数据会**串列写入**(例如把来源填在 C 列会被当成住宿需求),系统不会报错。
---
## 阶段二:文件上传处理链路
### 2.1 环节顺序与判断分支
```
[1] 前端闸门
├─ 未选文件 → 提示"请先选择要导入的 Excel 文件",中断
└─ 已选文件 → 提交按钮进入 loading,禁止重复点击
[2] 接口入口
├─ 权限 train:enrollment:import(无权限 → 403 提示)
└─ 记录操作日志(业务类型=导入)
[3] 服务端前置校验(任一失败 → 整批终止,不做任何写入)
├─ 文件为 null/空 → 报"Excel导入解析错误"
├─ 文件 > 10MB → 报"导入文件大小超出限制"
├─ 计划不存在/已删除 → 报"培训班不存在"
├─ 计划状态锁定(已完成/已归档)→ 报计划锁定错误
└─ 解析异常(格式损坏、非 Excel 内容)→ 报"Excel导入解析错误"
[4] 解析结果预处理
├─ 解析出的行数 > 500 → 报"单批次导入人数不得超过 500 行"(整批终止)
└─ 读取计划当前参训人数 currentCount(仅读取一次,循环内自增)
[5] 逐行处理(每行独立判定,失败不中断循环)
├─ 学号为空 → 失败行"学员ID不能为空"(正常情况下解析阶段已过滤,不会到达)
├─ 补默认值:来源←2、住宿需求←0
├─ 招生上限:currentCount ≥ recruitNum → 失败行"计划已达到招生上限(N人)"
├─ 学员存在性:
│ ├─ 存在 → 继续
│ ├─ 不存在 + 自动建档开 → 尝试插入最小档案(异常仅记日志,不中断)
│ └─ 不存在 + 自动建档关 → 失败行"学员档案不存在(学号)"
├─ 重复入班检查:按 (planId, studentId) 查已有档案
│ └─ 已存在 → 失败行"学员已在参训名单中(学号)"
└─ 入库:写入 train_enrollment(状态=0 待报到)
├─ 成功 → successCount++、currentCount++
├─ 唯一键冲突(并发)→ 失败行"并发重复入班(学号)"
└─ 其他异常 → 失败行"插入失败(原因)"
[6] 结果返回
└─ {totalCount, successCount, failureCount, errorList[]}
[7] 前端反馈
├─ 成功响应 → 仅提示 msg("操作成功")+ 关闭弹窗 + 刷新列表
└─ 整批异常 → 全局错误提示,弹窗保持打开可重试
```
### 2.2 各环节关键判断与结果分支
| 环节 | 关键判断 | 成功分支 | 失败分支 |
|---|---|---|---|
| 文件校验 | 空文件 / >10MB | 继续解析 | 整批失败,无数据写入 |
| 计划校验 | 计划存在且状态可编辑 | 继续 | 整批失败 |
| Excel 解析 | 可被 POI 打开 | 得到 DTO 列表 | 整批失败 |
| 行数校验 | ≤500 行 | 继续 | 整批失败 |
| 行内校验 | 学号非空 / 招生未满 / 学员存在 / 未重复入班 | 进入入库 | 记入 `errorList`,该行跳过 |
| 入库 | 数据库约束(唯一键、外键) | 成功计数 | 记入 `errorList`,该行跳过 |
### 2.3 事务与反馈的关键事实
- 方法整体标注了事务(`rollbackFor = Exception`),但**逐行 try/catch 捕获了行内异常**,因此:**前置类异常 → 整批不落库;行内异常 → 已成功行照常提交(部分成功)**。
- 前端目前**只展示 `msg`,未展示 `data.errorList`**:失败行的具体原因(第几行、什么错)后端已返回但界面不呈现,用户只能通过"人数没对上"或重新核对推断。
- **错误行号不是 Excel 真实行号**:`rowNum = 数据序号 + 1`,而解析阶段会跳过空行,故提示"第 1 行"通常对应 Excel 的第 2 行;存在空行时偏差更大。
---
## 阶段三:学员自动建档流程
### 3.1 触发条件
- 前端开关「学员不存在时自动建档」为开(默认开)→ 请求参数 `autoCreateStudent=true`;
- 且该行学号在 `train_student` 中**查不到**(按 `s_id` 精确查询)。
### 3.2 自动创建档案写入的字段与默认值
| 字段 | 取值来源 / 默认值 |
|---|---|
| `s_id`(主键) | 直接使用 Excel 学号 |
| `s_name` | 备注列非空 → **用备注作为姓名**;否则 `导入学员_<学号>` |
| `dept_name`(选派单位) | 取 DTO 的 `deptName`,但 Excel 解析未映射该列 → **实际为 null** |
| `card_no` | 空字符串(按"不可空"约束兜底写入) |
| `source_type` | 2(Excel 批量导入) |
| `status` | 1(正常) |
| `del_flag` | 0(存在) |
| `create_by` / `create_time` | 当前操作人 / 当前时间 |
| **未写入** | 性别、出生日期、手机、邮箱、`person_no`(人员编号)、身份证密文/哈希/后四位、头像、`face_ref`、`user_id` |
### 3.3 与班级成员关系的建立方式
- 建档与入班是**两步、两次写入**:先 `train_student`(主档),再 `train_enrollment`(`plan_id + student_id`)。
- 入班关系的唯一性由 **`UNIQUE(plan_id, student_id)`** 保证;主档与参训档案之间由 **外键 `fk_enrollment_student`** 约束(学号必须真实存在于主档)。
- 入班后初始状态固定为 **0 待报到**,`accommodation_required` 取模板 C 列(默认 0 不住宿),`source_type` 取 B 列(默认 2)。
### 3.4 与系统账号的关联规则
- **不建立任何账号关联**:自动建档**不创建 `sys_user`**、**不写 `user_id`**、不生成登录账号与初始密码。
- 因此自动建档出来的学员只能作为业务档案存在(可被排课、点名、住宿、成绩匹配),**无法登录系统**;如需登录账号,须走"学员新增(带账号)"渠道补建。
### 3.5 去重与幂等策略
| 层级 | 机制 |
|---|---|
| 学员主档 | 以 `s_id` 为主键去重:已存在则**直接复用**,不会重复建档 |
| 参训关系 | ① 前置查询 `(planId, studentId)` 是否已存在;② 数据库唯一约束兜底并发 |
| 重复导入同一文件 | 首行起全部命中"学员已在参训名单中",不产生重复数据(幂等) |
| 自动建档并发 | 无显式锁;两个并发导入同一新学号时,一侧 insert 成功,另一侧 insert 失败被吞(仅日志),随后其参训入库由唯一键/外键判定失败——结果仍不会产生重复数据,但**错误文案不友好** |
| 参训上限并发 | `currentCount` 为循环内自增的内存计数,非原子;多事务并发导入时可能**轻微超过招生上限** |
### 3.6 异常处理与回滚
| 异常点 | 处理 | 是否回滚 |
|---|---|---|
| 自动建档 insert 失败(主键冲突、字段超长等) | 捕获后仅 `warn` 日志,**不中断该行** | 否(内存对象仍非空) |
| 自动建档失败后继续入班 | 因外键 `fk_enrollment_student` 失败 → 该行记"插入失败(...)" | 该行不落库,其余行不受影响 |
| 参训 insert 唯一键冲突 | 记"并发重复入班" | 该行不落库 |
| 参训 insert 其他异常 | 记"插入失败(原因)" | 该行不落库 |
| 前置校验/解析/超行数异常 | 直接抛业务异常 | **整批不落库** |
| 事务整体 | 行内异常被吞,事务照常提交 | 已成功行**不回滚**(设计上为部分成功) |
---
## 核心环节与边界情况
| 边界场景 | 实际行为 | 风险/影响 |
|---|---|---|
| **重复导入** | 全部行判定"已在参训名单中";已建档学员不会重复建档 | 无数据污染,但失败明细前端不可见,用户可能误判为"导入无反应" |
| **部分成功** | 成功行入班、失败行跳过,接口仍返回成功(code=200),`failureCount` 与 `errorList` 描述失败的记录 | 需靠返回体判断,界面仅提示"操作成功" |
| **学号为空的行** | 解析阶段直接跳过,**不计入总数、不报错** | 表尾空行无害;但漏填学号的有效数据会被静默丢弃 |
| **字段缺失** | 来源、住宿需求自动补默认值;备注可空;学号不可空 | 住宿需求缺失一律按"不住宿",后续不会出现在住宿分配工作台 |
| **备注被当姓名** | 自动建档时优先用备注作为 `s_name` | 若备注写的是"第一批补录"等内容,会产生错误姓名,且后续改名前无校验 |
| **自动建档字段不全** | 无性别、无身份证、无人员编号、无手机 | 影响门禁/人脸、报到核验、成绩匹配等依赖完整档案的场景 |
| **表头/列顺序错位** | 按列下标取值,不校验表头 | 数据串列写入且无报错,属最隐蔽的边界问题 |
| **文件伪装/损坏** | 扩展名合规但内容非 Excel → POI 抛错 | 整批失败,提示"Excel导入解析错误" |
| **超行数/超大小** | 500 行 / 10MB 硬限 | 整批失败(非截断处理),需拆分文件 |
| **计划状态锁定** | 已完成/已归档计划禁止导入(前端按钮同时置灰) | 需先解锁或新建计划 |
| **并发导入同一计划** | 计划行锁串行化,唯一键兜底重复 | 可能出现"并发重复入班""插入失败"类文案;招生上限可能被轻微突破 |
| **错误行号偏差** | 报错行号是"数据序号",非 Excel 行号 | 排查时需按"序号+1"换算 |
| **无模板下载** | 模板需线下自备,且系统不校验表头 | 用户易因列顺序不符导致数据错列 |
---
## 建议关注的三处逻辑缺口(仅记录,不涉及改动)
1. **失败明细未回显**:后端已返回 `errorList`(含行号与原因),前端只提示 `msg`,用户无法获知哪些行失败。
2. **模板已隐式耦合列顺序**:无模板下载、无表头校验,列错位不报错。
3. **自动建档为"半成品档案"**:缺 `person_no`、性别、证件等关键字段,且与账号体系完全脱钩,后续业务(门禁、报到核验、登录)会受限制。