diff --git a/reading-platform-frontend/src/api/school.ts b/reading-platform-frontend/src/api/school.ts
index e5b00ea..02b1503 100644
--- a/reading-platform-frontend/src/api/school.ts
+++ b/reading-platform-frontend/src/api/school.ts
@@ -88,12 +88,10 @@ export interface ClassTeacher {
export interface AddClassTeacherDto {
teacherId: number;
role: 'MAIN' | 'ASSIST' | 'CARE';
- isPrimary?: boolean;
}
export interface UpdateClassTeacherDto {
role?: 'MAIN' | 'ASSIST' | 'CARE';
- isPrimary?: boolean;
}
export interface TransferStudentDto {
diff --git a/reading-platform-frontend/src/views/school/classes/ClassListView.vue b/reading-platform-frontend/src/views/school/classes/ClassListView.vue
index 01d6191..a8fbbae 100644
--- a/reading-platform-frontend/src/views/school/classes/ClassListView.vue
+++ b/reading-platform-frontend/src/views/school/classes/ClassListView.vue
@@ -257,7 +257,6 @@
配班
保育员
- 班主任
添加
@@ -269,18 +268,20 @@
{{ teacher.teacherName }}
-
{{ getRoleLabel(teacher.role) }}
+ 班主任
+
+
+
主班
配班
保育员
-
班主任
+
+ 移除
+
-
- 移除
-
@@ -494,10 +495,20 @@ const handleEdit = (record: ClassInfo) => {
formState.id = record.id;
formState.name = record.name;
formState.grade = record.grade;
- formState.teacherId = record.teacherId || null;
+ // 从 teachers 数组中提取班主任 ID
+ formState.teacherId = getPrimaryTeacherId(record.teachers) || null;
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 () => {
try {
await formRef.value?.validate();
@@ -583,11 +594,9 @@ const handleStudentsTableChange = (pag: any) => {
const teacherFormState = reactive<{
teacherId: number | null;
role: 'MAIN' | 'ASSIST' | 'CARE';
- isPrimary: boolean;
}>({
teacherId: null,
role: 'MAIN',
- isPrimary: false,
});
const editingTeacherId = ref(null);
@@ -611,7 +620,6 @@ const loadClassTeachers = async (classId: number) => {
const resetTeacherForm = () => {
teacherFormState.teacherId = null;
teacherFormState.role = 'MAIN';
- teacherFormState.isPrimary = false;
editingTeacherId.value = null;
};
@@ -626,7 +634,6 @@ const handleAddTeacher = async () => {
const dto: AddClassTeacherDto = {
teacherId: teacherFormState.teacherId,
role: teacherFormState.role,
- isPrimary: teacherFormState.isPrimary,
};
await addClassTeacher(currentClass.value!.id, dto);
message.success('添加成功');
@@ -644,7 +651,6 @@ const handleUpdateTeacherRole = async (teacher: ClassTeacher) => {
try {
await updateClassTeacher(currentClass.value!.id, teacher.teacherId, {
role: teacher.role,
- isPrimary: teacher.isPrimary,
});
message.success('更新成功');
loadClassTeachers(currentClass.value!.id);
@@ -1265,6 +1271,7 @@ onMounted(() => {
.teachers-list .teacher-info {
display: flex;
align-items: center;
+ gap: 8px;
}
.teachers-list .teacher-name {
@@ -1272,6 +1279,43 @@ onMounted(() => {
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 {
text-align: center;
padding: 40px;
diff --git a/reading-platform-frontend/src/views/school/parents/ParentListView.vue b/reading-platform-frontend/src/views/school/parents/ParentListView.vue
index 6809838..c3c541b 100644
--- a/reading-platform-frontend/src/views/school/parents/ParentListView.vue
+++ b/reading-platform-frontend/src/views/school/parents/ParentListView.vue
@@ -335,7 +335,7 @@ import type { Parent, CreateParentDto, ParentChild, Student } from '@/api/school
// 状态辅助函数
const getParentStatusText = (status: string): string => {
- if (status === 'ACTIVE') return '活跃';
+ if (status === 'ACTIVE') return '启用';
if (status === 'INACTIVE') return '停用';
return translateGenericStatus(status);
};
diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/enums/ClassTeacherRole.java b/reading-platform-java/src/main/java/com/reading/platform/common/enums/ClassTeacherRole.java
new file mode 100644
index 0000000..88b5fd5
--- /dev/null
+++ b/reading-platform-java/src/main/java/com/reading/platform/common/enums/ClassTeacherRole.java
@@ -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);
+ }
+
+}
diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolClassController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolClassController.java
index 788eb76..e105aba 100644
--- a/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolClassController.java
+++ b/reading-platform-java/src/main/java/com/reading/platform/controller/school/SchoolClassController.java
@@ -3,6 +3,7 @@ package com.reading.platform.controller.school;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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.LogOperationType;
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.dto.request.ClassCreateRequest;
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.dto.response.ClassResponse;
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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@Tag(name = "School - Class", description = "Class Management APIs for School")
+@Slf4j
@RestController
@RequestMapping("/api/v1/school/classes")
@RequiredArgsConstructor
@@ -71,7 +76,14 @@ public class SchoolClassController {
public Result getClass(@PathVariable Long id) {
Long tenantId = SecurityUtils.getCurrentTenantId();
Clazz clazz = classService.getClassByIdWithTenantCheck(id, tenantId);
- return Result.success(classMapper.toVO(clazz));
+ ClassResponse vo = classMapper.toVO(clazz);
+
+ // 填充教师列表
+ List classTeachers = classTeacherMapper.selectList(
+ new LambdaQueryWrapper().eq(ClassTeacher::getClassId, id));
+ vo.setTeachers(buildClassTeacherResponseList(classTeachers));
+
+ return Result.success(vo);
}
@Operation(summary = "Get class page")
@@ -97,24 +109,32 @@ public class SchoolClassController {
List classTeachers = classTeacherMapper.selectList(
new LambdaQueryWrapper().eq(ClassTeacher::getClassId, vo.getId()));
- List 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("班主任".equals(ct.getRole()) || "主班".equals(ct.getRole()))
- .createdAt(ct.getCreatedAt())
- .build());
- }
+ List teacherList = buildClassTeacherResponseList(classTeachers);
vo.setTeachers(teacherList);
}
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
}
+ /**
+ * 构建班级教师响应列表
+ */
+ private List buildClassTeacherResponseList(List classTeachers) {
+ List 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")
@Log(module = LogModule.CLASS, type = LogOperationType.DELETE, description = "删除班级")
@DeleteMapping("/{id}")
@@ -124,12 +144,44 @@ public class SchoolClassController {
return Result.success();
}
- @Operation(summary = "Assign teachers to class")
- @Log(module = LogModule.CLASS, type = LogOperationType.UPDATE, description = "分配教师到班级")
+ @Operation(summary = "Add teacher to class")
+ @Log(module = LogModule.CLASS, type = LogOperationType.CREATE, description = "添加教师到班级")
@PostMapping("/{id}/teachers")
- public Result assignTeachers(@PathVariable Long id, @RequestBody List teacherIds) {
+ public Result addClassTeacher(@PathVariable Long id, @Valid @RequestBody AddClassTeacherRequest request) {
Long tenantId = SecurityUtils.getCurrentTenantId();
- classService.assignTeachersWithTenantCheck(id, tenantId, teacherIds);
+ classService.getClassByIdWithTenantCheck(id, tenantId);
+
+ // 如果设置为新主班,先将原主班降级为配班
+ if (ClassTeacherRole.isPrimaryRole(request.getRole())) {
+ List allTeachers = classTeacherMapper.selectList(
+ new LambdaQueryWrapper().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()
+ .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();
}
@@ -163,20 +215,7 @@ public class SchoolClassController {
classService.getClassByIdWithTenantCheck(id, tenantId);
List classTeachers = classTeacherMapper.selectList(
new LambdaQueryWrapper().eq(ClassTeacher::getClassId, id));
- List 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("班主任".equals(ct.getRole()) || "主班".equals(ct.getRole()))
- .createdAt(ct.getCreatedAt())
- .build());
- }
- return Result.success(teacherList);
+ return Result.success(buildClassTeacherResponseList(classTeachers));
}
@Operation(summary = "Update class teacher role")
@@ -185,11 +224,13 @@ public class SchoolClassController {
public Result updateClassTeacher(
@PathVariable Long id,
@PathVariable Long teacherId,
- @RequestBody Object request) {
+ @Valid @RequestBody UpdateClassTeacherRequest request) {
// 验证班级属于当前租户
Long tenantId = SecurityUtils.getCurrentTenantId();
classService.getClassByIdWithTenantCheck(id, tenantId);
- // TODO: 实现更新班级教师
+
+ // 调用 Service 方法更新
+ classService.updateClassTeacherRole(id, teacherId, request.getRole(), null);
return Result.success();
}
@@ -202,7 +243,12 @@ public class SchoolClassController {
// 验证班级属于当前租户
Long tenantId = SecurityUtils.getCurrentTenantId();
classService.getClassByIdWithTenantCheck(id, tenantId);
- // TODO: 实现移除班级教师
+
+ // 删除班级教师关联记录
+ classTeacherMapper.delete(new LambdaQueryWrapper()
+ .eq(ClassTeacher::getClassId, id)
+ .eq(ClassTeacher::getTeacherId, teacherId));
+
return Result.success();
}
diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/AddClassTeacherRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/AddClassTeacherRequest.java
new file mode 100644
index 0000000..8dcf930
--- /dev/null
+++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/AddClassTeacherRequest.java
@@ -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;
+}
diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/ClassCreateRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/ClassCreateRequest.java
index 6004774..d813679 100644
--- a/reading-platform-java/src/main/java/com/reading/platform/dto/request/ClassCreateRequest.java
+++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/ClassCreateRequest.java
@@ -21,4 +21,7 @@ public class ClassCreateRequest {
@Schema(description = "容量")
private Integer capacity;
+ @Schema(description = "班主任教师 ID")
+ private Long teacherId;
+
}
diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/ClassTeacherUpdateRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/ClassTeacherUpdateRequest.java
new file mode 100644
index 0000000..84856e8
--- /dev/null
+++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/ClassTeacherUpdateRequest.java
@@ -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;
+
+}
diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/ClassUpdateRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/ClassUpdateRequest.java
index 3fd2f3b..7e23e24 100644
--- a/reading-platform-java/src/main/java/com/reading/platform/dto/request/ClassUpdateRequest.java
+++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/ClassUpdateRequest.java
@@ -22,4 +22,7 @@ public class ClassUpdateRequest {
@Schema(description = "状态")
private String status;
+ @Schema(description = "班主任教师 ID")
+ private Long teacherId;
+
}
diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/UpdateClassTeacherRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/UpdateClassTeacherRequest.java
new file mode 100644
index 0000000..5e0465f
--- /dev/null
+++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/UpdateClassTeacherRequest.java
@@ -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;
+}
diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/ClassService.java b/reading-platform-java/src/main/java/com/reading/platform/service/ClassService.java
index fcc96d8..2766a2f 100644
--- a/reading-platform-java/src/main/java/com/reading/platform/service/ClassService.java
+++ b/reading-platform-java/src/main/java/com/reading/platform/service/ClassService.java
@@ -84,10 +84,15 @@ public interface ClassService extends com.baomidou.mybatisplus.extension.service
List getTeacherIdsByClassId(Long classId);
/**
- * 获取教师在班级中的角色信息(role 字段)
+ * 获取学生在班级中的角色信息(role 字段)
*/
String getTeacherRoleInClass(Long classId, Long teacherId);
+ /**
+ * 更新班级教师角色
+ */
+ void updateClassTeacherRole(Long classId, Long teacherId, String role, Boolean isPrimary);
+
/**
* 获取租户下活跃班级列表
*/
diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TeacherServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TeacherServiceImpl.java
index 25f133f..56bee76 100644
--- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TeacherServiceImpl.java
+++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TeacherServiceImpl.java
@@ -61,6 +61,9 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
throw new BusinessException(ErrorCode.DATA_ALREADY_EXISTS, "用户名已存在");
}
+ // 检查手机号是否已存在
+ checkPhoneUnique(tenantId, request.getPhone(), null);
+
Teacher teacher = new Teacher();
teacher.setTenantId(tenantId);
teacher.setUsername(request.getUsername());
@@ -97,6 +100,9 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
Teacher teacher = getTeacherById(id);
+ // 检查手机号是否已存在(排除当前教师)
+ checkPhoneUnique(teacher.getTenantId(), request.getPhone(), teacher.getId());
+
if (StringUtils.hasText(request.getName())) {
teacher.setName(request.getName());
}
@@ -326,4 +332,33 @@ public class TeacherServiceImpl extends com.baomidou.mybatisplus.extension.servi
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 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, "手机号已存在");
+ }
+ }
+
}