feat: add teacher task review APIs

bugfix/ai-assessment-evidence-rescore
陈沅 1 month ago
parent a7dd10be3a
commit 2de6b89ca9

@ -15,8 +15,22 @@ public class StudentTrainingAnswerTeacherScoreSchemaInitializer {
@PostConstruct @PostConstruct
public void initialize() { public void initialize() {
jdbcTemplate.execute("ALTER TABLE student_training_answer ADD COLUMN IF NOT EXISTS teacher_score decimal(5,2) NULL"); addColumnIfMissing("teacher_score", "decimal(5,2) NULL");
jdbcTemplate.execute("ALTER TABLE student_training_answer ADD COLUMN IF NOT EXISTS teacher_scored_by varchar(64) NULL"); addColumnIfMissing("teacher_scored_by", "varchar(64) NULL");
jdbcTemplate.execute("ALTER TABLE student_training_answer ADD COLUMN IF NOT EXISTS teacher_scored_at datetime NULL"); addColumnIfMissing("teacher_scored_at", "datetime NULL");
}
private void addColumnIfMissing(String columnName, String definition) {
Integer columnCount = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM information_schema.COLUMNS " +
"WHERE TABLE_SCHEMA = DATABASE() " +
"AND TABLE_NAME = 'student_training_answer' " +
"AND COLUMN_NAME = ?",
Integer.class,
columnName
);
if (columnCount == null || columnCount == 0) {
jdbcTemplate.execute("ALTER TABLE student_training_answer ADD COLUMN " + columnName + " " + definition);
}
} }
} }

