fix: sync teacher task grades and training progress

bugfix/ai-assessment-evidence-rescore
陈沅 1 month ago
parent 1784ff010e
commit af8f337a5d

@ -1,50 +0,0 @@
# 教师成绩列表进度与评分同步设计
## 目标
教师端成绩列表展示学生已完成实训任务数与总实训任务数,并实时汇总教师在任务评阅中保存的逐任务评分;成绩列表不再允许直接录入或保存教师总分。
## 问题与根因
当前进度统计从旧的 `stu_grade` 查询已完成模块,而当前实训完成状态保存在 `student_training_answer`,导致完成实训的学生仍显示为 `0%`。当前教师评分写入单独的 `teaching_class_teacher_score` 汇总表,和逐任务评阅没有数据关联。
## 数据模型
在 `student_training_answer` 增加以下字段:
- `teacher_score`:教师对该任务的 0–100 分评分,可为空。
- `teacher_scored_by`:评分教师用户 ID,可为空。
- `teacher_scored_at`:评分时间,可为空。
每条答卷已经唯一对应学生、教学班与实训任务,因此不新增独立评分表,也不再使用人工汇总评分作为成绩列表的数据源。
## 评阅与权限
成绩详情弹窗扩展为任务评阅界面,展示当前教学班已启用的全部任务及对应任务状态、AI 评测成绩和教师评分。班级创建教师可对已提交或状态为 `COMPLETED` 的任务录入 0–100 分并保存;其他教师仅可查看。未完成任务不可评分。
成绩列表的“老师评分”列只读:尚无任何教师任务评分时显示“待评阅”,存在评分后显示实时计算结果。移除列表中的数字输入框、保存按钮和人工汇总评分接口调用。
## 实训进度
总任务数取当前教学班启用的已分配任务(使用学校默认任务回退规则,排除发布标记与未参与的综合实训)。完成任务数取同一学生、同一教学班中 `submitted = true` 或 `progress_status = COMPLETED` 的答卷,并按任务去重。
列表显示 `已完成数/总数`,并保留百分比提示;没有可用任务时显示 `--`。
## 教师评分计算
教师总分复用当前参考成绩的六模块算法:按 `TrainingTask.projectName` 把任务归入六个模块;每个模块先计算该模块所有已分配任务教师评分的平均值,未评分任务按 0 分参与平均;再乘以当前教学班的六项权重。综合实训是否参与、参与权重归一化与 `ScoreRankServiceImpl.calculateSixModuleScore` 保持一致。
若班级尚未保存权重配置,教师评分计算使用并持久化与参考成绩相同的默认六项权重,不能使用另一套默认值。
该值在成绩列表和导出查询时即时计算,不依赖“成绩更新”按钮。任务评分保存后,重新请求列表即可得到新总分。
## 接口与兼容性
- 成绩详情接口改为返回全部有效任务及答卷、AI 分数和教师评分。
- 新增受班级创建教师权限保护的逐任务评分保存接口。
- 现有 `/api/weight/saveTeacherScore` 不再由前端调用;旧汇总数据保留,不迁移、不删除。
## 测试
- 后端:验证答卷完成状态统计、无任务显示空进度、逐任务评分权限、未完成任务不能评分、按六模块权重汇总及未评分任务按零分计算。
- 前端:验证列表展示完成数/总数、教师评分只读、详情中存在任务评分控件、没有列表人工评分控件或人工评分 API 调用。

