feat(school): 新增学生调班与调班历史 API

- GET /api/v1/school/students/{id}/history 获取学生调班历史
- POST /api/v1/school/students/{id}/transfer 学生调班
- 新增 TransferStudentRequest、StudentTransferHistoryItemResponse DTO
- ClassService 新增 getStudentClassHistory 方法

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-23 16:38:54 +08:00
parent c6328dd441
commit 354071b6a3
5 changed files with 151 additions and 0 deletions

View File

@ -7,7 +7,9 @@ import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.dto.request.StudentCreateRequest;
import com.reading.platform.dto.request.StudentUpdateRequest;
import com.reading.platform.dto.request.TransferStudentRequest;
import com.reading.platform.dto.response.StudentResponse;
import com.reading.platform.dto.response.StudentTransferHistoryItemResponse;
import com.reading.platform.entity.Student;
import com.reading.platform.service.ClassService;
import com.reading.platform.service.StudentService;
@ -18,6 +20,7 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Tag(name = "School - Student", description = "Student Management APIs for School")
@RestController
@ -77,6 +80,17 @@ public class SchoolStudentController {
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
}
@Operation(summary = "Transfer student to another class")
@PostMapping("/{id}/transfer")
public Result<Map<String, String>> transferStudent(
@PathVariable Long id,
@Valid @RequestBody TransferStudentRequest request) {
Long tenantId = SecurityUtils.getCurrentTenantId();
studentService.getStudentByIdWithTenantCheck(id, tenantId);
classService.assignStudentToClass(id, request.getToClassId(), tenantId);
return Result.success(Map.of("message", "调班成功"));
}
@Operation(summary = "Delete student")
@DeleteMapping("/{id}")
public Result<Void> deleteStudent(@PathVariable Long id) {
@ -85,4 +99,13 @@ public class SchoolStudentController {
return Result.success();
}
@Operation(summary = "Get student class transfer history")
@GetMapping("/{id}/history")
public Result<List<StudentTransferHistoryItemResponse>> getStudentClassHistory(@PathVariable Long id) {
Long tenantId = SecurityUtils.getCurrentTenantId();
studentService.getStudentByIdWithTenantCheck(id, tenantId);
List<StudentTransferHistoryItemResponse> history = classService.getStudentClassHistory(id, tenantId);
return Result.success(history);
}
}

View File

@ -0,0 +1,17 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "学生调班请求")
public class TransferStudentRequest {
@NotNull(message = "目标班级 ID 不能为空")
@Schema(description = "目标班级 ID", required = true)
private Long toClassId;
@Schema(description = "调班原因")
private String reason;
}

View File

@ -0,0 +1,53 @@
package com.reading.platform.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 学生调班历史单条记录响应
* 前端期望格式fromClasstoClass 包含班级信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "学生调班历史单条记录")
public class StudentTransferHistoryItemResponse {
@Schema(description = "记录 ID")
private Long id;
@Schema(description = "调出班级(首次入园为 null")
private ClassBasicInfo fromClass;
@Schema(description = "调入班级")
private ClassBasicInfo toClass;
@Schema(description = "调班原因")
private String reason;
@Schema(description = "操作人 ID")
private Long operatedBy;
@Schema(description = "创建时间")
private LocalDateTime createdAt;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "班级基本信息")
public static class ClassBasicInfo {
@Schema(description = "班级 ID")
private Long id;
@Schema(description = "班级名称")
private String name;
@Schema(description = "年级")
private String grade;
}
}

View File

@ -3,6 +3,7 @@ package com.reading.platform.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.dto.request.ClassCreateRequest;
import com.reading.platform.dto.request.ClassUpdateRequest;
import com.reading.platform.dto.response.StudentTransferHistoryItemResponse;
import com.reading.platform.entity.Clazz;
import java.util.List;
@ -102,4 +103,9 @@ public interface ClassService extends com.baomidou.mybatisplus.extension.service
*/
Clazz getPrimaryClassByStudentId(Long studentId);
/**
* 获取学生调班历史带租户验证
*/
List<StudentTransferHistoryItemResponse> getStudentClassHistory(Long studentId, Long tenantId);
}

View File

@ -7,6 +7,7 @@ import com.reading.platform.common.enums.ErrorCode;
import com.reading.platform.common.exception.BusinessException;
import com.reading.platform.dto.request.ClassCreateRequest;
import com.reading.platform.dto.request.ClassUpdateRequest;
import com.reading.platform.dto.response.StudentTransferHistoryItemResponse;
import com.reading.platform.entity.ClassTeacher;
import com.reading.platform.entity.Clazz;
import com.reading.platform.entity.StudentClassHistory;
@ -22,6 +23,7 @@ import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
@ -337,4 +339,54 @@ public class ClassServiceImpl extends com.baomidou.mybatisplus.extension.service
return clazzMapper.selectById(history.getClassId());
}
@Override
public List<StudentTransferHistoryItemResponse> getStudentClassHistory(Long studentId, Long tenantId) {
log.debug("获取学生调班历史,学生 ID: {}, 租户 ID: {}", studentId, tenantId);
List<StudentClassHistory> histories = studentClassHistoryMapper.selectList(
new LambdaQueryWrapper<StudentClassHistory>()
.eq(StudentClassHistory::getStudentId, studentId)
.orderByAsc(StudentClassHistory::getStartDate)
);
List<StudentTransferHistoryItemResponse> result = new ArrayList<>();
Clazz prevClass = null;
for (StudentClassHistory h : histories) {
Clazz toClazz = getClassById(h.getClassId());
if (toClazz == null || !tenantId.equals(toClazz.getTenantId())) {
log.warn("调班历史引用的班级不存在或无权访问跳过。historyId: {}, classId: {}", h.getId(), h.getClassId());
continue;
}
StudentTransferHistoryItemResponse.ClassBasicInfo fromClassInfo = prevClass == null ? null
: StudentTransferHistoryItemResponse.ClassBasicInfo.builder()
.id(prevClass.getId())
.name(prevClass.getName())
.grade(prevClass.getGrade() != null ? prevClass.getGrade() : "")
.build();
StudentTransferHistoryItemResponse.ClassBasicInfo toClassInfo =
StudentTransferHistoryItemResponse.ClassBasicInfo.builder()
.id(toClazz.getId())
.name(toClazz.getName())
.grade(toClazz.getGrade() != null ? toClazz.getGrade() : "")
.build();
result.add(StudentTransferHistoryItemResponse.builder()
.id(h.getId())
.fromClass(fromClassInfo)
.toClass(toClassInfo)
.reason(null)
.operatedBy(null)
.createdAt(h.getCreatedAt())
.build());
prevClass = toClazz;
}
Collections.reverse(result);
return result;
}
}