@ -14,8 +14,9 @@ import com.sztzjy.linkCommerce.mapper.StudentTrainingAnswerMapper;
import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper; import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper;
import com.sztzjy.linkCommerce.mapper.TeachingClassStudentMapper; import com.sztzjy.linkCommerce.mapper.TeachingClassStudentMapper;
import com.sztzjy.linkCommerce.mapper.TrainingTaskMapper; import com.sztzjy.linkCommerce.mapper.TrainingTaskMapper;
import com.sztzjy.linkCommerce.mapper.UserinfoMapper;
import com.sztzjy.linkCommerce.mapper.WeightMapper; import com.sztzjy.linkCommerce.mapper.WeightMapper;
import com.sztzjy.linkCommerce.entity.dto.TeacherTaskReviewDTO; import com.sztzjy.linkCommerce.entity.dto.*;
import com.sztzjy.linkCommerce.service.ScoreRankService; import com.sztzjy.linkCommerce.service.ScoreRankService;
import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService; import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService;
import com.sztzjy.linkCommerce.service.TeacherScoreService; import com.sztzjy.linkCommerce.service.TeacherScoreService;
@ -61,6 +62,8 @@ public class TeaScoreController {
@Autowired(required = false) @Autowired(required = false)
SchoolDefaultTaskService schoolDefaultTaskService; SchoolDefaultTaskService schoolDefaultTaskService;
@Autowired @Autowired
UserinfoMapper userinfoMapper;
@Autowired
TeacherScoreService teacherScoreService; TeacherScoreService teacherScoreService;
@GetMapping("/ownedTeachingClasses") @GetMapping("/ownedTeachingClasses")
@ -156,6 +159,44 @@ public class TeaScoreController {
} }
} }
@GetMapping("/taskReview")
@ApiOperation("查询实训任务评阅工作台")
public ResultEntity<TeacherTaskReviewPageDTO> taskReview(@RequestParam String classId,
@RequestParam(required = false) String projectName,
@RequestParam(required = false) String taskId,
HttpServletRequest request) {
try {
teacherScoreService.requireAccessibleClass(currentUser(request), classId);
return new ResultEntity<>(HttpStatus.OK, "查询任务评阅成功",
buildTaskReviewPage(classId, projectName, taskId));
} catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) {
return new ResultEntity<>(e.getCode(), e.getMessage());
}
}
@PostMapping("/batchSaveTaskTeacherScore")
@ApiOperation("批量保存教师任务评分")
@Transactional
public ResultEntity<?> batchSaveTaskTeacherScore(@RequestBody BatchTaskTeacherScoreRequest body,
HttpServletRequest request) {
try {
JwtUser teacher = currentUser(request);
validateBatchTaskScoreRequest(body, teacher);
List<StudentTrainingAnswer> answers = new ArrayList<>();
for (String studentUserId : body.getStudentUserIds()) {
answers.add(requireScorableTaskAnswer(body.getClassId(), studentUserId, body.getTaskId()));
}
BigDecimal score = body.getTeacherScore().setScale(2, BigDecimal.ROUND_HALF_UP);
Date scoredAt = new Date();
for (StudentTrainingAnswer answer : answers) {
studentTrainingAnswerMapper.updateTeacherScore(answer.getId(), score, teacher.getUserId(), scoredAt);
}
return new ResultEntity<>(HttpStatus.OK, "批量任务评分保存成功", body.getStudentUserIds());
} catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) {
return new ResultEntity<>(e.getCode(), e.getMessage());
}
}
@PostMapping("/selectStuRankAndScore") @PostMapping("/selectStuRankAndScore")
@ApiOperation("查询学生成绩和排名") @ApiOperation("查询学生成绩和排名")
@AnonymousAccess @AnonymousAccess
@ -571,6 +612,140 @@ public class TeaScoreController {
return reviews; return reviews;
} }
private TeacherTaskReviewPageDTO buildTaskReviewPage(String classId, String projectName, String taskId) {
TeacherTaskReviewPageDTO page = new TeacherTaskReviewPageDTO();
List<TeacherTaskReviewOptionDTO> allTasks = new ArrayList<>();
Set<String> projectNames = new LinkedHashSet<>();
Set<String> seenTaskIds = new HashSet<>();
for (TaskAllocation allocation : resolveEnabledTaskAllocations(classId)) {
if (allocation == null || StringUtils.isBlank(allocation.getModule())) {
continue;
}
TrainingTask task = trainingTaskMapper == null ? null : trainingTaskMapper.selectByTaskKey(allocation.getModule());
if (task == null || StringUtils.isBlank(task.getId()) || !seenTaskIds.add(task.getId())) {
continue;
}
TeacherTaskReviewOptionDTO option = reviewOption(task);
allTasks.add(option);
if (StringUtils.isNotBlank(option.getProjectName())) {
projectNames.add(option.getProjectName());
}
}
page.setProjects(new ArrayList<>(projectNames));
List<TeacherTaskReviewOptionDTO> filteredTasks = allTasks.stream()
.filter(task -> StringUtils.isBlank(projectName) || StringUtils.equals(projectName, task.getProjectName()))
.collect(Collectors.toList());
page.setTasks(filteredTasks);
TeacherTaskReviewOptionDTO selected = filteredTasks.stream()
.filter(task -> StringUtils.equals(task.getTaskId(), taskId))
.findFirst().orElse(filteredTasks.isEmpty() ? null : filteredTasks.get(0));
page.setSelectedTask(selected);
if (selected == null) {
return page;
}
List<TeacherTaskReviewStudentDTO> students = new ArrayList<>();
for (TeachingClassStudent membership : teachingClassStudentMapper.selectByTeachingClassId(classId)) {
if (membership == null || StringUtils.isBlank(membership.getStudentUserId())) {
continue;
}
StudentTrainingAnswer answer = studentTrainingAnswerMapper == null ? null
: studentTrainingAnswerMapper.selectByStudentClassAndTask(membership.getStudentUserId(), classId, selected.getTaskId());
students.add(reviewStudent(membership.getStudentUserId(), answer));
}
students.sort(Comparator.comparing((TeacherTaskReviewStudentDTO student) -> student.getTeacherScore() != null)
.thenComparing(student -> StringUtils.defaultString(student.getName()))
.thenComparing(student -> StringUtils.defaultString(student.getUsername())));
page.setStudents(students);
return page;
}
private TeacherTaskReviewOptionDTO reviewOption(TrainingTask task) {
TeacherTaskReviewOptionDTO option = new TeacherTaskReviewOptionDTO();
option.setTaskId(task.getId());
option.setTaskKey(task.getTaskKey());
option.setProjectName(task.getProjectName());
option.setTaskName(task.getTaskName());
option.setSteps(task.getSteps());
return option;
}
private TeacherTaskReviewStudentDTO reviewStudent(String studentUserId, StudentTrainingAnswer answer) {
TeacherTaskReviewStudentDTO row = new TeacherTaskReviewStudentDTO();
row.setStudentUserId(studentUserId);
Userinfo student = userinfoMapper == null ? null : userinfoMapper.selectByPrimaryKey(studentUserId);
row.setName(student == null ? "" : StringUtils.defaultString(student.getName()));
row.setUsername(student == null ? "" : StringUtils.defaultString(student.getUsername()));
row.setProgressStatus(answer == null ? "NOT_STARTED" : answer.getProgressStatus());
row.setSubmitted(answer != null && Boolean.TRUE.equals(answer.getSubmitted()));
row.setScorable(isCompleted(answer));
if (answer != null) {
row.setAiAssessmentScore(answer.getAiAssessmentScore());
row.setTeacherScore(answer.getTeacherScore());
row.setStep1Answer(answer.getStep1Answer());
row.setStep2Answer(answer.getStep2Answer());
row.setStep3Answer(answer.getStep3Answer());
row.setStep4Answer(answer.getStep4Answer());
row.setDynamicStepAnswers(answer.getDynamicStepAnswers());
}
return row;
}
private void validateBatchTaskScoreRequest(BatchTaskTeacherScoreRequest body, JwtUser teacher) {
if (body == null || StringUtils.isBlank(body.getClassId()) || StringUtils.isBlank(body.getTaskId())) {
throw new com.sztzjy.linkCommerce.config.exception.handler.ServiceException(HttpStatus.BAD_REQUEST,
"教学班和实训任务不能为空");
}
teacherScoreService.requireOwnedClass(teacher, body.getClassId());
if (!isEnabledAllocatedTask(body.getClassId(), body.getTaskId())) {
throw new com.sztzjy.linkCommerce.config.exception.handler.ServiceException(HttpStatus.BAD_REQUEST,
"该实训任务未发布到当前教学班");
}
if (body.getStudentUserIds() == null || body.getStudentUserIds().isEmpty()) {
throw new com.sztzjy.linkCommerce.config.exception.handler.ServiceException(HttpStatus.BAD_REQUEST,
"请选择至少一名学生");
}
Set<String> distinctIds = new LinkedHashSet<>();
for (String studentUserId : body.getStudentUserIds()) {
if (StringUtils.isBlank(studentUserId) || !distinctIds.add(studentUserId)) {
throw new com.sztzjy.linkCommerce.config.exception.handler.ServiceException(HttpStatus.BAD_REQUEST,
"所选学生数据无效");
}
}
if (body.getTeacherScore() == null || body.getTeacherScore().compareTo(BigDecimal.ZERO) < 0
|| body.getTeacherScore().compareTo(new BigDecimal("100")) > 0
|| body.getTeacherScore().stripTrailingZeros().scale() > 0) {
throw new com.sztzjy.linkCommerce.config.exception.handler.ServiceException(HttpStatus.BAD_REQUEST,
"教师评分必须为0至100之间的整数");
}
}
private boolean isEnabledAllocatedTask(String classId, String taskId) {
for (TaskAllocation allocation : resolveEnabledTaskAllocations(classId)) {
if (allocation == null || StringUtils.isBlank(allocation.getModule())) {
continue;
}
TrainingTask task = trainingTaskMapper == null ? null : trainingTaskMapper.selectByTaskKey(allocation.getModule());
if (task != null && StringUtils.equals(taskId, task.getId())) {
return true;
}
}
return false;
}
private StudentTrainingAnswer requireScorableTaskAnswer(String classId, String studentUserId, String taskId) {
if (teachingClassStudentMapper.countByStudentUserIdAndTeachingClassId(studentUserId, classId) == 0) {
throw new com.sztzjy.linkCommerce.config.exception.handler.ServiceException(HttpStatus.BAD_REQUEST,
"该学生不在当前教学班");
}
StudentTrainingAnswer answer = studentTrainingAnswerMapper == null ? null
: studentTrainingAnswerMapper.selectByStudentClassAndTask(studentUserId, classId, taskId);
if (!isCompleted(answer)) {
throw new com.sztzjy.linkCommerce.config.exception.handler.ServiceException(HttpStatus.BAD_REQUEST,
"仅已完成的实训任务可以评分");
}
return answer;
}
// Retained for existing direct controller tests; the HTTP route is the JWT-scoped overload above. // Retained for existing direct controller tests; the HTTP route is the JWT-scoped overload above.
public void exportStuScore(HttpServletResponse response, String schoolId, String classId, String userName, String name) { public void exportStuScore(HttpServletResponse response, String schoolId, String classId, String userName, String name) {
StuRankExample example = new StuRankExample(); StuRankExample example = new StuRankExample();

@ -0,0 +1,20 @@
package com.sztzjy.linkCommerce.entity.dto;
import java.math.BigDecimal;
import java.util.List;
public class BatchTaskTeacherScoreRequest {
private String classId;
private String taskId;
private List<String> studentUserIds;
private BigDecimal teacherScore;
public String getClassId() { return classId; }
public void setClassId(String classId) { this.classId = classId; }
public String getTaskId() { return taskId; }
public void setTaskId(String taskId) { this.taskId = taskId; }
public List<String> getStudentUserIds() { return studentUserIds; }
public void setStudentUserIds(List<String> studentUserIds) { this.studentUserIds = studentUserIds; }
public BigDecimal getTeacherScore() { return teacherScore; }
public void setTeacherScore(BigDecimal teacherScore) { this.teacherScore = teacherScore; }
}

@ -0,0 +1,20 @@
package com.sztzjy.linkCommerce.entity.dto;
public class TeacherTaskReviewOptionDTO {
private String taskId;
private String projectName;
private String taskName;
private String taskKey;
private String steps;
public String getTaskId() { return taskId; }
public void setTaskId(String taskId) { this.taskId = taskId; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
public String getTaskName() { return taskName; }
public void setTaskName(String taskName) { this.taskName = taskName; }
public String getTaskKey() { return taskKey; }
public void setTaskKey(String taskKey) { this.taskKey = taskKey; }
public String getSteps() { return steps; }
public void setSteps(String steps) { this.steps = steps; }
}

@ -0,0 +1,20 @@
package com.sztzjy.linkCommerce.entity.dto;
import java.util.ArrayList;
import java.util.List;
public class TeacherTaskReviewPageDTO {
private List<String> projects = new ArrayList<>();
private List<TeacherTaskReviewOptionDTO> tasks = new ArrayList<>();
private TeacherTaskReviewOptionDTO selectedTask;
private List<TeacherTaskReviewStudentDTO> students = new ArrayList<>();
public List<String> getProjects() { return projects; }
public void setProjects(List<String> projects) { this.projects = projects; }
public List<TeacherTaskReviewOptionDTO> getTasks() { return tasks; }
public void setTasks(List<TeacherTaskReviewOptionDTO> tasks) { this.tasks = tasks; }
public TeacherTaskReviewOptionDTO getSelectedTask() { return selectedTask; }
public void setSelectedTask(TeacherTaskReviewOptionDTO selectedTask) { this.selectedTask = selectedTask; }
public List<TeacherTaskReviewStudentDTO> getStudents() { return students; }
public void setStudents(List<TeacherTaskReviewStudentDTO> students) { this.students = students; }
}

@ -0,0 +1,46 @@
package com.sztzjy.linkCommerce.entity.dto;
import java.math.BigDecimal;
public class TeacherTaskReviewStudentDTO {
private String studentUserId;
private String name;
private String username;
private String progressStatus;
private Boolean submitted;
private Boolean scorable;
private Integer aiAssessmentScore;
private BigDecimal teacherScore;
private String step1Answer;
private String step2Answer;
private String step3Answer;
private String step4Answer;
private String dynamicStepAnswers;
public String getStudentUserId() { return studentUserId; }
public void setStudentUserId(String studentUserId) { this.studentUserId = studentUserId; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getProgressStatus() { return progressStatus; }
public void setProgressStatus(String progressStatus) { this.progressStatus = progressStatus; }
public Boolean getSubmitted() { return submitted; }
public void setSubmitted(Boolean submitted) { this.submitted = submitted; }
public Boolean getScorable() { return scorable; }
public void setScorable(Boolean scorable) { this.scorable = scorable; }
public Integer getAiAssessmentScore() { return aiAssessmentScore; }
public void setAiAssessmentScore(Integer aiAssessmentScore) { this.aiAssessmentScore = aiAssessmentScore; }
public BigDecimal getTeacherScore() { return teacherScore; }
public void setTeacherScore(BigDecimal teacherScore) { this.teacherScore = teacherScore; }
public String getStep1Answer() { return step1Answer; }
public void setStep1Answer(String step1Answer) { this.step1Answer = step1Answer; }
public String getStep2Answer() { return step2Answer; }
public void setStep2Answer(String step2Answer) { this.step2Answer = step2Answer; }
public String getStep3Answer() { return step3Answer; }
public void setStep3Answer(String step3Answer) { this.step3Answer = step3Answer; }
public String getStep4Answer() { return step4Answer; }
public void setStep4Answer(String step4Answer) { this.step4Answer = step4Answer; }
public String getDynamicStepAnswers() { return dynamicStepAnswers; }
public void setDynamicStepAnswers(String dynamicStepAnswers) { this.dynamicStepAnswers = dynamicStepAnswers; }
}

@ -0,0 +1,115 @@
package com.sztzjy.linkCommerce.controller.stu;
import com.sztzjy.linkCommerce.config.security.JwtUser;
import com.sztzjy.linkCommerce.entity.StudentTrainingAnswer;
import com.sztzjy.linkCommerce.entity.TaskAllocation;
import com.sztzjy.linkCommerce.entity.TrainingTask;
import com.sztzjy.linkCommerce.entity.dto.BatchTaskTeacherScoreRequest;
import com.sztzjy.linkCommerce.mapper.StudentTrainingAnswerMapper;
import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper;
import com.sztzjy.linkCommerce.mapper.TeachingClassStudentMapper;
import com.sztzjy.linkCommerce.mapper.TrainingTaskMapper;
import com.sztzjy.linkCommerce.service.TeacherScoreService;
import com.sztzjy.linkCommerce.util.ResultEntity;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.util.ReflectionTestUtils;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class TeaScoreControllerTaskReviewTest {
@Test
void exposesAtomicBatchTaskScoringEndpoint() {
assertDoesNotThrow(() -> TeaScoreController.class.getMethod(
"batchSaveTaskTeacherScore",
Class.forName("com.sztzjy.linkCommerce.entity.dto.BatchTaskTeacherScoreRequest"),
HttpServletRequest.class));
}
@Test
void exposesTaskReviewQueryEndpoint() {
assertDoesNotThrow(() -> TeaScoreController.class.getMethod(
"taskReview", String.class, String.class, String.class, HttpServletRequest.class));
}
@Test
void batchScoreRejectsTaskThatIsNotAllocatedToTheClassBeforeWritingAnyScore() {
TeaScoreController controller = controllerWithCompletedAnswer();
TaskAllocationMapper allocationMapper = field(controller, "taskAllocationMapper");
StudentTrainingAnswerMapper answerMapper = field(controller, "studentTrainingAnswerMapper");
when(allocationMapper.selectByExample(any())).thenReturn(Collections.singletonList(allocation("another-task-key")));
ResultEntity<?> result = controller.batchSaveTaskTeacherScore(batchRequest(), new MockHttpServletRequest());
assertEquals(HttpStatus.BAD_REQUEST, result.getStatusCode());
verify(answerMapper, never()).updateTeacherScore(anyString(), any(), anyString(), any());
}
private TeaScoreController controllerWithCompletedAnswer() {
TeaScoreController controller = new TeaScoreController() {
@Override
protected JwtUser currentUser(HttpServletRequest request) {
JwtUser teacher = new JwtUser();
teacher.setUserId("teacher-1");
teacher.setSchoolId("school-1");
teacher.setRoleId(3);
return teacher;
}
};
TeacherScoreService teacherScoreService = mock(TeacherScoreService.class);
TeachingClassStudentMapper membershipMapper = mock(TeachingClassStudentMapper.class);
StudentTrainingAnswerMapper answerMapper = mock(StudentTrainingAnswerMapper.class);
TrainingTaskMapper taskMapper = mock(TrainingTaskMapper.class);
ReflectionTestUtils.setField(controller, "teacherScoreService", teacherScoreService);
ReflectionTestUtils.setField(controller, "teachingClassStudentMapper", membershipMapper);
ReflectionTestUtils.setField(controller, "studentTrainingAnswerMapper", answerMapper);
ReflectionTestUtils.setField(controller, "trainingTaskMapper", taskMapper);
ReflectionTestUtils.setField(controller, "taskAllocationMapper", mock(TaskAllocationMapper.class));
when(membershipMapper.countByStudentUserIdAndTeachingClassId("student-1", "class-1")).thenReturn(1L);
when(answerMapper.selectByStudentClassAndTask("student-1", "class-1", "task-1"))
.thenReturn(completedAnswer());
return controller;
}
@SuppressWarnings("unchecked")
private <T> T field(TeaScoreController controller, String name) {
return (T) ReflectionTestUtils.getField(controller, name);
}
private BatchTaskTeacherScoreRequest batchRequest() {
BatchTaskTeacherScoreRequest body = new BatchTaskTeacherScoreRequest();
body.setClassId("class-1");
body.setTaskId("task-1");
body.setStudentUserIds(Collections.singletonList("student-1"));
body.setTeacherScore(new BigDecimal("90"));
return body;
}
private StudentTrainingAnswer completedAnswer() {
StudentTrainingAnswer answer = new StudentTrainingAnswer();
answer.setId("answer-1");
answer.setProgressStatus("COMPLETED");
return answer;
}
private TaskAllocation allocation(String taskKey) {
TaskAllocation allocation = new TaskAllocation();
allocation.setModule(taskKey);
allocation.setDisabledStatus((byte) 0);
return allocation;
}
}
Loading…
Cancel
Save