Files
railwaysystem/docs/reports/enrollment-batch-import-logic.md
T

193 lines
13 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.
# 班级成员 · 批量导入参训学员 —— 全流程逻辑说明
> 生成日期: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`、性别、证件等关键字段,且与账号体系完全脱钩,后续业务(门禁、报到核验、登录)会受限制。