Compare commits

...

2 Commits

Author SHA1 Message Date
En
bf03bc30af feat: 教师手机号唯一性校验 & 班级相关功能优化
1. 教师手机号唯一性校验 (TeacherServiceImpl)
   - 添加 checkPhoneUnique 私有方法
   - createTeacher 和 updateTeacher 方法中调用校验
   - 租户隔离,编辑时排除当前教师

2. 班级相关功能优化
   - SchoolClassController: 班级管理接口
   - ClassService/ClassServiceImpl: 班级服务层
   - ClassCreateRequest/ClassUpdateRequest: 请求 DTO 优化
   - 新增班级教师相关 DTO: AddClassTeacherRequest, ClassTeacherUpdateRequest, UpdateClassTeacherRequest
   - 新增 ClassTeacherRole 枚举

3. 前端适配
   - school.ts API 更新
   - ClassListView.vue 班级列表页面优化
   - ParentListView.vue 家长列表页面优化

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 23:54:01 +08:00
En
c436f9f384 feat: 班级名称唯一性校验
- 在 createClass 方法中添加班级名称唯一性校验
- 在 updateClass 方法中添加班级名称唯一性校验(排除当前班级)
- 使用 MyBatis-Plus 自动逻辑删除过滤
- 同一租户下已存在的班级名称抛出 INVALID_PARAMETER 异常

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 23:49:19 +08:00
14 changed files with 553 additions and 52 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

