# 专业合并功能实施计划 > 状态:P0 已落地(待联调验收) > 更新时间:2026-08-18 > 入口页面:`/student/classmsg/majormsg`(专业信息) > 背景:生产环境存在重复专业定义(如「中药」与「中药(技师)」),需将源专业下班级/学生及业务当前归属合并到目标专业。 ### 已落地代码位置 | 项 | 路径 | |----|------| | 预览/合并 API | `POST /xg/major/merge/preview`、`POST /xg/major/merge` | | Controller | `ruoyi-xg/.../XgMajorMergeController.java` | | Service | `ruoyi-xg/.../XgMajorMergeServiceImpl.java` | | 前端 | `web/src/views/xg/xsjcxx/classmsg/majormsg/index.vue` | | DDL | `docs/设计文档/database/专业信息管理/专业合并快照表结构.sql` | | 权限 | `docs/设计文档/database/专业信息管理/专业合并菜单权限.sql` | --- ## 1. 目标与原则 ### 1.1 业务目标 在专业信息界面多选专业 → 点击【专业合并】→ 弹窗从已选中指定**一个目标专业** → 其余源专业的**当前归属数据**全部改挂到目标专业 → 源专业**停用**。 ### 1.2 已确认规则 | 规则 | 结论 | |------|------| | 班级处理 | 班级整体挂到目标专业;**班级 ID 不变**;同名班**不合并** | | 学生处理 | 只改专业归属,**不改班级** | | 源专业处理后 | 本版仅 **停用**(`status=1`),是否逻辑删除后续再定 | | 学院约束 | 仅允许**同一学院**下的专业互相合并 | | 历史快照 | 转班等「原专业/新专业」历史记录 **不回写** | ### 1.3 核心原则(防漏改) **凡表示「当前专业归属」的字段,`major_id` 与 `major_name`(以及存在的 `major_code`)必须成对同步更新。** 禁止只改 ID、不改名称:列表/导出/筛选会出现「ID 已是中药、名称仍显示中药(技师)」的异常。 更新条件统一为: ```text WHERE major_id IN (源专业ID列表) SET major_id = 目标专业ID, major_name = 目标专业名称 -- 若表有 major_code / major_dept_id,一并改为目标专业对应值 ``` --- ## 2. 改造范围总表 ### 2.1 组织与主数据(必改,事务内优先) | 序号 | 表 | 动作 | ID / 名称字段 | |------|-----|------|----------------| | 1 | `sys_dept`(班级节点 `bj-*`) | `parent_id` 改为目标专业;重算 `ancestors` | 无冗余名称 | | 2 | `sys_dept`(源专业 `zy-*`) | `status = '1'`(停用) | `dept_name` 保留作追溯 | | 3 | `xg_class_msg` | 当前归属改挂 | **`major_id` + `major_name` 同改** | | 4 | `xg_student_msg` | 当前归属改挂 | **`major_id` + `major_name` 同改** | | 5 | `xg_student_manager_source` | `data_id` 若等于源专业 ID,改为目标专业 ID | 无名称字段 | 说明:`sys_user.dept_id` 本系统学生通常挂**班级**;合并时班级 ID 不变则用户表一般无需改。合并前应用 SQL 探测是否存在 `dept_id IN (源专业ID)`,有则改为目标专业 ID。 ### 2.2 业务冗余表(必改:当前归属 = 源专业时) 原则:**有 ID 改 ID,有名称改名称,有编码改编码;能按 ID 定位的优先按 ID。** | 模块 | 表 | 同改字段 | 备注 | |------|-----|----------|------| | 请假 | `xg_student_leave` | `major_id` + `major_name` | | | 销假 | `xg_student_cancel_leave` | `major_id` + `major_name` | | | 宿舍申请 | `xg_dorm_application` | `major_id` + `major_name` | | | 床位分配 | `xg_dorm_bed_student` | `major_id` | 无名称字段 | | 卫生评分 | `xg_dorm_hygiene_student_score` | `major_id` + `major_name` | | | 查寝明细 | `xg_dorm_roll_call_detail` | `major_id` + `major_name` | | | 违纪 | `xg_dorm_violation` | `major_id` + `major_name` | | | 假期留宿 | `xg_holiday_stay` | `major_id` + `major_name` | | | 信息收集批次班级 | `xg_stu_col_batch_class` | `major_id` + `major_name` | | | 学生信息批次 | `xg_student_msg_batch` | `major_id` + `major_name` | | | 心理测评 | `xljk_evaluation` | `major_id` + `major_name` | | | 考核申请 | `xskh_assessment_application` | `major_id` + `major_name` | | | 考核审批 | `xskh_assessment_application_approval` | `major_id` + `major_name` | | | 处分 | `xskh_discipline_record` | `major_id` + `major_name` | | | 奖励 | `xskh_reward_record` | `major_id` + `major_name` | | | 贫困认定 | `jcbt_poverty_application` | `major_id` + `major_name`(+`major_code` 若有) | | | 奖学金申请 | `jcbt_scholarship_application` | `major_id` | 无名称字段 | | 勤工助学 | `jcbt_work_study_application` | **仅有 `major_name`** | 见 §2.4 特殊处理 | | 毕业生 | `zsjy_graduate_info` | `major_id` + `major_name` | | | 实习 | `zsjy_internship_info` | `major_id` | 无名称字段 | | 招生专业配置 | `zsjy_enrollment_major` | `major_dept_id` + `major_name` + `major_code` | **特殊**:存在 `uk_major_dept` 唯一键;目标已有配置则**逻辑删除源行**,否则改挂 | ### 2.3 配置 / 权限类(有则改) | 表 | 动作 | |----|------| | `jcbt_scholarship_quota` | `target_dept_id` 若等于源专业 ID → 改为目标专业 ID(注意唯一约束冲突时需合并名额行) | | `sys_role_dept` | `dept_id` 若等于源专业 ID → 改为目标专业 ID(注意主键冲突去重) | ### 2.4 特殊处理表 | 表 | 策略 | |----|------| | `jcbt_work_study_application` | 仅有 `major_name`:按**源专业名称精确匹配**更新为目标专业名称;合并前日志记录影响行数 | | `xg_student_class_change` | **不更新**(`original_*` / `new_*` 为历史真相) | | `*_his`、备份表、`sys_oper_log` | **不更新** | | `cx_zysj` / `cx_bjsj` / `cx_xssj` | **本版不更新**;文档标注超星同步回写风险,P2 做映射 | | `zsjy_major_info` / `his_*` | 非学工主链路组织树;本版不改,避免误伤招生目录 | ### 2.5 班级维度表(一般不必改专业) 以下按 `class_id` 关联,班级 ID 不变则**无需因合并改行**(除非另有 `major_id`): - `xg_conduct_score` - `xg_class_pos_msg` - `xg_class_teacher_counselor_rel` 合并后统计「按专业」时走班级 → `xg_class_msg.major_id`,因此班级主数据改对即可。 --- ## 3. 防漏改机制(解决「范围不够」) ### 3.1 后端统一更新器(推荐) 不要散落多处手写 UPDATE。在服务内维护**白名单清单**: ```text MajorMergeTableSpec { tableName idColumn // major_id / major_dept_id / data_id / target_dept_id / dept_id nameColumn? // major_name,可空 codeColumn? // major_code,可空 extraWhere? // 如 deleted=0 strategy // BY_ID | BY_NAME_ONLY | DEPT_TREE | SKIP_HISTORY } ``` 合并服务循环执行: 1. 预检 COUNT(按源 ID / 源名称)→ 写入合并结果 VO / 操作日志 2. 事务内按清单 UPDATE 3. 组织树改挂 + 源专业停用 4. 合并后复查:各表白名单 `COUNT(源专业ID) == 0`,否则整单回滚并报错 ### 3.2 合并前预检 SQL(上线前必跑) 对每个候选源专业 ID,输出各表命中行数;发现「新表有 `major_id` 但不在白名单」时,**禁止上线**,先补清单。 可维护仓库脚本:`test/专业合并影响行数预检.sql`(开发阶段补充)。 ### 3.3 名称同步硬规则 ```text IF 表同时存在 major_id 与 major_name: 禁止只更新其中一个 IF 仅存在 major_name: 使用源专业名称精确匹配更新(并记录日志) IF 仅存在 major_id: 只更新 ID ``` 目标专业名称取自合并时刻 `sys_dept.dept_name`(目标节点),保证与组织树一致。 --- ## 3.4 合并前行级快照备份(P0 必做,便于错误恢复) ### 为什么要做 / 为什么不做整表备份 | 方式 | 结论 | |------|------| | 整表 `CREATE TABLE AS SELECT` | **不采用**:学生/业务表体量大,每次合并磁盘与耗时不可控 | | 仅事务回滚 | **不够**:合并**成功提交后**若发现选错目标专业,事务已结束无法回退 | | **只备份将被修改的行** | **采用**:体积小、可按 `merge_batch_id` 追溯与回滚 | ### 表设计 **1)合并批次主表 `xg_major_merge_batch`** | 字段 | 说明 | |------|------| | `id` | 雪花 ID,即 `merge_batch_id` | | `target_major_id` / `target_major_name` | 目标专业 | | `source_major_ids` / `source_major_names` | JSON,源专业列表 | | `college_id` | 所属学院 | | `status` | `SUCCESS` / `FAILED` / `ROLLED_BACK` | | `summary_json` | 各表更新行数摘要 | | `operator_id` / `create_time` 等 | 标准审计字段 | **2)行快照表 `xg_major_merge_row_snapshot`** | 字段 | 说明 | |------|------| | `id` | 雪花 ID | | `merge_batch_id` | 关联批次 | | `table_name` | 原表名,如 `xg_student_msg` | | `row_pk` | 原表主键值(字符串,兼容联合主键时用约定拼接) | | `row_json` | 更新前整行 JSON(或至少还原所需列:专业相关字段 + 主键 + 组织树字段) | | `create_time` | 快照时间 | 说明:`sys_dept` 班级改挂、源专业停用前,同样写入快照(记录改前的 `parent_id`/`ancestors`/`status`)。 ### 执行顺序(必须先快照再改数) ```text 1. 校验参数与同学院 2. 生成 merge_batch_id,写入 batch 主表(status 可先 SUCCESS 前用 RUNNING) 3. 按白名单:把「即将被 UPDATE 的行」INSERT 进 row_snapshot 4. 再执行各表 UPDATE / 组织树改挂 / 停用源专业 5. 复查无源专业 ID 残留 6. 更新 batch.summary_json、status=SUCCESS 失败则事务回滚(含已写入的本批快照);若需「成功后回滚」见下 ``` 事务策略建议: - **同事务**:快照 + 改数同一事务 → 失败自动干净回滚。 - **成功后误操作恢复**:另提供运维接口/SQL:按 `merge_batch_id` 读取 `row_json` 回写原表(P0 至少落库快照并文档化手工回滚步骤;P1 再做一键 `rollback` API)。 ### 回滚注意事项 - 回滚只能恢复「本批改过的行」;合并后若业务又改过这些行,回滚会覆盖后续人工修改 → 回滚前需人工确认。 - `sys_role_dept` / 名额表若发生「删源行 + 合并到目标行」,快照需记录删除前整行,回滚逻辑单独处理。 - 快照表本身不做业务查询用途,可按保留策略定期归档(如保留 180 天)。 --- ## 4. 接口设计 ### 4.1 影响预览(建议 P0 带上,降低误操作) ```http POST /system/dept/major/merge/preview 权限: majormsg:major:merge Body: { "targetMajorId": 20251223133304109, "sourceMajorIds": [20251223133304116] } Response data: { "targetMajorId": ..., "targetMajorName": "中药", "sources": [ { "majorId": ..., "majorName": "中药(技师)", "classCount": 1, "studentCount": 50, "tableHits": [ { "table": "xg_student_leave", "rows": 0 }, ... ] } ], "warnings": ["同学院校验通过", "..."] } ``` ### 4.2 执行合并 ```http POST /system/dept/major/merge 权限: majormsg:major:merge Body: 同 preview Response data: { "success": true, "mergeBatchId": 2089..., "movedClassCount": 1, "updatedStudentCount": 50, "disabledMajorCount": 1, "snapshotRowCount": 120, "tableUpdateSummary": [ { "table": "xg_class_msg", "updated": 1 }, ... ] } ``` 校验失败示例: - 源列表为空 / 未含目标 / 目标不在已选集合 - 所选不足 2 个专业 - 跨学院 - 目标已停用或不存在 - 复查仍有源专业 ID 残留 ### 4.3 权限与菜单 - 新增按钮权限:`majormsg:major:merge` - 菜单/角色授权与现有 `majormsg:major:*` 一并配置 SQL --- ## 5. 前端设计(`majormsg/index.vue`) 1. 工具栏增加【专业合并】(`v-hasPermi="['majormsg:major:merge']"`) 2. `selection.length < 2` 时禁用 3. 弹窗: - 展示已选专业列表(名称 + 类别编码) - 单选「目标专业」(仅能选已选中的项) - 可选:调用 preview 展示班级数/学生数/业务命中 - 二次确认文案:明确「源专业将停用;班级不合并;名称与 ID 同步改挂」 4. 成功后刷新列表 --- ## 6. 后端实现要点 | 项 | 说明 | |----|------| | 模块位置 | 建议放在系统部门能力旁:`SysDept` 相关 Service,或学工模块包装后调部门服务;**事务边界在同一服务方法** | | 事务 | `@Transactional(rollbackFor = Exception.class)` | | 祖先路径 | 班级 `ancestors = 目标专业.ancestors + ',' + 目标专业.deptId` | | 操作日志 | `@Log` 记录目标/源/摘要 | | 幂等 | 源专业已停用且无子班级时可提示「无需合并」 | 伪代码顺序: ```text 1. 校验参数与同学院 2. 创建 merge_batch,按白名单把「将改行」写入 xg_major_merge_row_snapshot 3. 更新各业务表白名单(ID+名称同改) 4. 更新 xg_student_msg / xg_class_msg 5. 更新 sys_dept 班级 parent/ancestors 6. 处理 manager_source / role_dept / scholarship_quota 7. 停用源专业 8. 复查 COUNT(源ID)==0,否则抛错回滚 9. 回写 batch 摘要与 SUCCESS;接口返回 mergeBatchId ``` --- ## 7. 分期交付 | 阶段 | 内容 | 验收 | |------|------|------| | **P0** | 前端按钮+弹窗;preview+merge API;**行级快照表+批次表**;`sys_dept`+`xg_class_msg`+`xg_student_msg`+manager_source;白名单业务表 ID+名称同改;源专业停用;权限+日志;文档化按 batch 手工回滚步骤 | 用「中药(技师)→中药」演练:班级挂对、学生名称变「中药」、源专业停用、复查无残留;快照行数与更新行数一致 | | **P1** | 预检脚本入库;名额/角色冲突处理增强;**一键按 mergeBatchId 回滚 API**;合并结果导出 | 成功合并后可一键恢复到合并前 | | **P2** | 超星同步映射;可选逻辑删除源专业;快照归档策略 | 同步不回写重复专业 | --- ## 8. 测试用例(最小集) 1. 同学院 2 个专业合并成功;目标名称写入所有命中业务行 2. 只改 ID 漏改名称的回归:抽查 `xg_student_msg`、`xg_student_leave` 等 `major_id` 与 `major_name` 一致 3. 跨学院拒绝 4. 选 1 个专业无法合并 5. 同名班级并存(不合并班级) 6. `xg_student_class_change` 历史行不变 7. 事务中途失败整单回滚(含本批快照) 8. 无权限看不到按钮 / 接口 403 9. 合并成功后快照可查;按文档步骤可手工恢复抽样表 --- ## 9. 文档与配置同步(开发时) - API:`docs/开发文档/API/` 下增补「专业合并」章节(或写入专业信息管理 API) - 权限初始化 SQL - 快照/批次表 DDL 放入 `docs/设计文档/database/` - 本实施计划随代码落地更新「状态:已完成」 --- ## 10. 开放问题(实现前可默认) | 问题 | 默认建议 | |------|----------| | 勤工助学仅名称字段误伤同名? | 精确匹配源 `dept_name`;合并前 preview 展示行数人工确认 | | 奖学金名额行冲突 | 目标已有名额则累加 `total_quota` 后删源行,或报错人工处理(P0 可报错) | | 超星 | P0 合并后口头告知运营:勿从超星再灌该源专业码 | | 快照保留多久 | 默认 180 天,可配置 | --- ## 11. 评审确认清单 请确认后进入开发: - [ ] ID 与名称(及 code)同改原则同意 - [ ] §2 表白名单范围同意(可增删) - [ ] 历史表 / 超星本版不改同意 - [ ] P0 含 preview 同意 - [ ] 权限码 `majormsg:major:merge` 同意 - [x] **合并前行级快照备份**同意(非整表;P0 落库 + 手工回滚说明;P1 一键回滚) 确认后按本计划实现前后端与预检 SQL。