guoyu/fix_class_assignment_summary.md

265 lines
7.8 KiB
Markdown
Raw Normal View History

2025-12-11 23:28:07 +08:00
# 修复学生多班级问题总结
## 📋 **问题描述**
一个学生在`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
**测试状态:** ⏳ 待测试