This commit is contained in:
zlwu committed 2026-08-20 18:15:07 +08:00
1 parent 6a0f1206fa
commit 672f45befd
18 files changed
+624504 -2

No files matched your search

@@ -0,0 +1,395 @@
# 专业合并功能实施计划
> 状态: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。