@ -0,0 +1,22 @@
package com.sztzjy.linkCommerce.config;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class StudentTrainingAnswerTeacherScoreSchemaInitializer {
private final JdbcTemplate jdbcTemplate;
public StudentTrainingAnswerTeacherScoreSchemaInitializer(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@PostConstruct
public void initialize() {
jdbcTemplate.execute("ALTER TABLE student_training_answer ADD COLUMN IF NOT EXISTS teacher_score decimal(5,2) NULL");
jdbcTemplate.execute("ALTER TABLE student_training_answer ADD COLUMN IF NOT EXISTS teacher_scored_by varchar(64) NULL");
jdbcTemplate.execute("ALTER TABLE student_training_answer ADD COLUMN IF NOT EXISTS teacher_scored_at datetime NULL");
}
}

@ -13,7 +13,9 @@ import com.sztzjy.linkCommerce.mapper.StuRankMapper;
import com.sztzjy.linkCommerce.mapper.StudentTrainingAnswerMapper; 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.WeightMapper; import com.sztzjy.linkCommerce.mapper.WeightMapper;
import com.sztzjy.linkCommerce.entity.dto.TeacherTaskReviewDTO;
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;
@ -30,11 +32,7 @@ import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Arrays; import java.util.*;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -57,6 +55,8 @@ public class TeaScoreController {
@Autowired @Autowired
TaskAllocationMapper taskAllocationMapper; TaskAllocationMapper taskAllocationMapper;
@Autowired(required = false) @Autowired(required = false)
TrainingTaskMapper trainingTaskMapper;
@Autowired(required = false)
StudentTrainingAnswerMapper studentTrainingAnswerMapper; StudentTrainingAnswerMapper studentTrainingAnswerMapper;
@Autowired(required = false) @Autowired(required = false)
SchoolDefaultTaskService schoolDefaultTaskService; SchoolDefaultTaskService schoolDefaultTaskService;
@ -122,6 +122,40 @@ public class TeaScoreController {
} }
} }
@PostMapping("/saveTaskTeacherScore")
@ApiOperation("保存教师对单个实训任务的评分")
public ResultEntity<?> saveTaskTeacherScore(@RequestParam String classId,
@RequestParam String studentUserId,
@RequestParam String taskId,
@RequestParam BigDecimal teacherScore,
HttpServletRequest request) {
try {
JwtUser teacher = currentUser(request);
teacherScoreService.requireOwnedClass(teacher, classId);
if (teachingClassStudentMapper.countByStudentUserIdAndTeachingClassId(studentUserId, classId) == 0) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "该学生不在当前教学班");
}
StudentTrainingAnswer answer = studentTrainingAnswerMapper == null ? null
: studentTrainingAnswerMapper.selectByStudentClassAndTask(studentUserId, classId, taskId);
if (answer == null || !isCompleted(answer)) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "仅已完成的实训任务可以评分");
}
if (teacherScore == null || teacherScore.compareTo(BigDecimal.ZERO) < 0
|| teacherScore.compareTo(new BigDecimal("100")) > 0) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, "教师评分必须为0至100之间的数值");
}
Date scoredAt = new Date();
studentTrainingAnswerMapper.updateTeacherScore(answer.getId(), teacherScore.setScale(2, BigDecimal.ROUND_HALF_UP),
teacher.getUserId(), scoredAt);
answer.setTeacherScore(teacherScore.setScale(2, BigDecimal.ROUND_HALF_UP));
answer.setTeacherScoredBy(teacher.getUserId());
answer.setTeacherScoredAt(scoredAt);
return new ResultEntity<>(HttpStatus.OK, "任务评分保存成功", answer);
} catch (com.sztzjy.linkCommerce.config.exception.handler.ServiceException e) {
return new ResultEntity<>(e.getCode(), e.getMessage());
}
}
@PostMapping("/selectStuRankAndScore") @PostMapping("/selectStuRankAndScore")
@ApiOperation("查询学生成绩和排名") @ApiOperation("查询学生成绩和排名")
@AnonymousAccess @AnonymousAccess
@ -185,10 +219,9 @@ public class TeaScoreController {
if (teachingClassStudentMapper.countByStudentUserIdAndTeachingClassId(userId, teachingClass.getSchoolClassId()) == 0) { if (teachingClassStudentMapper.countByStudentUserIdAndTeachingClassId(userId, teachingClass.getSchoolClassId()) == 0) {
return new ResultEntity<>(HttpStatus.FORBIDDEN, "无权查看该学生成绩"); return new ResultEntity<>(HttpStatus.FORBIDDEN, "无权查看该学生成绩");
} }
if (studentTrainingAnswerMapper != null) { if (studentTrainingAnswerMapper != null && trainingTaskMapper != null) {
List<StudentTrainingAnswer> answers = studentTrainingAnswerMapper.selectByStudentAndTeachingClass(userId, classId);
return new ResultEntity<>(HttpStatus.OK, "查看学生成绩详情成功", return new ResultEntity<>(HttpStatus.OK, "查看学生成绩详情成功",
answers == null ? Collections.emptyList() : answers); buildTaskReviews(userId, classId));
} }
// 定义模块的固定排序顺序 // 定义模块的固定排序顺序
List<String> moduleOrder = Arrays.asList( List<String> moduleOrder = Arrays.asList(
@ -357,70 +390,56 @@ public class TeaScoreController {
if (ranks == null || ranks.isEmpty()) { if (ranks == null || ranks.isEmpty()) {
return; return;
} }
Map<String, BigDecimal> classTeacherScores = Collections.emptyMap();
if (teacherScoreService != null && StringUtils.isNotBlank(requestedClassId)) {
Map<String, BigDecimal> loadedScores = teacherScoreService.getTeacherScores(requestedClassId, ranks.stream()
.map(StuRank::getUserId)
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList()));
if (loadedScores != null) {
classTeacherScores = loadedScores;
}
}
for (StuRank rank : ranks) { for (StuRank rank : ranks) {
String classId = StringUtils.isNotBlank(requestedClassId) ? requestedClassId : rank.getSchoolClassId(); String classId = StringUtils.isNotBlank(requestedClassId) ? requestedClassId : rank.getSchoolClassId();
rank.setTeacherScore(StringUtils.isNotBlank(requestedClassId) List<TaskAllocation> allocations = resolveEnabledTaskAllocations(classId);
? classTeacherScores.get(rank.getUserId()) int totalTaskCount = (int) allocations.stream().map(TaskAllocation::getModule)
: teacherScoreService == null ? null : teacherScoreService.getTeacherScore(classId, rank.getUserId())); .filter(StringUtils::isNotBlank).distinct().count();
Set<String> enabledModules = resolveEnabledTaskModules(classId);
int totalTaskCount = enabledModules.size();
rank.setTotalTaskCount(totalTaskCount); rank.setTotalTaskCount(totalTaskCount);
if (totalTaskCount == 0) { if (totalTaskCount == 0) {
rank.setCompletedTaskCount(0); rank.setCompletedTaskCount(0);
rank.setTrainingProgress(null); rank.setTrainingProgress(null);
rank.setTeacherScore(null);
continue; continue;
} }
List<StudentTrainingAnswer> answers = studentTrainingAnswerMapper == null ? Collections.emptyList()
StuGradeExample gradeExample = new StuGradeExample(); : studentTrainingAnswerMapper.selectByStudentAndTeachingClass(rank.getUserId(), classId);
StuGradeExample.Criteria gradeCriteria = gradeExample.createCriteria().andUserIdEqualTo(rank.getUserId()); Map<String, StudentTrainingAnswer> answersByTaskKey = indexAnswersByTaskKey(answers);
if (StringUtils.isNotBlank(classId)) { int completedTaskCount = (int) allocations.stream().map(TaskAllocation::getModule)
gradeCriteria.andSchoolClassIdEqualTo(classId); .filter(StringUtils::isNotBlank).distinct()
} .filter(taskKey -> isCompleted(answersByTaskKey.get(taskKey))).count();
List<StuGrade> grades = stuGradeMapper == null ? Collections.emptyList() : stuGradeMapper.selectByExample(gradeExample);
int completedTaskCount = (int) (grades == null ? Collections.<StuGrade>emptyList() : grades).stream()
.map(StuGrade::getModule)
.filter(enabledModules::contains)
.distinct()
.count();
rank.setCompletedTaskCount(completedTaskCount); rank.setCompletedTaskCount(completedTaskCount);
rank.setTrainingProgress(BigDecimal.valueOf(completedTaskCount) rank.setTrainingProgress(BigDecimal.valueOf(completedTaskCount)
.multiply(BigDecimal.valueOf(100)) .multiply(BigDecimal.valueOf(100))
.divide(BigDecimal.valueOf(totalTaskCount), 2, BigDecimal.ROUND_HALF_UP)); .divide(BigDecimal.valueOf(totalTaskCount), 2, BigDecimal.ROUND_HALF_UP));
rank.setTeacherScore(calculateTeacherScore(classId, allocations, answersByTaskKey));
} }
} }
private Set<String> resolveEnabledTaskModules(String classId) { private List<TaskAllocation> resolveEnabledTaskAllocations(String classId) {
if (StringUtils.isBlank(classId)) { if (StringUtils.isBlank(classId)) {
return Collections.emptySet(); return Collections.emptyList();
} }
if (taskAllocationMapper == null) {
return Collections.emptyList();
}
List<TaskAllocation> classAllocations = selectTaskAllocations(classId);
boolean comprehensiveParticipating = hasPublishedComprehensiveTask(classAllocations);
List<TaskAllocation> allocations; List<TaskAllocation> allocations;
if (schoolDefaultTaskService != null) { if (schoolDefaultTaskService != null) {
allocations = schoolDefaultTaskService.resolveForTeachingClass(classId); allocations = schoolDefaultTaskService.resolveForTeachingClass(classId);
} else { } else {
if (taskAllocationMapper == null) { allocations = classAllocations;
return Collections.emptySet();
}
allocations = selectTaskAllocations(classId);
if ((allocations == null || allocations.isEmpty()) && !"999999999".equals(classId)) { if ((allocations == null || allocations.isEmpty()) && !"999999999".equals(classId)) {
allocations = selectTaskAllocations("999999999"); allocations = selectTaskAllocations("999999999");
} }
} }
return (allocations == null ? Collections.<TaskAllocation>emptyList() : allocations).stream() return (allocations == null ? Collections.<TaskAllocation>emptyList() : allocations).stream()
.filter(allocation -> allocation.getDisabledStatus() == null || allocation.getDisabledStatus() != 1) .filter(allocation -> allocation.getDisabledStatus() == null || allocation.getDisabledStatus() != 1)
.map(TaskAllocation::getModule) .filter(allocation -> !TaskAllocation.PUBLICATION_MARKER.equals(allocation.getModule()))
.filter(StringUtils::isNotBlank) .filter(allocation -> !"comprehensive-case-training".equals(allocation.getModule())
.collect(Collectors.toSet()); || comprehensiveParticipating)
.collect(Collectors.toList());
} }
private List<TaskAllocation> selectTaskAllocations(String classId) { private List<TaskAllocation> selectTaskAllocations(String classId) {
@ -429,6 +448,129 @@ public class TeaScoreController {
return taskAllocationMapper.selectByExample(example); return taskAllocationMapper.selectByExample(example);
} }
private boolean hasPublishedComprehensiveTask(List<TaskAllocation> allocations) {
boolean published = false;
boolean enabled = false;
for (TaskAllocation allocation : allocations == null ? Collections.<TaskAllocation>emptyList() : allocations) {
if (allocation == null) {
continue;
}
if (TaskAllocation.PUBLICATION_MARKER.equals(allocation.getModule())) {
published = true;
} else if ("comprehensive-case-training".equals(allocation.getModule())
&& (allocation.getDisabledStatus() == null || allocation.getDisabledStatus() == 0)) {
enabled = true;
}
}
return published && enabled;
}
private Map<String, StudentTrainingAnswer> indexAnswersByTaskKey(List<StudentTrainingAnswer> answers) {
Map<String, StudentTrainingAnswer> indexed = new HashMap<>();
for (StudentTrainingAnswer answer : answers == null ? Collections.<StudentTrainingAnswer>emptyList() : answers) {
if (answer != null && StringUtils.isNotBlank(answer.getTaskKey())) {
indexed.put(answer.getTaskKey(), answer);
}
}
return indexed;
}
private boolean isCompleted(StudentTrainingAnswer answer) {
return answer != null && (Boolean.TRUE.equals(answer.getSubmitted())
|| "COMPLETED".equals(answer.getProgressStatus()));
}
private BigDecimal calculateTeacherScore(String classId, List<TaskAllocation> allocations,
Map<String, StudentTrainingAnswer> answersByTaskKey) {
if (teacherScoreService == null || trainingTaskMapper == null) {
return null;
}
Map<String, List<BigDecimal>> scoresByProject = new HashMap<>();
boolean hasTeacherScore = false;
boolean comprehensiveParticipating = false;
for (TaskAllocation allocation : allocations) {
if (allocation == null || StringUtils.isBlank(allocation.getModule())) {
continue;
}
if ("comprehensive-case-training".equals(allocation.getModule())) {
comprehensiveParticipating = true;
}
TrainingTask task = trainingTaskMapper.selectByTaskKey(allocation.getModule());
if (task == null || StringUtils.isBlank(task.getProjectName())) {
continue;
}
StudentTrainingAnswer answer = answersByTaskKey.get(allocation.getModule());
BigDecimal taskScore = answer == null ? null : answer.getTeacherScore();
if (taskScore != null) {
hasTeacherScore = true;
}
scoresByProject.computeIfAbsent(task.getProjectName(), key -> new ArrayList<>())
.add(taskScore == null ? BigDecimal.ZERO : taskScore);
}
if (!hasTeacherScore) {
return null;
}
TeachingClassScoreWeight weight = teacherScoreService.getOrCreateWeightForCalculation(classId);
if (weight == null) {
return null;
}
BigDecimal weightedTotal = projectAverage(scoresByProject, "互联网产品开发基础认知").multiply(safeWeight(weight.getFoundationWeight()))
.add(projectAverage(scoresByProject, "市场洞察与需求分析").multiply(safeWeight(weight.getMarketInsightWeight())))
.add(projectAverage(scoresByProject, "产品规划与设计").multiply(safeWeight(weight.getPlanningDesignWeight())))
.add(projectAverage(scoresByProject, "产品开发与测试验证").multiply(safeWeight(weight.getDevelopmentValidationWeight())))
.add(projectAverage(scoresByProject, "产品上线与运营推广").multiply(safeWeight(weight.getLaunchOperationWeight())))
.add(projectAverage(scoresByProject, "综合实训").multiply(safeWeight(weight.getComprehensiveTrainingWeight())));
BigDecimal participatingWeight = safeWeight(weight.getFoundationWeight()).add(safeWeight(weight.getMarketInsightWeight()))
.add(safeWeight(weight.getPlanningDesignWeight())).add(safeWeight(weight.getDevelopmentValidationWeight()))
.add(safeWeight(weight.getLaunchOperationWeight()));
if (comprehensiveParticipating) {
participatingWeight = participatingWeight.add(safeWeight(weight.getComprehensiveTrainingWeight()));
}
return weightedTotal.divide(participatingWeight.signum() == 0 ? BigDecimal.ONE : participatingWeight,
2, BigDecimal.ROUND_HALF_UP);
}
private BigDecimal projectAverage(Map<String, List<BigDecimal>> scoresByProject, String projectName) {
List<BigDecimal> scores = scoresByProject.get(projectName);
if (scores == null || scores.isEmpty()) {
return BigDecimal.ZERO;
}
BigDecimal total = scores.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
return total.divide(BigDecimal.valueOf(scores.size()), 4, BigDecimal.ROUND_HALF_UP);
}
private BigDecimal safeWeight(BigDecimal weight) {
return weight == null ? BigDecimal.ZERO : weight;
}
private List<TeacherTaskReviewDTO> buildTaskReviews(String studentUserId, String classId) {
Map<String, StudentTrainingAnswer> answersByTaskKey = indexAnswersByTaskKey(
studentTrainingAnswerMapper.selectByStudentAndTeachingClass(studentUserId, classId));
List<TeacherTaskReviewDTO> reviews = new ArrayList<>();
Set<String> seenTaskKeys = new HashSet<>();
for (TaskAllocation allocation : resolveEnabledTaskAllocations(classId)) {
if (allocation == null || StringUtils.isBlank(allocation.getModule()) || !seenTaskKeys.add(allocation.getModule())) {
continue;
}
TrainingTask task = trainingTaskMapper.selectByTaskKey(allocation.getModule());
if (task == null) {
continue;
}
StudentTrainingAnswer answer = answersByTaskKey.get(allocation.getModule());
TeacherTaskReviewDTO review = new TeacherTaskReviewDTO();
review.setTaskId(task.getId());
review.setTaskKey(task.getTaskKey());
review.setTaskName(task.getTaskName());
review.setProjectName(task.getProjectName());
review.setProgressStatus(answer == null ? "NOT_STARTED" : answer.getProgressStatus());
review.setSubmitted(answer != null && Boolean.TRUE.equals(answer.getSubmitted()));
review.setAiAssessmentScore(answer == null ? null : answer.getAiAssessmentScore());
review.setTeacherScore(answer == null ? null : answer.getTeacherScore());
reviews.add(review);
}
return reviews;
}
// 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();

@ -3,6 +3,7 @@ package com.sztzjy.linkCommerce.entity;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import java.util.Date; import java.util.Date;
import java.math.BigDecimal;
public class StudentTrainingAnswer { public class StudentTrainingAnswer {
@ApiModelProperty("主键ID") @ApiModelProperty("主键ID")
@ -49,6 +50,15 @@ public class StudentTrainingAnswer {
private Integer aiAssessmentScore; private Integer aiAssessmentScore;
@ApiModelProperty("教师任务评分")
private BigDecimal teacherScore;
@ApiModelProperty("教师评分人")
private String teacherScoredBy;
@ApiModelProperty("教师评分时间")
private Date teacherScoredAt;
@ApiModelProperty("保存动作:SAVE/SUBMIT/RESET") @ApiModelProperty("保存动作:SAVE/SUBMIT/RESET")
private String saveAction; private String saveAction;
@ -181,6 +191,13 @@ public class StudentTrainingAnswer {
this.aiAssessmentScore = aiAssessmentScore; this.aiAssessmentScore = aiAssessmentScore;
} }
public BigDecimal getTeacherScore() { return teacherScore; }
public void setTeacherScore(BigDecimal teacherScore) { this.teacherScore = teacherScore; }
public String getTeacherScoredBy() { return teacherScoredBy; }
public void setTeacherScoredBy(String teacherScoredBy) { this.teacherScoredBy = teacherScoredBy == null ? null : teacherScoredBy.trim(); }
public Date getTeacherScoredAt() { return teacherScoredAt; }
public void setTeacherScoredAt(Date teacherScoredAt) { this.teacherScoredAt = teacherScoredAt; }
public String getSaveAction() { public String getSaveAction() {
return saveAction; return saveAction;
} }

@ -0,0 +1,31 @@
package com.sztzjy.linkCommerce.entity.dto;
import java.math.BigDecimal;
public class TeacherTaskReviewDTO {
private String taskId;
private String taskKey;
private String taskName;
private String projectName;
private String progressStatus;
private Boolean submitted;
private Integer aiAssessmentScore;
private BigDecimal teacherScore;
public String getTaskId() { return taskId; }
public void setTaskId(String taskId) { this.taskId = taskId; }
public String getTaskKey() { return taskKey; }
public void setTaskKey(String taskKey) { this.taskKey = taskKey; }
public String getTaskName() { return taskName; }
public void setTaskName(String taskName) { this.taskName = taskName; }
public String getProjectName() { return projectName; }
public void setProjectName(String projectName) { this.projectName = projectName; }
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 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; }
}

@ -27,6 +27,11 @@ public interface StudentTrainingAnswerMapper {
@Param("score") Integer score, @Param("score") Integer score,
@Param("updateTime") Date updateTime); @Param("updateTime") Date updateTime);
int updateTeacherScore(@Param("id") String id,
@Param("score") java.math.BigDecimal score,
@Param("teacherUserId") String teacherUserId,
@Param("scoredAt") Date scoredAt);
List<StudentTrainingAnswer> selectByStudentAndTeachingClass(@Param("studentUserId") String studentUserId, List<StudentTrainingAnswer> selectByStudentAndTeachingClass(@Param("studentUserId") String studentUserId,
@Param("teachingClassId") String teachingClassId); @Param("teachingClassId") String teachingClassId);
} }

