Files
hzdyjs/docs/业务模块/专业信息管理/专业合并功能实施计划.md
T
2026-08-20 18:15:07 +08:00

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