396 lines
15 KiB
Markdown
396 lines
15 KiB
Markdown
# 专业合并功能实施计划
|
||
|
||
> 状态: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。
|