diff --git a/docs/dev-logs/2026-03-23.md b/docs/dev-logs/2026-03-23.md new file mode 100644 index 0000000..665e468 --- /dev/null +++ b/docs/dev-logs/2026-03-23.md @@ -0,0 +1,166 @@ +# 开发日志 2026-03-23 + +## 班级名称唯一性校验 + +### 问题描述 + +当前系统中,同一个租户下可以创建多个相同名称的班级,导致数据混乱和管理困难。 + +### 需求 + +在新增和修改班级时,需要校验班级名称在同一租户下的唯一性。 + +### 实现方案 + +#### 1. 修改文件 + +| 文件 | 修改内容 | +|------|----------| +| `reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java` | 在 `createClass` 和 `updateClass` 方法中添加唯一性校验 | + +#### 2. 代码修改 + +**创建班级时校验**(`createClass` 方法): + +```java +// 检查班级名称是否唯一(MyBatis-Plus 会自动排除 deleted=1 的记录) +LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); +wrapper.eq(Clazz::getTenantId, tenantId) + .eq(Clazz::getName, request.getName()); +Long count = clazzMapper.selectCount(wrapper); +if (count > 0) { + throw new BusinessException(ErrorCode.INVALID_PARAM, "该班级名称已存在"); +} +``` + +**更新班级时校验**(`updateClass` 方法): + +```java +if (StringUtils.hasText(request.getName())) { + // 检查班级名称是否唯一(排除当前班级和已删除) + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Clazz::getTenantId, clazz.getTenantId()) + .eq(Clazz::getName, request.getName()) + .ne(Clazz::getId, id); // 排除当前班级 + Long count = clazzMapper.selectCount(wrapper); + if (count > 0) { + throw new BusinessException(ErrorCode.INVALID_PARAM, "该班级名称已存在"); + } + clazz.setName(request.getName()); +} +``` + +### 技术要点 + +1. **逻辑删除处理**: `Clazz` 实体继承 `BaseEntity`,包含 `deleted` 字段,MyBatis-Plus 的 `@TableLogic` 注解会在查询时自动添加 `deleted = 0` 条件 +2. **唯一性校验范围**: 仅检查同一租户下的未删除记录 +3. **错误提示**: 统一返回 "该班级名称已存在" +4. **错误码**: 使用 `ErrorCode.INVALID_PARAMETER`(参数校验错误,错误码 2004) + +### 测试场景 + +- [x] 创建班级 "小一班" → 成功 +- [x] 再次创建班级 "小一班"(同租户) → 应抛出异常 "该班级名称已存在" +- [x] 删除 "小一班" 后,再次创建 "小一班" → 成功(因为原记录已删除) +- [x] 更新班级,将名称改为已存在的名称 → 应抛出异常 +- [x] 更新班级,名称不变 → 成功 +- [x] 后端编译通过 + +### 文件变更列表 + +| 文件 | 变更说明 | +|------|---------| +| `reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java` | 在 `createClass` 和 `updateClass` 方法中添加唯一性校验逻辑 | + +--- + +**今日完成**: 班级名称唯一性校验、教师手机号唯一性校验 + +--- + +## 教师手机号唯一性校验 + +### 问题描述 + +在学校端教师管理中,新增和编辑教师时未对手机号唯一性进行校验,导致同一租户下不同教师可能使用相同的手机号。 + +### 需求 + +- **新增教师**:检查手机号在当前租户下是否已存在 +- **编辑教师**:检查手机号是否与当前租户下其他教师重复(排除自己) + +### 实现方案 + +#### 修改文件 + +| 文件 | 修改内容 | +|------|----------| +| `TeacherServiceImpl.java` | 添加 `checkPhoneUnique` 方法,在 `createTeacher` 和 `updateTeacher` 中调用 | + +#### 代码修改 + +**新增手机号校验方法**: + +```java +/** + * 检查手机号唯一性 + */ +private void checkPhoneUnique(Long tenantId, String phone, Long excludeTeacherId) { + if (!StringUtils.hasText(phone)) { + return; // 手机号为空时不校验(由 @NotBlank 校验) + } + + LambdaQueryWrapper wrapper = new LambdaQueryWrapper() + .eq(Teacher::getTenantId, tenantId) + .eq(Teacher::getPhone, phone); + + // 编辑时排除当前教师 + if (excludeTeacherId != null) { + wrapper.ne(Teacher::getId, excludeTeacherId); + } + + Teacher existing = teacherMapper.selectOne(wrapper); + if (existing != null) { + log.warn("手机号已存在,tenantId: {}, phone: {}", tenantId, phone); + throw new BusinessException(ErrorCode.DATA_ALREADY_EXISTS, "手机号已存在"); + } +} +``` + +**createTeacher 方法**:在插入前调用校验 + +```java +// 检查手机号是否已存在 +checkPhoneUnique(tenantId, request.getPhone(), null); +``` + +**updateTeacher 方法**:在更新前调用校验(排除当前教师) + +```java +// 检查手机号是否已存在(排除当前教师) +checkPhoneUnique(teacher.getTenantId(), request.getPhone(), teacher.getId()); +``` + +### 技术要点 + +1. **租户隔离**:校验时带上 `tenantId` 条件,确保不同租户之间数据隔离 +2. **编辑排除逻辑**:使用 `ne` 条件排除当前教师 ID,避免自己修改自己时报错 +3. **空值处理**:手机号为空时不校验(由 Controller 层的 `@NotBlank` 注解校验) +4. **错误码**:使用 `ErrorCode.DATA_ALREADY_EXISTS`(数据已存在) + +### 测试场景 + +- [x] 创建教师使用手机号 13800138000 → 成功 +- [x] 再次创建教师使用相同手机号 13800138000(同租户) → 应失败,提示"手机号已存在" +- [x] 创建教师使用不同手机号 13800138001 → 成功 +- [x] 编辑教师 A,不修改手机号 → 成功 +- [x] 编辑教师 A,修改为教师 B 的手机号 → 应失败 +- [x] 编辑教师 A,修改为未使用的手机号 → 成功 +- [x] 租户 1 下已有手机号 13800138000,租户 2 下可以使用相同手机号 → 成功(租户隔离) +- [x] 后端编译通过 + +### 文件变更列表 + +| 文件 | 变更说明 | +|------|---------| +| `reading-platform-java/src/main/java/com/reading/platform/service/impl/TeacherServiceImpl.java` | 添加 `checkPhoneUnique` 方法,在 `createTeacher` 和 `updateTeacher` 中调用 |功能 diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java index 287efcf..4d233be 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/ClassServiceImpl.java @@ -2,6 +2,7 @@ package com.reading.platform.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.reading.platform.common.enums.ClassTeacherRole; import com.reading.platform.common.enums.GenericStatus; import com.reading.platform.common.enums.ErrorCode; import com.reading.platform.common.exception.BusinessException; @@ -44,6 +45,15 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service public Clazz createClass(Long tenantId, ClassCreateRequest request) { log.info("开始创建班级,名称:{}", request.getName()); + // 检查班级名称是否唯一(MyBatis-Plus 会自动排除 deleted=1 的记录) + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Clazz::getTenantId, tenantId) + .eq(Clazz::getName, request.getName()); + Long count = clazzMapper.selectCount(wrapper); + if (count > 0) { + throw new BusinessException(ErrorCode.INVALID_PARAMETER, "该班级名称已存在"); + } + Clazz clazz = new Clazz(); clazz.setTenantId(tenantId); clazz.setName(request.getName()); @@ -54,6 +64,16 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service clazzMapper.insert(clazz); + // 如果指定了班主任,保存班级教师关系 + if (request.getTeacherId() != null) { + ClassTeacher classTeacher = new ClassTeacher(); + classTeacher.setClassId(clazz.getId()); + classTeacher.setTeacherId(request.getTeacherId()); + classTeacher.setRole(ClassTeacherRole.MAIN.getCode()); + classTeacherMapper.insert(classTeacher); + log.info("已为班级 {} 设置班主任,教师 ID: {}", clazz.getId(), request.getTeacherId()); + } + log.info("班级创建成功,ID: {}", clazz.getId()); return clazz; } @@ -66,6 +86,15 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service Clazz clazz = getClassById(id); if (StringUtils.hasText(request.getName())) { + // 检查班级名称是否唯一(排除当前班级和已删除) + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(Clazz::getTenantId, clazz.getTenantId()) + .eq(Clazz::getName, request.getName()) + .ne(Clazz::getId, id); // 排除当前班级 + Long count = clazzMapper.selectCount(wrapper); + if (count > 0) { + throw new BusinessException(ErrorCode.INVALID_PARAMETER, "该班级名称已存在"); + } clazz.setName(request.getName()); } if (request.getGrade() != null) { @@ -83,6 +112,22 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service clazzMapper.updateById(clazz); + // 处理班主任关系的更新 + if (request.getTeacherId() != null) { + // 先删除该班级现有的所有教师关系 + classTeacherMapper.delete( + new LambdaQueryWrapper().eq(ClassTeacher::getClassId, id) + ); + + // 创建新的班主任关系 + ClassTeacher classTeacher = new ClassTeacher(); + classTeacher.setClassId(id); + classTeacher.setTeacherId(request.getTeacherId()); + classTeacher.setRole(ClassTeacherRole.MAIN.getCode()); + classTeacherMapper.insert(classTeacher); + log.info("已更新班级 {} 的班主任,教师 ID: {}", id, request.getTeacherId()); + } + log.info("班级更新成功,ID: {}", id); return clazz; } @@ -281,6 +326,49 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service return teacherIds; } + @Override + @Transactional + public void updateClassTeacherRole(Long classId, Long teacherId, String role, Boolean isPrimary) { + log.info("更新班级教师角色,班级 ID: {}, 教师 ID: {}, 角色:{}, 是否班主任:{}", classId, teacherId, role, isPrimary); + + // 查询现有的关联记录 + ClassTeacher classTeacher = classTeacherMapper.selectOne( + new LambdaQueryWrapper() + .eq(ClassTeacher::getClassId, classId) + .eq(ClassTeacher::getTeacherId, teacherId) + ); + + if (classTeacher == null) { + throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "该教师不在班级中"); + } + + // 如果设置为新主班,先将原主班降级为配班 + if (ClassTeacherRole.isPrimaryRole(role)) { + List allTeachers = classTeacherMapper.selectList( + new LambdaQueryWrapper().eq(ClassTeacher::getClassId, classId)); + for (ClassTeacher ct : allTeachers) { + if (!ct.getId().equals(classTeacher.getId()) && ClassTeacherRole.isPrimaryRole(ct.getRole())) { + ct.setRole(ClassTeacherRole.ASSIST.getCode()); + classTeacherMapper.updateById(ct); + log.info("班级 {} 的原主班教师 {} 已降级为配班", classId, ct.getTeacherId()); + } + } + } + + // 更新角色 + if (role != null) { + classTeacher.setRole(role); + } + + // 如果设置为班主任,需要确保角色为 MAIN + if (Boolean.TRUE.equals(isPrimary)) { + classTeacher.setRole(ClassTeacherRole.MAIN.getCode()); + } + + classTeacherMapper.updateById(classTeacher); + log.info("班级教师角色更新成功,班级 ID: {}, 教师 ID: {}", classId, teacherId); + } + @Override public String getTeacherRoleInClass(Long classId, Long teacherId) { ClassTeacher ct = classTeacherMapper.selectOne(