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

7.8 KiB
Raw Blame History

修复学生多班级问题总结

📋 问题描述

一个学生在student_class表中可能有多条status=1(活跃)的班级记录,导致:

  1. 人员列表显示班级不一致
  2. 课程分配界面同一学生出现在多个班级中
  3. 编号+班级筛选时显示错误班级

🔍 根本原因

1. UI分配班级逻辑问题

  • assignStudentToClass方法先查询学生所有班级,再逐条移除
  • 如果并发操作或异常中断,可能导致移除不完全

2. 导入数据逻辑问题

  • importStudentsWithProgress方法在更新用户班级时逐条更新status
  • 没有事务保护可能并发导致多条status=1记录

3. 查询逻辑限制

  • 之前修改selectClassIdsByStudentId只返回1条导致其他旧班级无法被移除

修复方案

1. 添加批量禁用SQL原子操作

文件: StudyStudentClassMapper.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

/**
 * 批量禁用学生的所有活跃班级(确保一个学生只能有一个活跃班级)
 * 
 * @param studentId 学员ID
 * @return 影响的行数
 */
public int deactivateAllActiveClassesByStudentId(Long studentId);

3. 修改UI分配班级逻辑

文件: StudyClassUserServiceImpl.java -> assignStudentToClass方法

修改前:

// 查询学员是否已有班级
List<Long> existingClassIds = studentClassMapper.selectClassIdsByStudentId(studentId);

// 遍历移除旧班级
for (Long existingClassId : existingClassIds) {
    // 逐条更新status=0
}

修改后:

// ✅ 先禁用学员的所有活跃班级使用批量SQL确保原子性
int deactivatedCount = studentClassMapper.deactivateAllActiveClassesByStudentId(studentId);
logger.info("已禁用学员 {} 的所有活跃班级,共 {} 条", studentId, deactivatedCount);

4. 修改导入逻辑

文件: StudyClassUserServiceImpl.java -> importStudentsWithProgress方法

修改前:

// 查询现有班级关联
List<StudyStudentClass> oldClassList = studentClassMapper.selectStudentClassList(query);

// 如果不在该班级,将其他班级设为已移除
if (!classUpdated) {
    for (StudyStudentClass old : oldClassList) {
        if (old.getStatus() == 1) {
            old.setStatus(0);
            studentClassMapper.updateStudentClass(old);
        }
    }
}

修改后:

// ✅ 先禁用学员的所有活跃班级使用批量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 -> 新增用户时的班级分配

添加:

// ✅ 先禁用所有活跃班级(确保一致性,虽然新用户不应该有)
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. 查询数据库:
    SELECT * FROM student_class WHERE student_id = xxx AND status = 1;
    
  5. 预期结果只有1条status=1的记录

3. 测试导入数据

  1. 创建测试Excel文件包含已存在的学生但班级不同
  2. 导入数据
  3. 查询数据库:
    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. 添加唯一索引(数据库层面防护)

    ALTER TABLE student_class 
    ADD UNIQUE INDEX idx_student_status_unique (student_id, status) 
    WHERE status = 1;
    

    注意MySQL 5.7不支持部分索引,可以使用触发器实现

  2. 添加数据一致性检查任务

    -- 定期检查是否有学生有多个活跃班级
    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

    • 添加deactivateAllActiveClassesByStudentIdSQL
  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 测试状态: 待测试