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

13 KiB
Raw Blame History

班级成员 · 批量导入参训学员 —— 全流程逻辑说明

生成日期: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、性别、证件等关键字段,且与账号体系完全脱钩,后续业务(门禁、报到核验、登录)会受限制。