feat: 参考答案推送与学生实验报告成绩展示

- 任务配置开放参考答案开关(setClassTaskReferenceAnswerEnabled),
  学生任务接口仅在推送后返回参考答案字段
- 学生实验报告任务明细携带AI评分/老师评分与评语
- 新增学生端参考答案预览接口(docx转PDF,推送门控)
- 同步测试
main
chenyuan 9 hours ago
parent 78dbd4d01a
commit d6ebf06afa

@ -7,12 +7,16 @@ import com.sztzjy.linkCommerce.config.security.TokenProvider;
import com.sztzjy.linkCommerce.entity.TrainingTask; import com.sztzjy.linkCommerce.entity.TrainingTask;
import com.sztzjy.linkCommerce.entity.importDto.TrainingTaskImportDTO; import com.sztzjy.linkCommerce.entity.importDto.TrainingTaskImportDTO;
import com.sztzjy.linkCommerce.service.TrainingTaskService; import com.sztzjy.linkCommerce.service.TrainingTaskService;
import com.sztzjy.linkCommerce.util.file.IFileUtil;
import com.sztzjy.linkCommerce.util.ResultEntity; import com.sztzjy.linkCommerce.util.ResultEntity;
import com.sztzjy.linkCommerce.util.pdfUtils.PDFConvertUtil;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport; import org.springframework.transaction.interceptor.TransactionAspectSupport;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -20,9 +24,12 @@ import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -33,6 +40,9 @@ public class TrainingTaskController {
@Autowired @Autowired
private TrainingTaskService trainingTaskService; private TrainingTaskService trainingTaskService;
@Autowired
private IFileUtil fileUtil;
@GetMapping @GetMapping
@ApiOperation("Training task list") @ApiOperation("Training task list")
public ResultEntity<List<TrainingTask>> list(@RequestParam(required = false) String projectName, public ResultEntity<List<TrainingTask>> list(@RequestParam(required = false) String projectName,
@ -77,6 +87,39 @@ public class TrainingTaskController {
return new ResultEntity<>(HttpStatus.OK, "Query success", task); return new ResultEntity<>(HttpStatus.OK, "Query success", task);
} }
@GetMapping("/key/{taskKey}/reference-answer/preview")
@ApiOperation("学生在线查看已推送的参考答案(docx 转 PDF)")
public ResponseEntity<byte[]> previewReferenceAnswerForStudent(@PathVariable String taskKey,
HttpServletRequest request) {
try {
JwtUser user = TokenProvider.getJWTUser(request);
if (!isStudent(user)) return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
// getStudentTaskByTaskKey 走班级配置解析;未推送(未开放)时参考答案字段对学生为空
TrainingTask task = trainingTaskService.getStudentTaskByTaskKey(taskKey, user);
if (task == null || StringUtils.isBlank(task.getReferenceAnswerUrl())) {
return ResponseEntity.notFound().build();
}
String name = StringUtils.defaultIfBlank(task.getReferenceAnswerName(), task.getReferenceAnswerUrl());
String path = task.getReferenceAnswerUrl();
int slash = path.indexOf("/file/");
if (slash >= 0) path = path.substring(slash + 1);
File source = new File(fileUtil.getFullPath(path));
if (!source.exists() || !PDFConvertUtil.isCreatePdf(name)) return ResponseEntity.notFound().build();
ByteArrayOutputStream output = new ByteArrayOutputStream();
try (InputStream input = Files.newInputStream(source.toPath())) {
if (!PDFConvertUtil.convertToPDFByFileNameSuffix(name, input, output)) {
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build();
}
}
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_PDF)
.header("Content-Disposition", "inline; filename=reference-answer.pdf")
.body(output.toByteArray());
} catch (RuntimeException | java.io.IOException e) {
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).build();
}
}
@GetMapping("/classes/{teachingClassId}") @GetMapping("/classes/{teachingClassId}")
@ApiOperation("Training task list for teaching class") @ApiOperation("Training task list for teaching class")
public ResultEntity<List<TrainingTask>> listForTeachingClass(@PathVariable String teachingClassId, public ResultEntity<List<TrainingTask>> listForTeachingClass(@PathVariable String teachingClassId,
@ -123,6 +166,22 @@ public class TrainingTaskController {
} }
} }
@PutMapping("/classes/{teachingClassId}/{taskKey}/reference-answer")
@ApiOperation("Toggle reference answer visibility for a teaching-class task")
public ResultEntity<TrainingTask> setReferenceAnswerVisibility(@PathVariable String teachingClassId,
@PathVariable String taskKey,
@RequestParam Boolean enabled,
HttpServletRequest request) {
JwtUser user = requireTeacher(request);
try {
return new ResultEntity<>(HttpStatus.OK, "Reference answer visibility updated",
trainingTaskService.setClassTaskReferenceAnswerEnabled(
teachingClassId, taskKey, enabled, user.getUserId()));
} catch (IllegalArgumentException e) {
return new ResultEntity<>(HttpStatus.BAD_REQUEST, e.getMessage());
}
}
@DeleteMapping("/classes/{teachingClassId}/{taskKey}/override") @DeleteMapping("/classes/{teachingClassId}/{taskKey}/override")
@ApiOperation("Restore training task default for teaching class") @ApiOperation("Restore training task default for teaching class")
public ResultEntity restoreClassTaskDefault(@PathVariable String teachingClassId, public ResultEntity restoreClassTaskDefault(@PathVariable String teachingClassId,
@ -245,9 +304,10 @@ public class TrainingTaskController {
return user != null && user.getRoleId() == 4; return user != null && user.getRoleId() == 4;
} }
/** 参考答案属于教师评阅资料,学生任务接口不得返回文件名或地址。 */ /** 参考答案默认对学生隐藏,只有教学班任务配置明确开放时才返回。 */
private void removeReferenceAnswerForStudent(TrainingTask task) { private void removeReferenceAnswerForStudent(TrainingTask task) {
if (task == null) return; if (task == null) return;
if (Boolean.TRUE.equals(task.getReferenceAnswerEnabled())) return;
task.setReferenceAnswerName(null); task.setReferenceAnswerName(null);
task.setReferenceAnswerUrl(null); task.setReferenceAnswerUrl(null);
} }

@ -65,6 +65,9 @@ public class TrainingTask {
@ApiModelProperty("参考答案地址") @ApiModelProperty("参考答案地址")
private String referenceAnswerUrl; private String referenceAnswerUrl;
@ApiModelProperty("是否向学生开放参考答案")
private Boolean referenceAnswerEnabled;
@ApiModelProperty("排序") @ApiModelProperty("排序")
private Integer sort; private Integer sort;
@ -240,6 +243,14 @@ public class TrainingTask {
this.referenceAnswerUrl = referenceAnswerUrl == null ? null : referenceAnswerUrl.trim(); this.referenceAnswerUrl = referenceAnswerUrl == null ? null : referenceAnswerUrl.trim();
} }
public Boolean getReferenceAnswerEnabled() {
return referenceAnswerEnabled;
}
public void setReferenceAnswerEnabled(Boolean referenceAnswerEnabled) {
this.referenceAnswerEnabled = referenceAnswerEnabled;
}
public Integer getSort() { public Integer getSort() {
return sort; return sort;
} }

@ -68,6 +68,9 @@ public class TrainingTaskClassConfig {
@ApiModelProperty("Reference answer URL") @ApiModelProperty("Reference answer URL")
private String referenceAnswerUrl; private String referenceAnswerUrl;
@ApiModelProperty("Whether the reference answer is visible to students")
private Boolean referenceAnswerEnabled;
@ApiModelProperty("Sort order") @ApiModelProperty("Sort order")
private Integer sort; private Integer sort;
@ -251,6 +254,14 @@ public class TrainingTaskClassConfig {
this.referenceAnswerUrl = referenceAnswerUrl == null ? null : referenceAnswerUrl.trim(); this.referenceAnswerUrl = referenceAnswerUrl == null ? null : referenceAnswerUrl.trim();
} }
public Boolean getReferenceAnswerEnabled() {
return referenceAnswerEnabled;
}
public void setReferenceAnswerEnabled(Boolean referenceAnswerEnabled) {
this.referenceAnswerEnabled = referenceAnswerEnabled;
}
public Integer getSort() { public Integer getSort() {
return sort; return sort;
} }

@ -1,4 +1,26 @@
package com.sztzjy.linkCommerce.entity.dto; package com.sztzjy.linkCommerce.entity.dto;
import java.math.BigDecimal; import java.util.List; import java.math.BigDecimal; import java.util.List;
public class StudentExperimentReportDTO { private String teachingClassId; private Integer rank; private BigDecimal totalScore; private List<StudentExperimentReportModuleDTO> modules; private List<StudentExperimentReportTaskDTO> tasks; public class StudentExperimentReportDTO {
public String getTeachingClassId(){return teachingClassId;} public void setTeachingClassId(String v){teachingClassId=v;} public Integer getRank(){return rank;} public void setRank(Integer v){rank=v;} public BigDecimal getTotalScore(){return totalScore;} public void setTotalScore(BigDecimal v){totalScore=v;} public List<StudentExperimentReportModuleDTO> getModules(){return modules;} public void setModules(List<StudentExperimentReportModuleDTO> v){modules=v;} public List<StudentExperimentReportTaskDTO> getTasks(){return tasks;} public void setTasks(List<StudentExperimentReportTaskDTO> v){tasks=v;} } private String teachingClassId;
private Integer rank;
private BigDecimal totalScore;
private BigDecimal aiAssessmentScore;
private BigDecimal teacherScore;
private List<StudentExperimentReportModuleDTO> modules;
private List<StudentExperimentReportTaskDTO> tasks;
public String getTeachingClassId() { return teachingClassId; }
public void setTeachingClassId(String v) { teachingClassId = v; }
public Integer getRank() { return rank; }
public void setRank(Integer v) { rank = v; }
public BigDecimal getTotalScore() { return totalScore; }
public void setTotalScore(BigDecimal v) { totalScore = v; }
public BigDecimal getAiAssessmentScore() { return aiAssessmentScore; }
public void setAiAssessmentScore(BigDecimal v) { aiAssessmentScore = v; }
public BigDecimal getTeacherScore() { return teacherScore; }
public void setTeacherScore(BigDecimal v) { teacherScore = v; }
public List<StudentExperimentReportModuleDTO> getModules() { return modules; }
public void setModules(List<StudentExperimentReportModuleDTO> v) { modules = v; }
public List<StudentExperimentReportTaskDTO> getTasks() { return tasks; }
public void setTasks(List<StudentExperimentReportTaskDTO> v) { tasks = v; }
}

@ -1,3 +1,22 @@
package com.sztzjy.linkCommerce.entity.dto; package com.sztzjy.linkCommerce.entity.dto;
public class StudentExperimentReportTaskDTO { private String taskName; private String moduleName; private String progressStatus; private Boolean submitted; private Integer aiAssessmentScore; public class StudentExperimentReportTaskDTO {
public String getTaskName(){return taskName;} public void setTaskName(String v){taskName=v;} public String getModuleName(){return moduleName;} public void setModuleName(String v){moduleName=v;} public String getProgressStatus(){return progressStatus;} public void setProgressStatus(String v){progressStatus=v;} public Boolean getSubmitted(){return submitted;} public void setSubmitted(Boolean v){submitted=v;} public Integer getAiAssessmentScore(){return aiAssessmentScore;} public void setAiAssessmentScore(Integer v){aiAssessmentScore=v;} } private String taskName;
private String moduleName;
private String progressStatus;
private Boolean submitted;
private Integer aiAssessmentScore;
private java.math.BigDecimal teacherScore;
public String getTaskName() { return taskName; }
public void setTaskName(String v) { taskName = v; }
public String getModuleName() { return moduleName; }
public void setModuleName(String v) { moduleName = v; }
public String getProgressStatus() { return progressStatus; }
public void setProgressStatus(String v) { progressStatus = v; }
public Boolean getSubmitted() { return submitted; }
public void setSubmitted(Boolean v) { submitted = v; }
public Integer getAiAssessmentScore() { return aiAssessmentScore; }
public void setAiAssessmentScore(Integer v) { aiAssessmentScore = v; }
public java.math.BigDecimal getTeacherScore() { return teacherScore; }
public void setTeacherScore(java.math.BigDecimal v) { teacherScore = v; }
}

@ -33,6 +33,9 @@ public interface TrainingTaskService {
TrainingTask saveClassTask(String teachingClassId, String taskKey, TrainingTask task, String operatorId); TrainingTask saveClassTask(String teachingClassId, String taskKey, TrainingTask task, String operatorId);
TrainingTask setClassTaskReferenceAnswerEnabled(String teachingClassId, String taskKey,
Boolean enabled, String operatorId);
void restoreClassTaskDefault(String teachingClassId, String taskKey, String operatorId); void restoreClassTaskDefault(String teachingClassId, String taskKey, String operatorId);
void restoreClassTasksDefaults(String teachingClassId, String operatorId); void restoreClassTasksDefaults(String teachingClassId, String operatorId);

@ -1,11 +1,217 @@
package com.sztzjy.linkCommerce.service.impl; package com.sztzjy.linkCommerce.service.impl;
import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.entity.*; import com.sztzjy.linkCommerce.entity.dto.*; import com.sztzjy.linkCommerce.mapper.*; import com.sztzjy.linkCommerce.service.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;
import java.math.*; import java.util.*; import com.sztzjy.linkCommerce.config.security.JwtUser;
@Service public class StudentExperimentReportServiceImpl implements StudentExperimentReportService { import com.sztzjy.linkCommerce.entity.StudentTrainingAnswer;
@Autowired public StudentTeachingClassResolver studentTeachingClassResolver; @Autowired public SchoolDefaultTaskService schoolDefaultTaskService; @Autowired public TrainingTaskMapper trainingTaskMapper; @Autowired public StudentTrainingAnswerMapper studentTrainingAnswerMapper; @Autowired(required=false) public TeachingClassScoreWeightMapper teachingClassScoreWeightMapper; @Autowired(required=false) public TaskAllocationMapper taskAllocationMapper; import com.sztzjy.linkCommerce.entity.TaskAllocation;
private static final String[] NAMES={"互联网产品开发基础认知","市场洞察与需求分析","产品规划与设计","产品开发与测试验证","产品上线与运营推广","综合实训"}; import com.sztzjy.linkCommerce.entity.TaskAllocationExample;
private static final BigDecimal[] DEFAULTS={new BigDecimal("0.10"),new BigDecimal("0.30"),new BigDecimal("0.10"),new BigDecimal("0.10"),new BigDecimal("0.20"),new BigDecimal("0.20")}; import com.sztzjy.linkCommerce.entity.TeachingClassScoreWeight;
public StudentExperimentReportDTO getCurrentReport(JwtUser user){ String classId=studentTeachingClassResolver.resolveRequired(user); StudentExperimentReportDTO report=new StudentExperimentReportDTO(); report.setTeachingClassId(classId); List<StudentExperimentReportModuleDTO> modules=new ArrayList<>(); Map<String,StudentExperimentReportModuleDTO> index=new HashMap<>(); BigDecimal[] weights=weights(classId); boolean comprehensiveParticipating=hasPublishedComprehensiveTask(classId); for(int i=0;i<NAMES.length;i++){StudentExperimentReportModuleDTO m=new StudentExperimentReportModuleDTO();m.setModuleName(NAMES[i]);m.setWeight(weights[i]);m.setParticipating(i<NAMES.length-1||comprehensiveParticipating);modules.add(m);index.put(NAMES[i],m);} List<StudentExperimentReportTaskDTO> tasks=new ArrayList<>(); for(TaskAllocation a: schoolDefaultTaskService.resolveForTeachingClass(classId)){if(a==null||a.getDisabledStatus()!=null&&a.getDisabledStatus()==1||TaskAllocation.PUBLICATION_MARKER.equals(a.getModule())||("comprehensive-case-training".equals(a.getModule())&&!comprehensiveParticipating))continue; TrainingTask t=trainingTaskMapper.selectByTaskKey(a.getModule()); if(t==null||!index.containsKey(t.getProjectName()))continue; StudentTrainingAnswer ans=studentTrainingAnswerMapper.selectByStudentClassAndTask(user.getUserId(),classId,t.getId()); StudentExperimentReportModuleDTO m=index.get(t.getProjectName());m.setTotalTaskCount(m.getTotalTaskCount()+1);int score=ans==null||ans.getAiAssessmentScore()==null?0:ans.getAiAssessmentScore();m.setScore(m.getScore().add(BigDecimal.valueOf(score)));if(ans!=null&&(Boolean.TRUE.equals(ans.getSubmitted())||"COMPLETED".equals(ans.getProgressStatus())))m.setCompletedTaskCount(m.getCompletedTaskCount()+1);StudentExperimentReportTaskDTO row=new StudentExperimentReportTaskDTO();row.setTaskName(t.getTaskName());row.setModuleName(t.getProjectName());if(ans!=null){row.setProgressStatus(ans.getProgressStatus());row.setSubmitted(ans.getSubmitted());row.setAiAssessmentScore(ans.getAiAssessmentScore());}tasks.add(row);} BigDecimal total=BigDecimal.ZERO;BigDecimal participatingWeight=BigDecimal.ZERO;for(StudentExperimentReportModuleDTO m:modules){BigDecimal score=m.getTotalTaskCount()==0?BigDecimal.ZERO:m.getScore().divide(BigDecimal.valueOf(m.getTotalTaskCount()),2,BigDecimal.ROUND_HALF_UP);m.setScore(score);if(Boolean.TRUE.equals(m.getParticipating())){BigDecimal weight=m.getWeight()==null?BigDecimal.ZERO:m.getWeight();total=total.add(score.multiply(weight));participatingWeight=participatingWeight.add(weight);}}report.setTotalScore(participatingWeight.signum()==0?BigDecimal.ZERO:total.divide(participatingWeight,2,BigDecimal.ROUND_HALF_UP));report.setModules(modules);report.setTasks(tasks);return report; } import com.sztzjy.linkCommerce.entity.TrainingTask;
private boolean hasPublishedComprehensiveTask(String classId){if(taskAllocationMapper==null)return false;TaskAllocationExample example=new TaskAllocationExample();example.createCriteria().andClassIdEqualTo(classId);List<TaskAllocation> allocations=taskAllocationMapper.selectByExample(example);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;} import com.sztzjy.linkCommerce.entity.dto.StudentExperimentReportDTO;
private BigDecimal[] weights(String id){TeachingClassScoreWeight w=teachingClassScoreWeightMapper==null?null:teachingClassScoreWeightMapper.selectByTeachingClassId(id);if(w==null)return DEFAULTS;return new BigDecimal[]{w.getFoundationWeight(),w.getMarketInsightWeight(),w.getPlanningDesignWeight(),w.getDevelopmentValidationWeight(),w.getLaunchOperationWeight(),w.getComprehensiveTrainingWeight()};} import com.sztzjy.linkCommerce.entity.dto.StudentExperimentReportModuleDTO;
import com.sztzjy.linkCommerce.entity.dto.StudentExperimentReportTaskDTO;
import com.sztzjy.linkCommerce.mapper.StudentTrainingAnswerMapper;
import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper;
import com.sztzjy.linkCommerce.mapper.TeachingClassScoreWeightMapper;
import com.sztzjy.linkCommerce.mapper.TrainingTaskMapper;
import com.sztzjy.linkCommerce.service.SchoolDefaultTaskService;
import com.sztzjy.linkCommerce.service.StudentExperimentReportService;
import com.sztzjy.linkCommerce.service.StudentTeachingClassResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class StudentExperimentReportServiceImpl implements StudentExperimentReportService {
@Autowired
public StudentTeachingClassResolver studentTeachingClassResolver;
@Autowired
public SchoolDefaultTaskService schoolDefaultTaskService;
@Autowired
public TrainingTaskMapper trainingTaskMapper;
@Autowired
public StudentTrainingAnswerMapper studentTrainingAnswerMapper;
@Autowired(required = false)
public TeachingClassScoreWeightMapper teachingClassScoreWeightMapper;
@Autowired(required = false)
public TaskAllocationMapper taskAllocationMapper;
private static final String[] NAMES = {
"互联网产品开发基础认知", "市场洞察与需求分析", "产品规划与设计",
"产品开发与测试验证", "产品上线与运营推广", "综合实训"
};
private static final BigDecimal[] DEFAULTS = {
new BigDecimal("0.10"), new BigDecimal("0.30"), new BigDecimal("0.10"),
new BigDecimal("0.10"), new BigDecimal("0.20"), new BigDecimal("0.20")
};
@Override
public StudentExperimentReportDTO getCurrentReport(JwtUser user) {
String classId = studentTeachingClassResolver.resolveRequired(user);
StudentExperimentReportDTO report = new StudentExperimentReportDTO();
report.setTeachingClassId(classId);
List<StudentExperimentReportModuleDTO> modules = new ArrayList<>();
Map<String, StudentExperimentReportModuleDTO> moduleByName = new HashMap<>();
Map<String, Integer> moduleIndex = new HashMap<>();
BigDecimal[] weights = weights(classId);
boolean comprehensiveParticipating = hasPublishedComprehensiveTask(classId);
for (int i = 0; i < NAMES.length; i++) {
StudentExperimentReportModuleDTO module = new StudentExperimentReportModuleDTO();
module.setModuleName(NAMES[i]);
module.setWeight(weights[i]);
module.setParticipating(i < NAMES.length - 1 || comprehensiveParticipating);
modules.add(module);
moduleByName.put(NAMES[i], module);
moduleIndex.put(NAMES[i], i);
}
BigDecimal[] aiSums = zeroScores();
BigDecimal[] teacherSums = zeroScores();
int[] taskCounts = new int[NAMES.length];
boolean[] hasAiScore = new boolean[NAMES.length];
boolean[] hasTeacherScore = new boolean[NAMES.length];
List<StudentExperimentReportTaskDTO> tasks = new ArrayList<>();
List<TaskAllocation> allocations = schoolDefaultTaskService.resolveForTeachingClass(classId);
for (TaskAllocation allocation : allocations == null
? Collections.<TaskAllocation>emptyList() : allocations) {
if (allocation == null || (allocation.getDisabledStatus() != null && allocation.getDisabledStatus() == 1)
|| TaskAllocation.PUBLICATION_MARKER.equals(allocation.getModule())
|| ("comprehensive-case-training".equals(allocation.getModule()) && !comprehensiveParticipating)) {
continue;
}
TrainingTask task = trainingTaskMapper.selectByTaskKey(allocation.getModule());
Integer index = task == null ? null : moduleIndex.get(task.getProjectName());
if (index == null) {
continue;
}
StudentTrainingAnswer answer = studentTrainingAnswerMapper
.selectByStudentClassAndTask(user.getUserId(), classId, task.getId());
StudentExperimentReportModuleDTO module = moduleByName.get(task.getProjectName());
module.setTotalTaskCount(module.getTotalTaskCount() + 1);
taskCounts[index]++;
StudentExperimentReportTaskDTO row = new StudentExperimentReportTaskDTO();
row.setTaskName(task.getTaskName());
row.setModuleName(task.getProjectName());
if (answer != null) {
row.setProgressStatus(answer.getProgressStatus());
row.setSubmitted(answer.getSubmitted());
row.setAiAssessmentScore(answer.getAiAssessmentScore());
row.setTeacherScore(answer.getTeacherScore());
if (answer.getAiAssessmentScore() != null) {
aiSums[index] = aiSums[index].add(BigDecimal.valueOf(answer.getAiAssessmentScore()));
hasAiScore[index] = true;
}
if (answer.getTeacherScore() != null) {
teacherSums[index] = teacherSums[index].add(answer.getTeacherScore());
hasTeacherScore[index] = true;
}
if (Boolean.TRUE.equals(answer.getSubmitted())
|| "COMPLETED".equals(answer.getProgressStatus())) {
module.setCompletedTaskCount(module.getCompletedTaskCount() + 1);
}
}
tasks.add(row);
}
boolean anyAiScore = false;
boolean anyTeacherScore = false;
for (int i = 0; i < NAMES.length; i++) {
BigDecimal aiAverage = average(aiSums[i], taskCounts[i], hasAiScore[i]);
BigDecimal teacherAverage = average(teacherSums[i], taskCounts[i], hasTeacherScore[i]);
anyAiScore = anyAiScore || aiAverage != null;
anyTeacherScore = anyTeacherScore || teacherAverage != null;
// 教师评分优先;本模块没有教师评分时才使用 AI 评分。
modules.get(i).setScore(teacherAverage != null ? teacherAverage : aiAverage);
}
BigDecimal aiTotal = aggregate(modules, aiSums, taskCounts, hasAiScore);
BigDecimal teacherTotal = aggregate(modules, teacherSums, taskCounts, hasTeacherScore);
report.setAiAssessmentScore(anyAiScore ? aiTotal : null);
report.setTeacherScore(anyTeacherScore ? teacherTotal : null);
// 总成绩同样遵循教师评分优先,且没有任何成绩时返回 null。
report.setTotalScore(anyTeacherScore ? teacherTotal : (anyAiScore ? aiTotal : null));
report.setModules(modules);
report.setTasks(tasks);
return report;
}
private BigDecimal[] zeroScores() {
BigDecimal[] scores = new BigDecimal[NAMES.length];
for (int i = 0; i < scores.length; i++) {
scores[i] = BigDecimal.ZERO;
}
return scores;
}
private BigDecimal average(BigDecimal sum, int count, boolean present) {
if (!present || count <= 0) {
return null;
}
return sum.divide(BigDecimal.valueOf(count), 2, BigDecimal.ROUND_HALF_UP);
}
private BigDecimal aggregate(List<StudentExperimentReportModuleDTO> modules,
BigDecimal[] sums, int[] taskCounts, boolean[] present) {
BigDecimal total = BigDecimal.ZERO;
BigDecimal participatingWeight = BigDecimal.ZERO;
for (int i = 0; i < modules.size(); i++) {
BigDecimal average = average(sums[i], taskCounts[i], present[i]);
if (average == null || !Boolean.TRUE.equals(modules.get(i).getParticipating())) {
continue;
}
BigDecimal weight = modules.get(i).getWeight() == null
? BigDecimal.ZERO : modules.get(i).getWeight();
total = total.add(average.multiply(weight));
participatingWeight = participatingWeight.add(weight);
}
return participatingWeight.signum() == 0
? BigDecimal.ZERO
: total.divide(participatingWeight, 2, BigDecimal.ROUND_HALF_UP);
}
private boolean hasPublishedComprehensiveTask(String classId) {
if (taskAllocationMapper == null) {
return false;
}
TaskAllocationExample example = new TaskAllocationExample();
example.createCriteria().andClassIdEqualTo(classId);
List<TaskAllocation> allocations = taskAllocationMapper.selectByExample(example);
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 BigDecimal[] weights(String id) {
TeachingClassScoreWeight weight = teachingClassScoreWeightMapper == null
? null : teachingClassScoreWeightMapper.selectByTeachingClassId(id);
if (weight == null) {
return DEFAULTS;
}
return new BigDecimal[]{
weight.getFoundationWeight(), weight.getMarketInsightWeight(),
weight.getPlanningDesignWeight(), weight.getDevelopmentValidationWeight(),
weight.getLaunchOperationWeight(), weight.getComprehensiveTrainingWeight()
};
}
} }

@ -252,6 +252,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
@Override @Override
public List<TrainingTask> listForTeachingClass(String teachingClassId, Boolean enabledOnly, JwtUser operator) { public List<TrainingTask> listForTeachingClass(String teachingClassId, Boolean enabledOnly, JwtUser operator) {
requireReadableTeachingClass(teachingClassId, operator); requireReadableTeachingClass(teachingClassId, operator);
ensureDefaultTasks(null);
List<TrainingTask> defaults = trainingTaskMapper.selectList(null, null, null); List<TrainingTask> defaults = trainingTaskMapper.selectList(null, null, null);
return filterEnabled(readClassTasks(teachingClassId, defaults), enabledOnly); return filterEnabled(readClassTasks(teachingClassId, defaults), enabledOnly);
} }
@ -318,6 +319,26 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
return toTask(effective); return toTask(effective);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public TrainingTask setClassTaskReferenceAnswerEnabled(String teachingClassId, String taskKey,
Boolean enabled, String operatorId) {
requireOwnedTeachingClass(teachingClassId, operatorId);
ensureDefaultTasks(null);
String normalizedTaskKey = StringUtils.trimToEmpty(taskKey);
ensureClassTasksInitialized(teachingClassId, trainingTaskMapper.selectList(null, null, null), operatorId);
TrainingTaskClassConfig config = trainingTaskClassConfigMapper
.selectByTeachingClassAndTaskKey(teachingClassId, normalizedTaskKey);
if (config == null) {
throw new IllegalArgumentException("Training task not found");
}
config.setReferenceAnswerEnabled(Boolean.TRUE.equals(enabled));
config.setUpdateTime(new Date());
trainingTaskClassConfigMapper.updateByPrimaryKeySelective(config);
TrainingTaskClassConfig saved = trainingTaskClassConfigMapper.selectByPrimaryKey(config.getId());
return toTask(saved == null ? config : saved);
}
private void indexCaseMaterial(TrainingTask task, EffectiveTrainingTaskContent content, String teachingClassId) { private void indexCaseMaterial(TrainingTask task, EffectiveTrainingTaskContent content, String teachingClassId) {
if (trainingKnowledgeService == null || task == null) return; if (trainingKnowledgeService == null || task == null) return;
try { try {
@ -446,12 +467,12 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
} }
/** /**
* @return null when this teaching class has never been configured (default: all tasks), * @return the explicitly published task keys, or the first three tasks in the first
* otherwise the explicitly published task keys, which may be empty. * project when this teaching class has never been configured.
*/ */
private Set<String> getPublishedTaskKeys(String teachingClassId) { private Set<String> getPublishedTaskKeys(String teachingClassId) {
if (taskAllocationMapper == null || StringUtils.isBlank(teachingClassId)) { if (taskAllocationMapper == null || StringUtils.isBlank(teachingClassId)) {
return null; return getDefaultPublishedTaskKeys();
} }
TaskAllocationExample example = new TaskAllocationExample(); TaskAllocationExample example = new TaskAllocationExample();
example.createCriteria().andClassIdEqualTo(teachingClassId); example.createCriteria().andClassIdEqualTo(teachingClassId);
@ -467,7 +488,34 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
} }
} }
} }
return configured ? taskKeys : null; return configured ? taskKeys : getDefaultPublishedTaskKeys();
}
private Set<String> getDefaultPublishedTaskKeys() {
List<TrainingTask> defaults = sortTasks(trainingTaskMapper.selectList(null, null, null));
Set<String> taskKeys = new LinkedHashSet<>();
if (defaults == null) {
return taskKeys;
}
String firstProjectName = null;
for (TrainingTask task : defaults) {
if (task == null || StringUtils.isBlank(task.getTaskKey())
|| requiresExplicitPublication(task.getTaskKey())) {
continue;
}
String projectName = StringUtils.defaultString(task.getProjectName(), "其他");
if (firstProjectName == null) {
firstProjectName = projectName;
}
if (!StringUtils.equals(firstProjectName, projectName)) {
continue;
}
taskKeys.add(task.getTaskKey());
if (taskKeys.size() >= 3) {
break;
}
}
return taskKeys;
} }
private String requireStudentTeachingClass(JwtUser student) { private String requireStudentTeachingClass(JwtUser student) {
@ -738,6 +786,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
task.setAiManualUrl(config.getAiManualUrl()); task.setAiManualUrl(config.getAiManualUrl());
task.setReferenceAnswerName(config.getReferenceAnswerName()); task.setReferenceAnswerName(config.getReferenceAnswerName());
task.setReferenceAnswerUrl(config.getReferenceAnswerUrl()); task.setReferenceAnswerUrl(config.getReferenceAnswerUrl());
task.setReferenceAnswerEnabled(config.getReferenceAnswerEnabled());
task.setSort(config.getSort()); task.setSort(config.getSort());
task.setEnabled(config.getEnabled()); task.setEnabled(config.getEnabled());
task.setCreatedBy(config.getCreatedBy()); task.setCreatedBy(config.getCreatedBy());
@ -842,6 +891,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
"ai_manual_url varchar(500) NULL," + "ai_manual_url varchar(500) NULL," +
"reference_answer_name varchar(255) NULL," + "reference_answer_name varchar(255) NULL," +
"reference_answer_url varchar(500) NULL," + "reference_answer_url varchar(500) NULL," +
"reference_answer_enabled bit(1) NOT NULL DEFAULT b'0' COMMENT '是否向学生开放参考答案'," +
"sort int DEFAULT 0," + "sort int DEFAULT 0," +
"enabled bit(1) DEFAULT b'1'," + "enabled bit(1) DEFAULT b'1'," +
"created_by varchar(64) NULL," + "created_by varchar(64) NULL," +
@ -855,9 +905,26 @@ public class TrainingTaskServiceImpl implements TrainingTaskService {
ensureTextColumn("training_task_class_config", "step_one_questions", "第一步选择题JSON数组"); ensureTextColumn("training_task_class_config", "step_one_questions", "第一步选择题JSON数组");
ensureMaterialColumns("training_task_class_config"); ensureMaterialColumns("training_task_class_config");
ensureReferenceAnswerColumns("training_task_class_config"); ensureReferenceAnswerColumns("training_task_class_config");
ensureReferenceAnswerVisibilityColumn("training_task_class_config");
trainingTaskClassConfigTableChecked = true; trainingTaskClassConfigTableChecked = true;
} }
private void ensureReferenceAnswerVisibilityColumn(String tableName) {
if (jdbcTemplate == null) {
return;
}
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(1) FROM information_schema.COLUMNS " +
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = 'reference_answer_enabled'",
Integer.class, tableName);
if (count != null && count > 0) {
return;
}
jdbcTemplate.execute("ALTER TABLE " + tableName
+ " ADD COLUMN reference_answer_enabled bit(1) NOT NULL DEFAULT b'0' "
+ "COMMENT '是否向学生开放参考答案' AFTER reference_answer_url");
}
/** Add the field for deployments that already created the task tables before this feature existed. */ /** Add the field for deployments that already created the task tables before this feature existed. */
private void ensureTextColumn(String tableName, String columnName, String comment) { private void ensureTextColumn(String tableName, String columnName, String comment) {
if (jdbcTemplate == null) { if (jdbcTemplate == null) {

@ -23,6 +23,7 @@
<result column="ai_manual_url" jdbcType="VARCHAR" property="aiManualUrl" /> <result column="ai_manual_url" jdbcType="VARCHAR" property="aiManualUrl" />
<result column="reference_answer_name" jdbcType="VARCHAR" property="referenceAnswerName" /> <result column="reference_answer_name" jdbcType="VARCHAR" property="referenceAnswerName" />
<result column="reference_answer_url" jdbcType="VARCHAR" property="referenceAnswerUrl" /> <result column="reference_answer_url" jdbcType="VARCHAR" property="referenceAnswerUrl" />
<result column="reference_answer_enabled" jdbcType="BIT" property="referenceAnswerEnabled" />
<result column="sort" jdbcType="INTEGER" property="sort" /> <result column="sort" jdbcType="INTEGER" property="sort" />
<result column="enabled" jdbcType="BIT" property="enabled" /> <result column="enabled" jdbcType="BIT" property="enabled" />
<result column="created_by" jdbcType="VARCHAR" property="createdBy" /> <result column="created_by" jdbcType="VARCHAR" property="createdBy" />
@ -34,7 +35,7 @@
id, teaching_class_id, task_key, project_name, task_name, knowledge, background, objectives, requirements, id, teaching_class_id, task_key, project_name, task_name, knowledge, background, objectives, requirements,
steps, step_one_questions, material_name, material_url, steps, step_one_questions, material_name, material_url,
training_data_name, training_data_url, guide_manual_name, guide_manual_url, ai_manual_name, ai_manual_url, training_data_name, training_data_url, guide_manual_name, guide_manual_url, ai_manual_name, ai_manual_url,
reference_answer_name, reference_answer_url, reference_answer_name, reference_answer_url, reference_answer_enabled,
sort, enabled, created_by, create_time, update_time sort, enabled, created_by, create_time, update_time
</sql> </sql>
@ -103,6 +104,7 @@
<if test="aiManualUrl != null">ai_manual_url,</if> <if test="aiManualUrl != null">ai_manual_url,</if>
<if test="referenceAnswerName != null">reference_answer_name,</if> <if test="referenceAnswerName != null">reference_answer_name,</if>
<if test="referenceAnswerUrl != null">reference_answer_url,</if> <if test="referenceAnswerUrl != null">reference_answer_url,</if>
<if test="referenceAnswerEnabled != null">reference_answer_enabled,</if>
<if test="sort != null">sort,</if> <if test="sort != null">sort,</if>
<if test="enabled != null">enabled,</if> <if test="enabled != null">enabled,</if>
<if test="createdBy != null">created_by,</if> <if test="createdBy != null">created_by,</if>
@ -131,6 +133,7 @@
<if test="aiManualUrl != null">#{aiManualUrl,jdbcType=VARCHAR},</if> <if test="aiManualUrl != null">#{aiManualUrl,jdbcType=VARCHAR},</if>
<if test="referenceAnswerName != null">#{referenceAnswerName,jdbcType=VARCHAR},</if> <if test="referenceAnswerName != null">#{referenceAnswerName,jdbcType=VARCHAR},</if>
<if test="referenceAnswerUrl != null">#{referenceAnswerUrl,jdbcType=VARCHAR},</if> <if test="referenceAnswerUrl != null">#{referenceAnswerUrl,jdbcType=VARCHAR},</if>
<if test="referenceAnswerEnabled != null">#{referenceAnswerEnabled,jdbcType=BIT},</if>
<if test="sort != null">#{sort,jdbcType=INTEGER},</if> <if test="sort != null">#{sort,jdbcType=INTEGER},</if>
<if test="enabled != null">#{enabled,jdbcType=BIT},</if> <if test="enabled != null">#{enabled,jdbcType=BIT},</if>
<if test="createdBy != null">#{createdBy,jdbcType=VARCHAR},</if> <if test="createdBy != null">#{createdBy,jdbcType=VARCHAR},</if>
@ -160,6 +163,7 @@
<if test="aiManualUrl != null">ai_manual_url = #{aiManualUrl,jdbcType=VARCHAR},</if> <if test="aiManualUrl != null">ai_manual_url = #{aiManualUrl,jdbcType=VARCHAR},</if>
<if test="referenceAnswerName != null">reference_answer_name = #{referenceAnswerName,jdbcType=VARCHAR},</if> <if test="referenceAnswerName != null">reference_answer_name = #{referenceAnswerName,jdbcType=VARCHAR},</if>
<if test="referenceAnswerUrl != null">reference_answer_url = #{referenceAnswerUrl,jdbcType=VARCHAR},</if> <if test="referenceAnswerUrl != null">reference_answer_url = #{referenceAnswerUrl,jdbcType=VARCHAR},</if>
<if test="referenceAnswerEnabled != null">reference_answer_enabled = #{referenceAnswerEnabled,jdbcType=BIT},</if>
<if test="sort != null">sort = #{sort,jdbcType=INTEGER},</if> <if test="sort != null">sort = #{sort,jdbcType=INTEGER},</if>
<if test="enabled != null">enabled = #{enabled,jdbcType=BIT},</if> <if test="enabled != null">enabled = #{enabled,jdbcType=BIT},</if>
<if test="updateTime != null">update_time = #{updateTime,jdbcType=TIMESTAMP},</if> <if test="updateTime != null">update_time = #{updateTime,jdbcType=TIMESTAMP},</if>

@ -46,6 +46,17 @@ class TeaScoreControllerTaskReviewTest {
"taskReview", String.class, String.class, String.class, HttpServletRequest.class)); "taskReview", String.class, String.class, String.class, HttpServletRequest.class));
} }
@Test
void taskScoreRejectsMissingTeacherScoreWithBusinessMessage() {
TeaScoreController controller = controllerWithCompletedAnswer();
ResultEntity<?> result = controller.saveTaskTeacherScore(
"class-1", "student-1", "task-1", null, "评语", new MockHttpServletRequest());
assertEquals(HttpStatus.BAD_REQUEST, result.getStatusCode());
assertEquals("教师评分必须为0至100之间的数值", result.getBody().getMsg());
}
@Test @Test
void batchScoreRejectsTaskThatIsNotAllocatedToTheClassBeforeWritingAnyScore() { void batchScoreRejectsTaskThatIsNotAllocatedToTheClassBeforeWritingAnyScore() {
TeaScoreController controller = controllerWithCompletedAnswer(); TeaScoreController controller = controllerWithCompletedAnswer();

@ -128,6 +128,20 @@ class TrainingTaskServiceImplTest {
assertFalse(task.getSteps().contains("Step 3")); assertFalse(task.getSteps().contains("Step 3"));
} }
@Test
void referenceAnswerRejectsNonWordFiles() {
TrainingTaskServiceImpl service = new TrainingTaskServiceImpl();
service.trainingTaskMapper = mock(TrainingTaskMapper.class);
TrainingTask task = storedTask(null, "new-product-survey");
task.setReferenceAnswerName("reference-answer.pdf");
IllegalArgumentException error = assertThrows(IllegalArgumentException.class,
() -> service.create(task, "teacher-1"));
assertEquals("参考答案仅支持Word文件(.doc或.docx)", error.getMessage());
verify(service.trainingTaskMapper, never()).insertSelective(any(TrainingTask.class));
}
@Test @Test
void importTaskRejectsUnknownTaskKeyBecauseTaskCountIsFixed() { void importTaskRejectsUnknownTaskKeyBecauseTaskCountIsFixed() {
TrainingTaskServiceImpl service = new TrainingTaskServiceImpl(); TrainingTaskServiceImpl service = new TrainingTaskServiceImpl();
@ -649,6 +663,8 @@ class TrainingTaskServiceImplTest {
payload.setTaskName("Class Name"); payload.setTaskName("Class Name");
payload.setKnowledge("Class Knowledge"); payload.setKnowledge("Class Knowledge");
payload.setMaterialName("Class Material"); payload.setMaterialName("Class Material");
payload.setReferenceAnswerName("参考答案.docx");
payload.setReferenceAnswerUrl("/file/reference-answer.docx");
payload.setEnabled(Boolean.TRUE); payload.setEnabled(Boolean.TRUE);
service.saveClassTask("class-1", "new-product-survey", payload, "teacher-1"); service.saveClassTask("class-1", "new-product-survey", payload, "teacher-1");
@ -665,6 +681,8 @@ class TrainingTaskServiceImplTest {
assertEquals(Integer.valueOf(3), savedOverride.getSort()); assertEquals(Integer.valueOf(3), savedOverride.getSort());
assertEquals("Class Knowledge", savedOverride.getKnowledge()); assertEquals("Class Knowledge", savedOverride.getKnowledge());
assertEquals("Class Material", savedOverride.getMaterialName()); assertEquals("Class Material", savedOverride.getMaterialName());
assertEquals("参考答案.docx", savedOverride.getReferenceAnswerName());
assertEquals("/file/reference-answer.docx", savedOverride.getReferenceAnswerUrl());
} }
@Test @Test

Loading…
Cancel
Save