feat: 班级名称唯一性校验

- 在 createClass 方法中添加班级名称唯一性校验
- 在 updateClass 方法中添加班级名称唯一性校验(排除当前班级)
- 使用 MyBatis-Plus 自动逻辑删除过滤
- 同一租户下已存在的班级名称抛出 INVALID_PARAMETER 异常

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
En 2026-03-23 23:49:19 +08:00
parent d6f66135f6
commit c436f9f384
2 changed files with 254 additions and 0 deletions

166
docs/dev-logs/2026-03-23.md Normal file
View File

@ -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<Clazz> 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<Clazz> 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<Teacher> wrapper = new LambdaQueryWrapper<Teacher>()
.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` 中调用 |功能

View File

@ -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<Clazz> 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<Clazz> 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<ClassTeacher>().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<ClassTeacher>()
.eq(ClassTeacher::getClassId, classId)
.eq(ClassTeacher::getTeacherId, teacherId)
);
if (classTeacher == null) {
throw new BusinessException(ErrorCode.DATA_NOT_FOUND, "该教师不在班级中");
}
// 如果设置为新主班先将原主班降级为配班
if (ClassTeacherRole.isPrimaryRole(role)) {
List<ClassTeacher> allTeachers = classTeacherMapper.selectList(
new LambdaQueryWrapper<ClassTeacher>().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(