班级成员 · 批量导入参训学员 —— 全流程逻辑说明
生成日期: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 模板解析流程步骤
- 用户在前端点击「批量导入」,打开对话框(仅"上传文件"+"学员不存在时自动建档"开关两项)。
- 选择文件后前端不立即上传(
auto-upload=false),只暂存 file.raw。
- 点击「开始导入」→ 组装
FormData{file, autoCreateStudent},POST /train/plans/{planId}/enrollments/batch。
- 服务端校验文件非空、大小 ≤10MB。
- 对计划加行锁(
SELECT … FOR UPDATE)并校验计划存在/未删除/未锁定(已完成、已归档不可导入)。
- POI 解析第 1 个 sheet;从第 2 行起逐行取 4 个单元格 → 组装 DTO。
- 进入批量处理引擎(阶段二)。
关键判断节点:解析阶段不校验表头文字、不校验列数量。列顺序被人为打乱时,数据会串列写入(例如把来源填在 C 列会被当成住宿需求),系统不会报错。
阶段二:文件上传处理链路
2.1 环节顺序与判断分支
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"换算 |
| 无模板下载 |
模板需线下自备,且系统不校验表头 |
用户易因列顺序不符导致数据错列 |
建议关注的三处逻辑缺口(仅记录,不涉及改动)
- 失败明细未回显:后端已返回
errorList(含行号与原因),前端只提示 msg,用户无法获知哪些行失败。
- 模板已隐式耦合列顺序:无模板下载、无表头校验,列错位不报错。
- 自动建档为"半成品档案":缺
person_no、性别、证件等关键字段,且与账号体系完全脱钩,后续业务(门禁、报到核验、登录)会受限制。