@ -15,6 +15,7 @@ public interface TeacherScoreService {
SchoolClass requireOwnedClass(JwtUser teacher, String teachingClassId); SchoolClass requireOwnedClass(JwtUser teacher, String teachingClassId);
SchoolClass requireAccessibleClass(JwtUser teacher, String teachingClassId); SchoolClass requireAccessibleClass(JwtUser teacher, String teachingClassId);
TeachingClassScoreWeight getOrCreateWeight(JwtUser teacher, String teachingClassId); TeachingClassScoreWeight getOrCreateWeight(JwtUser teacher, String teachingClassId);
TeachingClassScoreWeight getOrCreateWeightForCalculation(String teachingClassId);
TeachingClassScoreWeight saveWeight(JwtUser teacher, String teachingClassId, TeachingClassScoreWeight weight); TeachingClassScoreWeight saveWeight(JwtUser teacher, String teachingClassId, TeachingClassScoreWeight weight);
BigDecimal getTeacherScore(String teachingClassId, String studentUserId); BigDecimal getTeacherScore(String teachingClassId, String studentUserId);
Map<String, BigDecimal> getTeacherScores(String teachingClassId, List<String> studentUserIds); Map<String, BigDecimal> getTeacherScores(String teachingClassId, List<String> studentUserIds);

@ -97,6 +97,11 @@ public class TeacherScoreServiceImpl implements TeacherScoreService {
@Override @Override
public TeachingClassScoreWeight getOrCreateWeight(JwtUser teacher, String teachingClassId) { public TeachingClassScoreWeight getOrCreateWeight(JwtUser teacher, String teachingClassId) {
requireOwnedClass(teacher, teachingClassId); requireOwnedClass(teacher, teachingClassId);
return getOrCreateWeightForCalculation(teachingClassId);
}
@Override
public TeachingClassScoreWeight getOrCreateWeightForCalculation(String teachingClassId) {
TeachingClassScoreWeight existing = weightMapper == null ? null : weightMapper.selectByTeachingClassId(teachingClassId); TeachingClassScoreWeight existing = weightMapper == null ? null : weightMapper.selectByTeachingClassId(teachingClassId);
if (existing != null) { if (existing != null) {
return existing; return existing;

@ -17,6 +17,9 @@
<result column="progress_status" jdbcType="VARCHAR" property="progressStatus" /> <result column="progress_status" jdbcType="VARCHAR" property="progressStatus" />
<result column="submitted" jdbcType="BIT" property="submitted" /> <result column="submitted" jdbcType="BIT" property="submitted" />
<result column="ai_assessment_score" jdbcType="INTEGER" property="aiAssessmentScore" /> <result column="ai_assessment_score" jdbcType="INTEGER" property="aiAssessmentScore" />
<result column="teacher_score" jdbcType="DECIMAL" property="teacherScore" />
<result column="teacher_scored_by" jdbcType="VARCHAR" property="teacherScoredBy" />
<result column="teacher_scored_at" jdbcType="TIMESTAMP" property="teacherScoredAt" />
<result column="submit_time" jdbcType="TIMESTAMP" property="submitTime" /> <result column="submit_time" jdbcType="TIMESTAMP" property="submitTime" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" /> <result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" /> <result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
@ -24,7 +27,7 @@
<sql id="Base_Column_List"> <sql id="Base_Column_List">
id, student_user_id, teaching_class_id, task_id, task_key, task_name, step1_answer, step2_answer, id, student_user_id, teaching_class_id, task_id, task_key, task_name, step1_answer, step2_answer,
step3_answer, step4_answer, dynamic_step_answers, current_step, progress_status, submitted, ai_assessment_score, submit_time, create_time, update_time step3_answer, step4_answer, dynamic_step_answers, current_step, progress_status, submitted, ai_assessment_score, teacher_score, teacher_scored_by, teacher_scored_at, submit_time, create_time, update_time
</sql> </sql>
<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap"> <select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap">
@ -75,6 +78,9 @@
<if test="progressStatus != null">progress_status,</if> <if test="progressStatus != null">progress_status,</if>
<if test="submitted != null">submitted,</if> <if test="submitted != null">submitted,</if>
<if test="aiAssessmentScore != null">ai_assessment_score,</if> <if test="aiAssessmentScore != null">ai_assessment_score,</if>
<if test="teacherScore != null">teacher_score,</if>
<if test="teacherScoredBy != null">teacher_scored_by,</if>
<if test="teacherScoredAt != null">teacher_scored_at,</if>
<if test="submitTime != null">submit_time,</if> <if test="submitTime != null">submit_time,</if>
<if test="createTime != null">create_time,</if> <if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if> <if test="updateTime != null">update_time,</if>
@ -95,6 +101,9 @@
<if test="progressStatus != null">#{progressStatus,jdbcType=VARCHAR},</if> <if test="progressStatus != null">#{progressStatus,jdbcType=VARCHAR},</if>
<if test="submitted != null">#{submitted,jdbcType=BIT},</if> <if test="submitted != null">#{submitted,jdbcType=BIT},</if>
<if test="aiAssessmentScore != null">#{aiAssessmentScore,jdbcType=INTEGER},</if> <if test="aiAssessmentScore != null">#{aiAssessmentScore,jdbcType=INTEGER},</if>
<if test="teacherScore != null">#{teacherScore,jdbcType=DECIMAL},</if>
<if test="teacherScoredBy != null">#{teacherScoredBy,jdbcType=VARCHAR},</if>
<if test="teacherScoredAt != null">#{teacherScoredAt,jdbcType=TIMESTAMP},</if>
<if test="submitTime != null">#{submitTime,jdbcType=TIMESTAMP},</if> <if test="submitTime != null">#{submitTime,jdbcType=TIMESTAMP},</if>
<if test="createTime != null">#{createTime,jdbcType=TIMESTAMP},</if> <if test="createTime != null">#{createTime,jdbcType=TIMESTAMP},</if>
<if test="updateTime != null">#{updateTime,jdbcType=TIMESTAMP},</if> <if test="updateTime != null">#{updateTime,jdbcType=TIMESTAMP},</if>
@ -118,6 +127,9 @@
<if test="progressStatus != null">progress_status = #{progressStatus,jdbcType=VARCHAR},</if> <if test="progressStatus != null">progress_status = #{progressStatus,jdbcType=VARCHAR},</if>
<if test="submitted != null">submitted = #{submitted,jdbcType=BIT},</if> <if test="submitted != null">submitted = #{submitted,jdbcType=BIT},</if>
<if test="aiAssessmentScore != null">ai_assessment_score = #{aiAssessmentScore,jdbcType=INTEGER},</if> <if test="aiAssessmentScore != null">ai_assessment_score = #{aiAssessmentScore,jdbcType=INTEGER},</if>
<if test="teacherScore != null">teacher_score = #{teacherScore,jdbcType=DECIMAL},</if>
<if test="teacherScoredBy != null">teacher_scored_by = #{teacherScoredBy,jdbcType=VARCHAR},</if>
<if test="teacherScoredAt != null">teacher_scored_at = #{teacherScoredAt,jdbcType=TIMESTAMP},</if>
<if test="submitTime != null">submit_time = #{submitTime,jdbcType=TIMESTAMP},</if> <if test="submitTime != null">submit_time = #{submitTime,jdbcType=TIMESTAMP},</if>
<if test="createTime != null">create_time = #{createTime,jdbcType=TIMESTAMP},</if> <if test="createTime != null">create_time = #{createTime,jdbcType=TIMESTAMP},</if>
<if test="updateTime != null">update_time = #{updateTime,jdbcType=TIMESTAMP},</if> <if test="updateTime != null">update_time = #{updateTime,jdbcType=TIMESTAMP},</if>
@ -131,4 +143,13 @@
update_time = #{updateTime,jdbcType=TIMESTAMP} update_time = #{updateTime,jdbcType=TIMESTAMP}
where id = #{id,jdbcType=VARCHAR} where id = #{id,jdbcType=VARCHAR}
</update> </update>
<update id="updateTeacherScore">
update student_training_answer
set teacher_score = #{score,jdbcType=DECIMAL},
teacher_scored_by = #{teacherUserId,jdbcType=VARCHAR},
teacher_scored_at = #{scoredAt,jdbcType=TIMESTAMP},
update_time = #{scoredAt,jdbcType=TIMESTAMP}
where id = #{id,jdbcType=VARCHAR}
</update>
</mapper> </mapper>

@ -3,11 +3,13 @@ package com.sztzjy.linkCommerce.controller.stu;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.sztzjy.linkCommerce.entity.StuGrade; import com.sztzjy.linkCommerce.entity.StuGrade;
import com.sztzjy.linkCommerce.entity.StuRank; import com.sztzjy.linkCommerce.entity.StuRank;
import com.sztzjy.linkCommerce.entity.StudentTrainingAnswer;
import com.sztzjy.linkCommerce.entity.TaskAllocation; import com.sztzjy.linkCommerce.entity.TaskAllocation;
import com.sztzjy.linkCommerce.mapper.StuGradeMapper; import com.sztzjy.linkCommerce.mapper.StuGradeMapper;
import com.sztzjy.linkCommerce.mapper.StuRankMapper; import com.sztzjy.linkCommerce.mapper.StuRankMapper;
import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper; import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper;
import com.sztzjy.linkCommerce.mapper.SchoolClassMapper; import com.sztzjy.linkCommerce.mapper.SchoolClassMapper;
import com.sztzjy.linkCommerce.mapper.StudentTrainingAnswerMapper;
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.util.ResultEntity; import com.sztzjy.linkCommerce.util.ResultEntity;
@ -31,12 +33,13 @@ class TeaScoreControllerProgressTest {
TeaScoreController controller = controllerWithMocks(); TeaScoreController controller = controllerWithMocks();
StuRankMapper rankMapper = field(controller, "stuRankMapper"); StuRankMapper rankMapper = field(controller, "stuRankMapper");
TaskAllocationMapper taskMapper = field(controller, "taskAllocationMapper"); TaskAllocationMapper taskMapper = field(controller, "taskAllocationMapper");
StuGradeMapper gradeMapper = field(controller, "stuGradeMapper"); StudentTrainingAnswerMapper answerMapper = field(controller, "studentTrainingAnswerMapper");
StuRank rank = rank("student-1", "class-1"); StuRank rank = rank("student-1", "class-1");
when(rankMapper.selectByExample(any())).thenReturn(Collections.singletonList(rank)); when(rankMapper.selectByExample(any())).thenReturn(Collections.singletonList(rank));
when(taskMapper.selectByExample(any())).thenReturn(Arrays.asList(task("任务A", (byte) 0), task("任务B", (byte) 0), task("任务C", (byte) 1))); when(taskMapper.selectByExample(any())).thenReturn(Arrays.asList(task("任务A", (byte) 0), task("任务B", (byte) 0), task("任务C", (byte) 1)));
when(gradeMapper.selectByExample(any())).thenReturn(Arrays.asList(grade("任务A"), grade("任务A"), grade("任务C"))); when(answerMapper.selectByStudentAndTeachingClass("student-1", "class-1"))
.thenReturn(Arrays.asList(answer("任务A", "COMPLETED"), answer("任务A", "COMPLETED"), answer("任务C", "COMPLETED")));
ResultEntity<PageInfo<StuRank>> result = controller.selectStuRankAndScore("class-1", null, null, "school-1", 1, 10); ResultEntity<PageInfo<StuRank>> result = controller.selectStuRankAndScore("class-1", null, null, "school-1", 1, 10);
StuRank row = result.getBody().getData().getList().get(0); StuRank row = result.getBody().getData().getList().get(0);
@ -67,13 +70,14 @@ class TeaScoreControllerProgressTest {
void scoreProgressUsesSchoolDefaultResolverWhenTeachingClassHasNoOwnTasks() { void scoreProgressUsesSchoolDefaultResolverWhenTeachingClassHasNoOwnTasks() {
TeaScoreController controller = controllerWithMocks(); TeaScoreController controller = controllerWithMocks();
StuRankMapper rankMapper = field(controller, "stuRankMapper"); StuRankMapper rankMapper = field(controller, "stuRankMapper");
StuGradeMapper gradeMapper = field(controller, "stuGradeMapper"); StudentTrainingAnswerMapper answerMapper = field(controller, "studentTrainingAnswerMapper");
SchoolDefaultTaskService defaultTaskService = mock(SchoolDefaultTaskService.class); SchoolDefaultTaskService defaultTaskService = mock(SchoolDefaultTaskService.class);
ReflectionTestUtils.setField(controller, "schoolDefaultTaskService", defaultTaskService); ReflectionTestUtils.setField(controller, "schoolDefaultTaskService", defaultTaskService);
StuRank rank = rank("student-1", "class-1"); StuRank rank = rank("student-1", "class-1");
when(rankMapper.selectByExample(any())).thenReturn(Collections.singletonList(rank)); when(rankMapper.selectByExample(any())).thenReturn(Collections.singletonList(rank));
when(defaultTaskService.resolveForTeachingClass("class-1")).thenReturn(Collections.singletonList(task("学校默认任务", (byte) 0))); when(defaultTaskService.resolveForTeachingClass("class-1")).thenReturn(Collections.singletonList(task("学校默认任务", (byte) 0)));
when(gradeMapper.selectByExample(any())).thenReturn(Collections.singletonList(grade("学校默认任务"))); when(answerMapper.selectByStudentAndTeachingClass("student-1", "class-1"))
.thenReturn(Collections.singletonList(answer("学校默认任务", "COMPLETED")));
ResultEntity<PageInfo<StuRank>> result = controller.selectStuRankAndScore("class-1", null, null, "school-1", 1, 10); ResultEntity<PageInfo<StuRank>> result = controller.selectStuRankAndScore("class-1", null, null, "school-1", 1, 10);
StuRank row = result.getBody().getData().getList().get(0); StuRank row = result.getBody().getData().getList().get(0);
@ -82,11 +86,36 @@ class TeaScoreControllerProgressTest {
assertEquals(new BigDecimal("100.00"), row.getTrainingProgress()); assertEquals(new BigDecimal("100.00"), row.getTrainingProgress());
} }
@Test
void selectStuRankAndScoreCountsCompletedTrainingAnswersInsteadOfLegacyGrades() {
TeaScoreController controller = controllerWithMocks();
StuRankMapper rankMapper = field(controller, "stuRankMapper");
TaskAllocationMapper taskMapper = field(controller, "taskAllocationMapper");
StudentTrainingAnswerMapper answerMapper = field(controller, "studentTrainingAnswerMapper");
StuRank rank = rank("student-1", "class-1");
StudentTrainingAnswer completed = new StudentTrainingAnswer();
completed.setTaskKey("任务A");
completed.setProgressStatus("COMPLETED");
when(rankMapper.selectByExample(any())).thenReturn(Collections.singletonList(rank));
when(taskMapper.selectByExample(any())).thenReturn(Arrays.asList(task("任务A", (byte) 0), task("任务B", (byte) 0)));
when(answerMapper.selectByStudentAndTeachingClass("student-1", "class-1"))
.thenReturn(Collections.singletonList(completed));
ResultEntity<PageInfo<StuRank>> result = controller.selectStuRankAndScore("class-1", null, null, "school-1", 1, 10);
StuRank row = result.getBody().getData().getList().get(0);
assertEquals(1, row.getCompletedTaskCount());
assertEquals(2, row.getTotalTaskCount());
assertEquals(new BigDecimal("50.00"), row.getTrainingProgress());
}
private TeaScoreController controllerWithMocks() { private TeaScoreController controllerWithMocks() {
TeaScoreController controller = new TeaScoreController(); TeaScoreController controller = new TeaScoreController();
ReflectionTestUtils.setField(controller, "stuRankMapper", mock(StuRankMapper.class)); ReflectionTestUtils.setField(controller, "stuRankMapper", mock(StuRankMapper.class));
ReflectionTestUtils.setField(controller, "stuGradeMapper", mock(StuGradeMapper.class)); ReflectionTestUtils.setField(controller, "stuGradeMapper", mock(StuGradeMapper.class));
ReflectionTestUtils.setField(controller, "taskAllocationMapper", mock(TaskAllocationMapper.class)); ReflectionTestUtils.setField(controller, "taskAllocationMapper", mock(TaskAllocationMapper.class));
ReflectionTestUtils.setField(controller, "studentTrainingAnswerMapper", mock(StudentTrainingAnswerMapper.class));
ReflectionTestUtils.setField(controller, "schoolClassMapper", mock(SchoolClassMapper.class)); ReflectionTestUtils.setField(controller, "schoolClassMapper", mock(SchoolClassMapper.class));
ScoreRankService scoreRankService = mock(ScoreRankService.class); ScoreRankService scoreRankService = mock(ScoreRankService.class);
when(scoreRankService.getNowDate()).thenReturn("2026-07-30"); when(scoreRankService.getNowDate()).thenReturn("2026-07-30");
@ -118,4 +147,11 @@ class TeaScoreControllerProgressTest {
grade.setModule(module); grade.setModule(module);
return grade; return grade;
} }
private StudentTrainingAnswer answer(String taskKey, String progressStatus) {
StudentTrainingAnswer answer = new StudentTrainingAnswer();
answer.setTaskKey(taskKey);
answer.setProgressStatus(progressStatus);
return answer;
}
} }

Loading…
Cancel
Save