@ -88,12 +88,10 @@ export interface ClassTeacher {
export interface AddClassTeacherDto { export interface AddClassTeacherDto {
teacherId: number; teacherId: number;
role: 'MAIN' | 'ASSIST' | 'CARE'; role: 'MAIN' | 'ASSIST' | 'CARE';
isPrimary?: boolean;
} }
export interface UpdateClassTeacherDto { export interface UpdateClassTeacherDto {
role?: 'MAIN' | 'ASSIST' | 'CARE'; role?: 'MAIN' | 'ASSIST' | 'CARE';
isPrimary?: boolean;
} }
export interface TransferStudentDto { export interface TransferStudentDto {

View File

@ -257,7 +257,6 @@
<a-select-option value="ASSIST">配班</a-select-option> <a-select-option value="ASSIST">配班</a-select-option>
<a-select-option value="CARE">保育员</a-select-option> <a-select-option value="CARE">保育员</a-select-option>
</a-select> </a-select>
<a-checkbox v-model:checked="teacherFormState.isPrimary">班主任</a-checkbox>
<a-button type="primary" size="small" :loading="teachersSubmitting" @click="handleAddTeacher"> <a-button type="primary" size="small" :loading="teachersSubmitting" @click="handleAddTeacher">
添加 添加
</a-button> </a-button>
@ -269,18 +268,20 @@
<div v-for="teacher in classTeachers" :key="teacher.teacherId" class="teacher-item"> <div v-for="teacher in classTeachers" :key="teacher.teacherId" class="teacher-item">
<div class="teacher-info"> <div class="teacher-info">
<span class="teacher-name">{{ teacher.teacherName }}</span> <span class="teacher-name">{{ teacher.teacherName }}</span>
<a-select v-model:value="teacher.role" size="small" style="width: 80px; margin-left: 8px;" <span class="teacher-role-badge" :class="'role-' + teacher.role.toLowerCase()">{{ getRoleLabel(teacher.role) }}</span>
<span v-if="teacher.role === 'MAIN'" class="principal-badge">班主任</span>
</div>
<div class="teacher-actions">
<a-select v-model:value="teacher.role" size="small" style="width: 80px;"
@change="handleUpdateTeacherRole(teacher)"> @change="handleUpdateTeacherRole(teacher)">
<a-select-option value="MAIN">主班</a-select-option> <a-select-option value="MAIN">主班</a-select-option>
<a-select-option value="ASSIST">配班</a-select-option> <a-select-option value="ASSIST">配班</a-select-option>
<a-select-option value="CARE">保育员</a-select-option> <a-select-option value="CARE">保育员</a-select-option>
</a-select> </a-select>
<a-checkbox v-model:checked="teacher.isPrimary" <a-button type="link" danger size="small" @click="handleRemoveTeacher(teacher.teacherId)">
@change="handleUpdateTeacherRole(teacher)">班主任</a-checkbox> 移除
</a-button>
</div> </div>
<a-button type="link" danger size="small" @click="handleRemoveTeacher(teacher.teacherId)">
移除
</a-button>
</div> </div>
</div> </div>
@ -494,10 +495,20 @@ const handleEdit = (record: ClassInfo) => {
formState.id = record.id; formState.id = record.id;
formState.name = record.name; formState.name = record.name;
formState.grade = record.grade; formState.grade = record.grade;
formState.teacherId = record.teacherId || null; // teachers ID
formState.teacherId = getPrimaryTeacherId(record.teachers) || null;
modalVisible.value = true; modalVisible.value = true;
}; };
// ID
const getPrimaryTeacherId = (teachers?: ClassTeacher[]): number | undefined => {
if (!teachers || teachers.length === 0) {
return undefined;
}
const primaryTeacher = teachers.find(t => t.isPrimary || t.role === 'MAIN');
return primaryTeacher?.teacherId;
};
const handleModalOk = async () => { const handleModalOk = async () => {
try { try {
await formRef.value?.validate(); await formRef.value?.validate();
@ -583,11 +594,9 @@ const handleStudentsTableChange = (pag: any) => {
const teacherFormState = reactive<{ const teacherFormState = reactive<{
teacherId: number | null; teacherId: number | null;
role: 'MAIN' | 'ASSIST' | 'CARE'; role: 'MAIN' | 'ASSIST' | 'CARE';
isPrimary: boolean;
}>({ }>({
teacherId: null, teacherId: null,
role: 'MAIN', role: 'MAIN',
isPrimary: false,
}); });
const editingTeacherId = ref<number | null>(null); const editingTeacherId = ref<number | null>(null);
@ -611,7 +620,6 @@ const loadClassTeachers = async (classId: number) => {
const resetTeacherForm = () => { const resetTeacherForm = () => {
teacherFormState.teacherId = null; teacherFormState.teacherId = null;
teacherFormState.role = 'MAIN'; teacherFormState.role = 'MAIN';
teacherFormState.isPrimary = false;
editingTeacherId.value = null; editingTeacherId.value = null;
}; };
@ -626,7 +634,6 @@ const handleAddTeacher = async () => {
const dto: AddClassTeacherDto = { const dto: AddClassTeacherDto = {
teacherId: teacherFormState.teacherId, teacherId: teacherFormState.teacherId,
role: teacherFormState.role, role: teacherFormState.role,
isPrimary: teacherFormState.isPrimary,
}; };
await addClassTeacher(currentClass.value!.id, dto); await addClassTeacher(currentClass.value!.id, dto);
message.success('添加成功'); message.success('添加成功');
@ -644,7 +651,6 @@ const handleUpdateTeacherRole = async (teacher: ClassTeacher) => {
try { try {
await updateClassTeacher(currentClass.value!.id, teacher.teacherId, { await updateClassTeacher(currentClass.value!.id, teacher.teacherId, {
role: teacher.role, role: teacher.role,
isPrimary: teacher.isPrimary,
}); });
message.success('更新成功'); message.success('更新成功');
loadClassTeachers(currentClass.value!.id); loadClassTeachers(currentClass.value!.id);
@ -1265,6 +1271,7 @@ onMounted(() => {
.teachers-list .teacher-info { .teachers-list .teacher-info {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px;
} }
.teachers-list .teacher-name { .teachers-list .teacher-name {
@ -1272,6 +1279,43 @@ onMounted(() => {
color: #2D3436; color: #2D3436;
} }
.teacher-role-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.teacher-role-badge.role-main {
background: #E3F2FD;
color: #1976D2;
}
.teacher-role-badge.role-assist {
background: #E8F5E9;
color: #388E3C;
}
.teacher-role-badge.role-care {
background: #FFF3E0;
color: #F57C00;
}
.principal-badge {
padding: 2px 6px;
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.teacher-actions {
display: flex;
align-items: center;
gap: 8px;
}
.empty-teachers { .empty-teachers {
text-align: center; text-align: center;
padding: 40px; padding: 40px;

View File

@ -335,7 +335,7 @@ import type { Parent, CreateParentDto, ParentChild, Student } from '@/api/school
// //
const getParentStatusText = (status: string): string => { const getParentStatusText = (status: string): string => {
if (status === 'ACTIVE') return '活跃'; if (status === 'ACTIVE') return '启用';
if (status === 'INACTIVE') return '停用'; if (status === 'INACTIVE') return '停用';
return translateGenericStatus(status); return translateGenericStatus(status);
}; };

View File

@ -0,0 +1,53 @@
package com.reading.platform.common.enums;
import lombok.Getter;
/**
* 班级教师角色枚举
*/
@Getter
public enum ClassTeacherRole {
/**
* 主班/班主任
*/
MAIN("MAIN", "主班"),
/**
* 配班
*/
ASSIST("ASSIST", "配班"),
/**
* 保育员
*/
CARE("CARE", "保育员");
private final String code;
private final String description;
ClassTeacherRole(String code, String description) {
this.code = code;
this.description = description;
}
public static ClassTeacherRole fromCode(String code) {
if (code == null) {
return null;
}
for (ClassTeacherRole role : values()) {
if (role.getCode().equalsIgnoreCase(code)) {
return role;
}
}
return null;
}
/**
* 判断是否为主班/班主任角色
*/
public static boolean isPrimaryRole(String code) {
return MAIN.getCode().equalsIgnoreCase(code);
}
}

View File

@ -3,6 +3,7 @@ package com.reading.platform.controller.school;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.annotation.Log; import com.reading.platform.common.annotation.Log;
import com.reading.platform.common.enums.ClassTeacherRole;
import com.reading.platform.common.enums.LogModule; import com.reading.platform.common.enums.LogModule;
import com.reading.platform.common.enums.LogOperationType; import com.reading.platform.common.enums.LogOperationType;
import com.reading.platform.common.mapper.ClassMapper; import com.reading.platform.common.mapper.ClassMapper;
@ -11,6 +12,8 @@ import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils; import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.dto.request.ClassCreateRequest; import com.reading.platform.dto.request.ClassCreateRequest;
import com.reading.platform.dto.request.ClassUpdateRequest; import com.reading.platform.dto.request.ClassUpdateRequest;
import com.reading.platform.dto.request.AddClassTeacherRequest;
import com.reading.platform.dto.request.UpdateClassTeacherRequest;
import com.reading.platform.common.mapper.StudentMapper; import com.reading.platform.common.mapper.StudentMapper;
import com.reading.platform.dto.response.ClassResponse; import com.reading.platform.dto.response.ClassResponse;
import com.reading.platform.dto.response.ClassTeacherResponse; import com.reading.platform.dto.response.ClassTeacherResponse;
@ -29,12 +32,14 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@Tag(name = "School - Class", description = "Class Management APIs for School") @Tag(name = "School - Class", description = "Class Management APIs for School")
@Slf4j
@RestController @RestController
@RequestMapping("/api/v1/school/classes") @RequestMapping("/api/v1/school/classes")
@RequiredArgsConstructor @RequiredArgsConstructor
@ -71,7 +76,14 @@ public class SchoolClassController {
public Result<ClassResponse> getClass(@PathVariable Long id) { public Result<ClassResponse> getClass(@PathVariable Long id) {
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
Clazz clazz = classService.getClassByIdWithTenantCheck(id, tenantId); Clazz clazz = classService.getClassByIdWithTenantCheck(id, tenantId);
return Result.success(classMapper.toVO(clazz)); ClassResponse vo = classMapper.toVO(clazz);
// 填充教师列表
List<ClassTeacher> classTeachers = classTeacherMapper.selectList(
new LambdaQueryWrapper<ClassTeacher>().eq(ClassTeacher::getClassId, id));
vo.setTeachers(buildClassTeacherResponseList(classTeachers));
return Result.success(vo);
} }
@Operation(summary = "Get class page") @Operation(summary = "Get class page")
@ -97,24 +109,32 @@ public class SchoolClassController {
List<ClassTeacher> classTeachers = classTeacherMapper.selectList( List<ClassTeacher> classTeachers = classTeacherMapper.selectList(
new LambdaQueryWrapper<ClassTeacher>().eq(ClassTeacher::getClassId, vo.getId())); new LambdaQueryWrapper<ClassTeacher>().eq(ClassTeacher::getClassId, vo.getId()));
List<ClassTeacherResponse> teacherList = new ArrayList<>(); List<ClassTeacherResponse> teacherList = buildClassTeacherResponseList(classTeachers);
for (ClassTeacher ct : classTeachers) {
Teacher t = teacherService.findTeacherById(ct.getTeacherId());
teacherList.add(ClassTeacherResponse.builder()
.id(ct.getId())
.classId(ct.getClassId())
.teacherId(ct.getTeacherId())
.role(ct.getRole())
.teacherName(t != null ? t.getName() : null)
.isPrimary("班主任".equals(ct.getRole()) || "主班".equals(ct.getRole()))
.createdAt(ct.getCreatedAt())
.build());
}
vo.setTeachers(teacherList); vo.setTeachers(teacherList);
} }
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize())); return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
} }
/**
* 构建班级教师响应列表
*/
private List<ClassTeacherResponse> buildClassTeacherResponseList(List<ClassTeacher> classTeachers) {
List<ClassTeacherResponse> teacherList = new ArrayList<>();
for (ClassTeacher ct : classTeachers) {
Teacher t = teacherService.findTeacherById(ct.getTeacherId());
teacherList.add(ClassTeacherResponse.builder()
.id(ct.getId())
.classId(ct.getClassId())
.teacherId(ct.getTeacherId())
.role(ct.getRole())
.teacherName(t != null ? t.getName() : null)
.isPrimary(ClassTeacherRole.isPrimaryRole(ct.getRole()))
.createdAt(ct.getCreatedAt())
.build());
}
return teacherList;
}
@Operation(summary = "Delete class") @Operation(summary = "Delete class")
@Log(module = LogModule.CLASS, type = LogOperationType.DELETE, description = "删除班级") @Log(module = LogModule.CLASS, type = LogOperationType.DELETE, description = "删除班级")
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@ -124,12 +144,44 @@ public class SchoolClassController {
return Result.success(); return Result.success();
} }
@Operation(summary = "Assign teachers to class") @Operation(summary = "Add teacher to class")
@Log(module = LogModule.CLASS, type = LogOperationType.UPDATE, description = "分配教师到班级") @Log(module = LogModule.CLASS, type = LogOperationType.CREATE, description = "添加教师到班级")
@PostMapping("/{id}/teachers") @PostMapping("/{id}/teachers")
public Result<Void> assignTeachers(@PathVariable Long id, @RequestBody List<Long> teacherIds) { public Result<Void> addClassTeacher(@PathVariable Long id, @Valid @RequestBody AddClassTeacherRequest request) {
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
classService.assignTeachersWithTenantCheck(id, tenantId, teacherIds); classService.getClassByIdWithTenantCheck(id, tenantId);
// 如果设置为新主班先将原主班降级为配班
if (ClassTeacherRole.isPrimaryRole(request.getRole())) {
List<ClassTeacher> allTeachers = classTeacherMapper.selectList(
new LambdaQueryWrapper<ClassTeacher>().eq(ClassTeacher::getClassId, id));
for (ClassTeacher ct : allTeachers) {
if (ClassTeacherRole.isPrimaryRole(ct.getRole())) {
ct.setRole(ClassTeacherRole.ASSIST.getCode());
classTeacherMapper.updateById(ct);
log.info("班级 {} 的原主班教师 {} 已降级为配班", id, ct.getTeacherId());
}
}
}
// 查询是否已存在关联
ClassTeacher existing = classTeacherMapper.selectOne(new LambdaQueryWrapper<ClassTeacher>()
.eq(ClassTeacher::getClassId, id)
.eq(ClassTeacher::getTeacherId, request.getTeacherId()));
if (existing != null) {
// 更新现有角色
existing.setRole(request.getRole());
classTeacherMapper.updateById(existing);
} else {
// 创建新关联
ClassTeacher classTeacher = new ClassTeacher();
classTeacher.setClassId(id);
classTeacher.setTeacherId(request.getTeacherId());
classTeacher.setRole(request.getRole());
classTeacherMapper.insert(classTeacher);
}
return Result.success(); return Result.success();
} }
@ -163,20 +215,7 @@ public class SchoolClassController {
classService.getClassByIdWithTenantCheck(id, tenantId); classService.getClassByIdWithTenantCheck(id, tenantId);
List<ClassTeacher> classTeachers = classTeacherMapper.selectList( List<ClassTeacher> classTeachers = classTeacherMapper.selectList(
new LambdaQueryWrapper<ClassTeacher>().eq(ClassTeacher::getClassId, id)); new LambdaQueryWrapper<ClassTeacher>().eq(ClassTeacher::getClassId, id));
List<ClassTeacherResponse> teacherList = new ArrayList<>(); return Result.success(buildClassTeacherResponseList(classTeachers));
for (ClassTeacher ct : classTeachers) {
Teacher t = teacherService.findTeacherById(ct.getTeacherId());
teacherList.add(ClassTeacherResponse.builder()
.id(ct.getId())
.classId(ct.getClassId())
.teacherId(ct.getTeacherId())
.role(ct.getRole())
.teacherName(t != null ? t.getName() : null)
.isPrimary("班主任".equals(ct.getRole()) || "主班".equals(ct.getRole()))
.createdAt(ct.getCreatedAt())
.build());
}
return Result.success(teacherList);
} }
@Operation(summary = "Update class teacher role") @Operation(summary = "Update class teacher role")
@ -185,11 +224,13 @@ public class SchoolClassController {
public Result<Void> updateClassTeacher( public Result<Void> updateClassTeacher(
@PathVariable Long id, @PathVariable Long id,
@PathVariable Long teacherId, @PathVariable Long teacherId,
@RequestBody Object request) { @Valid @RequestBody UpdateClassTeacherRequest request) {
// 验证班级属于当前租户 // 验证班级属于当前租户
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
classService.getClassByIdWithTenantCheck(id, tenantId); classService.getClassByIdWithTenantCheck(id, tenantId);
// TODO: 实现更新班级教师
// 调用 Service 方法更新
classService.updateClassTeacherRole(id, teacherId, request.getRole(), null);
return Result.success(); return Result.success();
} }
@ -202,7 +243,12 @@ public class SchoolClassController {
// 验证班级属于当前租户 // 验证班级属于当前租户
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
classService.getClassByIdWithTenantCheck(id, tenantId); classService.getClassByIdWithTenantCheck(id, tenantId);
// TODO: 实现移除班级教师
// 删除班级教师关联记录
classTeacherMapper.delete(new LambdaQueryWrapper<ClassTeacher>()
.eq(ClassTeacher::getClassId, id)
.eq(ClassTeacher::getTeacherId, teacherId));
return Result.success(); return Result.success();
} }

View File

@ -0,0 +1,23 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 添加教师到班级请求
*/
@Data
@Schema(description = "添加教师到班级请求")
public class AddClassTeacherRequest {
@NotNull(message = "教师 ID 不能为空")
@Schema(description = "教师 ID", required = true, example = "1")
private Long teacherId;
@NotBlank(message = "角色不能为空")
@Schema(description = "角色MAIN(主班教师)|ASSIST(配班教师)|CARE(保育员)", required = true,
example = "MAIN", allowableValues = {"MAIN", "ASSIST", "CARE"})
private String role;
}

View File

@ -21,4 +21,7 @@ public class ClassCreateRequest {
@Schema(description = "容量") @Schema(description = "容量")
private Integer capacity; private Integer capacity;
@Schema(description = "班主任教师 ID")
private Long teacherId;
} }

View File

@ -0,0 +1,19 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 班级教师角色更新请求
*/
@Data
@Schema(description = "班级教师角色更新请求")
public class ClassTeacherUpdateRequest {
@Schema(description = "角色MAIN-主班ASSIST-配班CARE-保育员")
private String role;
@Schema(description = "是否班主任")
private Boolean isPrimary;
}

View File

@ -22,4 +22,7 @@ public class ClassUpdateRequest {
@Schema(description = "状态") @Schema(description = "状态")
private String status; private String status;
@Schema(description = "班主任教师 ID")
private Long teacherId;
} }

View File

@ -0,0 +1,18 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 更新班级教师角色请求
*/
@Data
@Schema(description = "更新班级教师角色请求")
public class UpdateClassTeacherRequest {
@NotBlank(message = "角色不能为空")
@Schema(description = "角色MAIN(主班教师)|ASSIST(配班教师)|CARE(保育员)", required = true,
example = "MAIN", allowableValues = {"MAIN", "ASSIST", "CARE"})
private String role;
}

View File

@ -84,10 +84,15 @@ public interface ClassService extends com.baomidou.mybatisplus.extension.service
List<Long> getTeacherIdsByClassId(Long classId); List<Long> getTeacherIdsByClassId(Long classId);
/** /**
* 获取教师在班级中的角色信息role 字段 * 获取学生在班级中的角色信息role 字段
*/ */
String getTeacherRoleInClass(Long classId, Long teacherId); String getTeacherRoleInClass(Long classId, Long teacherId);
/**
* 更新班级教师角色
*/
void updateClassTeacherRole(Long classId, Long teacherId, String role, Boolean isPrimary);
/** /**
* 获取租户下活跃班级列表 * 获取租户下活跃班级列表
*/ */

View File

@ -2,6 +2,7 @@ package com.reading.platform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.GenericStatus;
import com.reading.platform.common.enums.ErrorCode; import com.reading.platform.common.enums.ErrorCode;
import com.reading.platform.common.exception.BusinessException; 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) { public Clazz createClass(Long tenantId, ClassCreateRequest request) {
log.info("开始创建班级,名称:{}", request.getName()); 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 clazz = new Clazz();
clazz.setTenantId(tenantId); clazz.setTenantId(tenantId);
clazz.setName(request.getName()); clazz.setName(request.getName());
@ -54,6 +64,16 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service
clazzMapper.insert(clazz); 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()); log.info("班级创建成功ID: {}", clazz.getId());
return clazz; return clazz;
} }
@ -66,6 +86,15 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service
Clazz clazz = getClassById(id); Clazz clazz = getClassById(id);
if (StringUtils.hasText(request.getName())) { 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()); clazz.setName(request.getName());
} }
if (request.getGrade() != null) { if (request.getGrade() != null) {
@ -83,6 +112,22 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service
clazzMapper.updateById(clazz); 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); log.info("班级更新成功ID: {}", id);
return clazz; return clazz;
} }
@ -281,6 +326,49 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service
return teacherIds; 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 @Override
public String getTeacherRoleInClass(Long classId, Long teacherId) { public String getTeacherRoleInClass(Long classId, Long teacherId) {
ClassTeacher ct = classTeacherMapper.selectOne( ClassTeacher ct = classTeacherMapper.selectOne(

View File

@ -61,6 +61,9 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
throw new BusinessException(ErrorCode.DATA_ALREADY_EXISTS, "用户名已存在"); throw new BusinessException(ErrorCode.DATA_ALREADY_EXISTS, "用户名已存在");
} }
// 检查手机号是否已存在
checkPhoneUnique(tenantId, request.getPhone(), null);
Teacher teacher = new Teacher(); Teacher teacher = new Teacher();
teacher.setTenantId(tenantId); teacher.setTenantId(tenantId);
teacher.setUsername(request.getUsername()); teacher.setUsername(request.getUsername());
@ -97,6 +100,9 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
Teacher teacher = getTeacherById(id); Teacher teacher = getTeacherById(id);
// 检查手机号是否已存在排除当前教师
checkPhoneUnique(teacher.getTenantId(), request.getPhone(), teacher.getId());
if (StringUtils.hasText(request.getName())) { if (StringUtils.hasText(request.getName())) {
teacher.setName(request.getName()); teacher.setName(request.getName());
} }
@ -326,4 +332,33 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
response.setLessonCount((int) lessonCount); response.setLessonCount((int) lessonCount);
} }
/**
* 检查手机号唯一性
*
* @param tenantId 租户 ID
* @param phone 手机号
* @param excludeTeacherId 排除的教师 ID编辑时使用新增时为 null
* @throws BusinessException 手机号已存在时抛出
*/
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, "手机号已存在");
}
}
} }