265 lines
7.8 KiB
Markdown
265 lines
7.8 KiB
Markdown
# 修复学生多班级问题总结
|
||
|
||
## 📋 **问题描述**
|
||
|
||
一个学生在`student_class`表中可能有多条`status=1`(活跃)的班级记录,导致:
|
||
1. 人员列表显示班级不一致
|
||
2. 课程分配界面同一学生出现在多个班级中
|
||
3. 编号+班级筛选时显示错误班级
|
||
|
||
## 🔍 **根本原因**
|
||
|
||
### **1. UI分配班级逻辑问题**
|
||
- `assignStudentToClass`方法先查询学生所有班级,再逐条移除
|
||
- 如果并发操作或异常中断,可能导致移除不完全
|
||
|
||
### **2. 导入数据逻辑问题**
|
||
- `importStudentsWithProgress`方法在更新用户班级时,逐条更新status
|
||
- 没有事务保护,可能并发导致多条status=1记录
|
||
|
||
### **3. 查询逻辑限制**
|
||
- 之前修改`selectClassIdsByStudentId`只返回1条,导致其他旧班级无法被移除
|
||
|
||
---
|
||
|
||
## ✅ **修复方案**
|
||
|
||
### **1. 添加批量禁用SQL(原子操作)**
|
||
|
||
**文件:** `StudyStudentClassMapper.xml`
|
||
```xml
|
||
<!-- 批量禁用学生的所有活跃班级(确保一个学生只能有一个活跃班级) -->
|
||
<update id="deactivateAllActiveClassesByStudentId" parameterType="Long">
|
||
update student_class
|
||
set status = 0
|
||
where student_id = #{studentId} and status = 1
|
||
</update>
|
||
```
|
||
|
||
**优势:**
|
||
- ✅ 一条SQL语句原子性更新
|
||
- ✅ 不需要先查询再更新
|
||
- ✅ 避免并发问题
|
||
|
||
---
|
||
|
||
### **2. 添加Mapper接口方法**
|
||
|
||
**文件:** `StudyStudentClassMapper.java`
|
||
```java
|
||
/**
|
||
* 批量禁用学生的所有活跃班级(确保一个学生只能有一个活跃班级)
|
||
*
|
||
* @param studentId 学员ID
|
||
* @return 影响的行数
|
||
*/
|
||
public int deactivateAllActiveClassesByStudentId(Long studentId);
|
||
```
|
||
|
||
---
|
||
|
||
### **3. 修改UI分配班级逻辑**
|
||
|
||
**文件:** `StudyClassUserServiceImpl.java` -> `assignStudentToClass`方法
|
||
|
||
**修改前:**
|
||
```java
|
||
// 查询学员是否已有班级
|
||
List<Long> existingClassIds = studentClassMapper.selectClassIdsByStudentId(studentId);
|
||
|
||
// 遍历移除旧班级
|
||
for (Long existingClassId : existingClassIds) {
|
||
// 逐条更新status=0
|
||
}
|
||
```
|
||
|
||
**修改后:**
|
||
```java
|
||
// ✅ 先禁用学员的所有活跃班级(使用批量SQL,确保原子性)
|
||
int deactivatedCount = studentClassMapper.deactivateAllActiveClassesByStudentId(studentId);
|
||
logger.info("已禁用学员 {} 的所有活跃班级,共 {} 条", studentId, deactivatedCount);
|
||
```
|
||
|
||
---
|
||
|
||
### **4. 修改导入逻辑**
|
||
|
||
**文件:** `StudyClassUserServiceImpl.java` -> `importStudentsWithProgress`方法
|
||
|
||
**修改前:**
|
||
```java
|
||
// 查询现有班级关联
|
||
List<StudyStudentClass> oldClassList = studentClassMapper.selectStudentClassList(query);
|
||
|
||
// 如果不在该班级,将其他班级设为已移除
|
||
if (!classUpdated) {
|
||
for (StudyStudentClass old : oldClassList) {
|
||
if (old.getStatus() == 1) {
|
||
old.setStatus(0);
|
||
studentClassMapper.updateStudentClass(old);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**修改后:**
|
||
```java
|
||
// ✅ 先禁用学员的所有活跃班级(使用批量SQL,确保原子性)
|
||
studentClassMapper.deactivateAllActiveClassesByStudentId(u.getUserId());
|
||
|
||
// ✅ 检查是否已经存在该班级关联(可能是之前移除的)
|
||
StudyStudentClass query = new StudyStudentClass();
|
||
query.setStudentId(u.getUserId());
|
||
query.setClassId(classId);
|
||
List<StudyStudentClass> existingList = studentClassMapper.selectStudentClassList(query);
|
||
|
||
if (existingList != null && !existingList.isEmpty()) {
|
||
// 已存在该关联,更新状态为活跃
|
||
StudyStudentClass existing = existingList.get(0);
|
||
existing.setStatus(1);
|
||
existing.setJoinTime(new Date());
|
||
studentClassMapper.updateStudentClass(existing);
|
||
} else {
|
||
// 不存在该关联,插入新的班级关联
|
||
StudyStudentClass studentClass = new StudyStudentClass();
|
||
studentClass.setStudentId(u.getUserId());
|
||
studentClass.setClassId(classId);
|
||
studentClass.setStatus(1);
|
||
studentClass.setJoinTime(new Date());
|
||
studentClassMapper.insertStudentClass(studentClass);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### **5. 修改新增用户逻辑**
|
||
|
||
**文件:** `StudyClassUserServiceImpl.java` -> 新增用户时的班级分配
|
||
|
||
**添加:**
|
||
```java
|
||
// ✅ 先禁用所有活跃班级(确保一致性,虽然新用户不应该有)
|
||
studentClassMapper.deactivateAllActiveClassesByStudentId(user.getUserId());
|
||
|
||
// 插入学员-班级关联
|
||
StudyStudentClass studentClass = new StudyStudentClass();
|
||
studentClass.setStudentId(user.getUserId());
|
||
studentClass.setClassId(classId);
|
||
studentClass.setStatus(1);
|
||
studentClass.setJoinTime(new Date());
|
||
studentClassMapper.insertStudentClass(studentClass);
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 **测试步骤**
|
||
|
||
### **1. 重启后端服务**
|
||
修改了以下文件,需要重启:
|
||
- `StudyStudentClassMapper.xml`
|
||
- `StudyStudentClassMapper.java`
|
||
- `StudyClassUserServiceImpl.java`
|
||
|
||
### **2. 测试UI分配班级**
|
||
1. 进入人员管理
|
||
2. 选择一个学生,点击"分配班级"
|
||
3. 分配到新班级
|
||
4. 查询数据库:
|
||
```sql
|
||
SELECT * FROM student_class WHERE student_id = xxx AND status = 1;
|
||
```
|
||
5. 预期结果:只有1条status=1的记录
|
||
|
||
### **3. 测试导入数据**
|
||
1. 创建测试Excel文件(包含已存在的学生,但班级不同)
|
||
2. 导入数据
|
||
3. 查询数据库:
|
||
```sql
|
||
SELECT student_id, COUNT(*) as count
|
||
FROM student_class
|
||
WHERE status = 1
|
||
GROUP BY student_id
|
||
HAVING count > 1;
|
||
```
|
||
4. 预期结果:返回空结果(没有学生有多个活跃班级)
|
||
|
||
### **4. 测试并发导入**
|
||
1. 同时导入多个包含相同学生的Excel文件
|
||
2. 查询数据库(同上)
|
||
3. 预期结果:每个学生只有最后一次导入的班级为活跃状态
|
||
|
||
---
|
||
|
||
## 🔒 **防护措施**
|
||
|
||
### **已实施:**
|
||
1. ✅ 使用批量SQL更新(原子操作)
|
||
2. ✅ 使用`@Transactional`注解确保事务一致性
|
||
3. ✅ 在所有班级分配场景中统一使用`deactivateAllActiveClassesByStudentId`
|
||
|
||
### **建议(可选):**
|
||
1. **添加唯一索引**(数据库层面防护)
|
||
```sql
|
||
ALTER TABLE student_class
|
||
ADD UNIQUE INDEX idx_student_status_unique (student_id, status)
|
||
WHERE status = 1;
|
||
```
|
||
注意:MySQL 5.7不支持部分索引,可以使用触发器实现
|
||
|
||
2. **添加数据一致性检查任务**
|
||
```sql
|
||
-- 定期检查是否有学生有多个活跃班级
|
||
SELECT student_id, COUNT(*) as count
|
||
FROM student_class
|
||
WHERE status = 1
|
||
GROUP BY student_id
|
||
HAVING count > 1;
|
||
```
|
||
|
||
---
|
||
|
||
## 📝 **修改文件清单**
|
||
|
||
1. `Study-Vue-redis/ry-study-system/src/main/resources/mapper/study/StudyStudentClassMapper.xml`
|
||
- 添加`deactivateAllActiveClassesByStudentId`SQL
|
||
|
||
2. `Study-Vue-redis/ry-study-system/src/main/java/com/ddnai/system/mapper/study/StudyStudentClassMapper.java`
|
||
- 添加`deactivateAllActiveClassesByStudentId`接口方法
|
||
|
||
3. `Study-Vue-redis/ry-study-system/src/main/java/com/ddnai/system/service/impl/study/StudyClassUserServiceImpl.java`
|
||
- 修改`assignStudentToClass`方法(UI分配班级)
|
||
- 修改`importStudentsWithProgress`方法(导入数据时分配班级)
|
||
- 修改新增用户时的班级分配逻辑
|
||
|
||
---
|
||
|
||
## ✅ **预期效果**
|
||
|
||
1. **防止新增多班级问题**
|
||
- UI分配班级时:自动禁用旧班级,只保留新班级
|
||
- 导入数据时:自动禁用旧班级,只保留导入的班级
|
||
|
||
2. **数据一致性**
|
||
- 每个学生在`student_class`表中只有1条`status=1`的记录
|
||
- 其他旧班级记录保留但`status=0`(历史记录)
|
||
|
||
3. **业务逻辑正确**
|
||
- 人员列表显示学生的唯一活跃班级
|
||
- 课程分配界面学生只出现在1个班级中
|
||
- 编号+班级筛选正常工作
|
||
|
||
---
|
||
|
||
## 🔧 **后续维护**
|
||
|
||
1. 监控日志中的`deactivatedCount`,确认是否有异常
|
||
2. 定期执行数据一致性检查SQL
|
||
3. 如果需要学生历史班级记录,保留`status=0`的记录
|
||
4. 如果需要学生转班功能,使用`assignStudentToClass`方法
|
||
|
||
---
|
||
|
||
**修复时间:** 2025-12-11
|
||
**修复人员:** AI Assistant
|
||
**测试状态:** ⏳ 待测试
|