guoyu/fix_class_assignment_summary.md
2025-12-11 23:28:07 +08:00

265 lines
7.8 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.

# 修复学生多班级问题总结
## 📋 **问题描述**
一个学生在`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
**测试状态:** ⏳ 待测试