diff --git a/src/main/java/com/sztzjy/linkCommerce/controller/stu/TrainingTaskController.java b/src/main/java/com/sztzjy/linkCommerce/controller/stu/TrainingTaskController.java index 521b955..b794d23 100644 --- a/src/main/java/com/sztzjy/linkCommerce/controller/stu/TrainingTaskController.java +++ b/src/main/java/com/sztzjy/linkCommerce/controller/stu/TrainingTaskController.java @@ -7,12 +7,16 @@ import com.sztzjy.linkCommerce.config.security.TokenProvider; import com.sztzjy.linkCommerce.entity.TrainingTask; import com.sztzjy.linkCommerce.entity.importDto.TrainingTaskImportDTO; import com.sztzjy.linkCommerce.service.TrainingTaskService; +import com.sztzjy.linkCommerce.util.file.IFileUtil; import com.sztzjy.linkCommerce.util.ResultEntity; +import com.sztzjy.linkCommerce.util.pdfUtils.PDFConvertUtil; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; 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.interceptor.TransactionAspectSupport; 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.HttpServletResponse; +import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.InputStream; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.util.ArrayList; import java.util.List; @@ -33,6 +40,9 @@ public class TrainingTaskController { @Autowired private TrainingTaskService trainingTaskService; + @Autowired + private IFileUtil fileUtil; + @GetMapping @ApiOperation("Training task list") public ResultEntity> list(@RequestParam(required = false) String projectName, @@ -77,6 +87,39 @@ public class TrainingTaskController { return new ResultEntity<>(HttpStatus.OK, "Query success", task); } + @GetMapping("/key/{taskKey}/reference-answer/preview") + @ApiOperation("学生在线查看已推送的参考答案(docx 转 PDF)") + public ResponseEntity 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}") @ApiOperation("Training task list for teaching class") public ResultEntity> 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 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") @ApiOperation("Restore training task default for teaching class") public ResultEntity restoreClassTaskDefault(@PathVariable String teachingClassId, @@ -245,9 +304,10 @@ public class TrainingTaskController { return user != null && user.getRoleId() == 4; } - /** 参考答案属于教师评阅资料,学生任务接口不得返回文件名或地址。 */ + /** 参考答案默认对学生隐藏,只有教学班任务配置明确开放时才返回。 */ private void removeReferenceAnswerForStudent(TrainingTask task) { if (task == null) return; + if (Boolean.TRUE.equals(task.getReferenceAnswerEnabled())) return; task.setReferenceAnswerName(null); task.setReferenceAnswerUrl(null); } diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/TrainingTask.java b/src/main/java/com/sztzjy/linkCommerce/entity/TrainingTask.java index 93701e0..85c53a7 100644 --- a/src/main/java/com/sztzjy/linkCommerce/entity/TrainingTask.java +++ b/src/main/java/com/sztzjy/linkCommerce/entity/TrainingTask.java @@ -65,6 +65,9 @@ public class TrainingTask { @ApiModelProperty("参考答案地址") private String referenceAnswerUrl; + @ApiModelProperty("是否向学生开放参考答案") + private Boolean referenceAnswerEnabled; + @ApiModelProperty("排序") private Integer sort; @@ -240,6 +243,14 @@ public class TrainingTask { this.referenceAnswerUrl = referenceAnswerUrl == null ? null : referenceAnswerUrl.trim(); } + public Boolean getReferenceAnswerEnabled() { + return referenceAnswerEnabled; + } + + public void setReferenceAnswerEnabled(Boolean referenceAnswerEnabled) { + this.referenceAnswerEnabled = referenceAnswerEnabled; + } + public Integer getSort() { return sort; } diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/TrainingTaskClassConfig.java b/src/main/java/com/sztzjy/linkCommerce/entity/TrainingTaskClassConfig.java index 34aa33b..b06e409 100644 --- a/src/main/java/com/sztzjy/linkCommerce/entity/TrainingTaskClassConfig.java +++ b/src/main/java/com/sztzjy/linkCommerce/entity/TrainingTaskClassConfig.java @@ -68,6 +68,9 @@ public class TrainingTaskClassConfig { @ApiModelProperty("Reference answer URL") private String referenceAnswerUrl; + @ApiModelProperty("Whether the reference answer is visible to students") + private Boolean referenceAnswerEnabled; + @ApiModelProperty("Sort order") private Integer sort; @@ -251,6 +254,14 @@ public class TrainingTaskClassConfig { this.referenceAnswerUrl = referenceAnswerUrl == null ? null : referenceAnswerUrl.trim(); } + public Boolean getReferenceAnswerEnabled() { + return referenceAnswerEnabled; + } + + public void setReferenceAnswerEnabled(Boolean referenceAnswerEnabled) { + this.referenceAnswerEnabled = referenceAnswerEnabled; + } + public Integer getSort() { return sort; } diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportDTO.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportDTO.java index ee5b400..9e2cfa8 100644 --- a/src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportDTO.java +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportDTO.java @@ -1,4 +1,26 @@ package com.sztzjy.linkCommerce.entity.dto; import java.math.BigDecimal; import java.util.List; -public class StudentExperimentReportDTO { private String teachingClassId; private Integer rank; private BigDecimal totalScore; private List modules; private List 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 List getModules(){return modules;} public void setModules(List v){modules=v;} public List getTasks(){return tasks;} public void setTasks(List v){tasks=v;} } +public class StudentExperimentReportDTO { + private String teachingClassId; + private Integer rank; + private BigDecimal totalScore; + private BigDecimal aiAssessmentScore; + private BigDecimal teacherScore; + private List modules; + private List 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 getModules() { return modules; } + public void setModules(List v) { modules = v; } + public List getTasks() { return tasks; } + public void setTasks(List v) { tasks = v; } +} diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportTaskDTO.java b/src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportTaskDTO.java index 3d906a1..0946781 100644 --- a/src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportTaskDTO.java +++ b/src/main/java/com/sztzjy/linkCommerce/entity/dto/StudentExperimentReportTaskDTO.java @@ -1,3 +1,22 @@ 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 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 class StudentExperimentReportTaskDTO { + 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; } +} diff --git a/src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java b/src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java index fda9bbf..5c63e4c 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/TrainingTaskService.java @@ -33,6 +33,9 @@ public interface TrainingTaskService { 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 restoreClassTasksDefaults(String teachingClassId, String operatorId); diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImpl.java index 6d05b53..6360f67 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentExperimentReportServiceImpl.java @@ -1,11 +1,217 @@ 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.*; -@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")}; - public StudentExperimentReportDTO getCurrentReport(JwtUser user){ String classId=studentTeachingClassResolver.resolveRequired(user); StudentExperimentReportDTO report=new StudentExperimentReportDTO(); report.setTeachingClassId(classId); List modules=new ArrayList<>(); Map index=new HashMap<>(); BigDecimal[] weights=weights(classId); boolean comprehensiveParticipating=hasPublishedComprehensiveTask(classId); for(int i=0;i 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; } - private boolean hasPublishedComprehensiveTask(String classId){if(taskAllocationMapper==null)return false;TaskAllocationExample example=new TaskAllocationExample();example.createCriteria().andClassIdEqualTo(classId);List allocations=taskAllocationMapper.selectByExample(example);boolean published=false;boolean enabled=false;for(TaskAllocation allocation:allocations==null?Collections.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 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.config.security.JwtUser; +import com.sztzjy.linkCommerce.entity.StudentTrainingAnswer; +import com.sztzjy.linkCommerce.entity.TaskAllocation; +import com.sztzjy.linkCommerce.entity.TaskAllocationExample; +import com.sztzjy.linkCommerce.entity.TeachingClassScoreWeight; +import com.sztzjy.linkCommerce.entity.TrainingTask; +import com.sztzjy.linkCommerce.entity.dto.StudentExperimentReportDTO; +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 modules = new ArrayList<>(); + Map moduleByName = new HashMap<>(); + Map 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 tasks = new ArrayList<>(); + + List allocations = schoolDefaultTaskService.resolveForTeachingClass(classId); + for (TaskAllocation allocation : allocations == null + ? Collections.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 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 allocations = taskAllocationMapper.selectByExample(example); + boolean published = false; + boolean enabled = false; + for (TaskAllocation allocation : allocations == null + ? Collections.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() + }; + } } diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java index 1de4ad3..730c34f 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java @@ -252,6 +252,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { @Override public List listForTeachingClass(String teachingClassId, Boolean enabledOnly, JwtUser operator) { requireReadableTeachingClass(teachingClassId, operator); + ensureDefaultTasks(null); List defaults = trainingTaskMapper.selectList(null, null, null); return filterEnabled(readClassTasks(teachingClassId, defaults), enabledOnly); } @@ -318,6 +319,26 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { 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) { if (trainingKnowledgeService == null || task == null) return; try { @@ -446,12 +467,12 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { } /** - * @return null when this teaching class has never been configured (default: all tasks), - * otherwise the explicitly published task keys, which may be empty. + * @return the explicitly published task keys, or the first three tasks in the first + * project when this teaching class has never been configured. */ private Set getPublishedTaskKeys(String teachingClassId) { if (taskAllocationMapper == null || StringUtils.isBlank(teachingClassId)) { - return null; + return getDefaultPublishedTaskKeys(); } TaskAllocationExample example = new TaskAllocationExample(); example.createCriteria().andClassIdEqualTo(teachingClassId); @@ -467,7 +488,34 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { } } } - return configured ? taskKeys : null; + return configured ? taskKeys : getDefaultPublishedTaskKeys(); + } + + private Set getDefaultPublishedTaskKeys() { + List defaults = sortTasks(trainingTaskMapper.selectList(null, null, null)); + Set 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) { @@ -738,6 +786,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { task.setAiManualUrl(config.getAiManualUrl()); task.setReferenceAnswerName(config.getReferenceAnswerName()); task.setReferenceAnswerUrl(config.getReferenceAnswerUrl()); + task.setReferenceAnswerEnabled(config.getReferenceAnswerEnabled()); task.setSort(config.getSort()); task.setEnabled(config.getEnabled()); task.setCreatedBy(config.getCreatedBy()); @@ -842,6 +891,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { "ai_manual_url varchar(500) NULL," + "reference_answer_name varchar(255) NULL," + "reference_answer_url varchar(500) NULL," + + "reference_answer_enabled bit(1) NOT NULL DEFAULT b'0' COMMENT '是否向学生开放参考答案'," + "sort int DEFAULT 0," + "enabled bit(1) DEFAULT b'1'," + "created_by varchar(64) NULL," + @@ -855,9 +905,26 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { ensureTextColumn("training_task_class_config", "step_one_questions", "第一步选择题JSON数组"); ensureMaterialColumns("training_task_class_config"); ensureReferenceAnswerColumns("training_task_class_config"); + ensureReferenceAnswerVisibilityColumn("training_task_class_config"); 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. */ private void ensureTextColumn(String tableName, String columnName, String comment) { if (jdbcTemplate == null) { diff --git a/src/main/resources/mappers/TrainingTaskClassConfigMapper.xml b/src/main/resources/mappers/TrainingTaskClassConfigMapper.xml index 1bd019d..d78358d 100644 --- a/src/main/resources/mappers/TrainingTaskClassConfigMapper.xml +++ b/src/main/resources/mappers/TrainingTaskClassConfigMapper.xml @@ -23,6 +23,7 @@ + @@ -34,7 +35,7 @@ id, teaching_class_id, task_key, project_name, task_name, knowledge, background, objectives, requirements, 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, - reference_answer_name, reference_answer_url, + reference_answer_name, reference_answer_url, reference_answer_enabled, sort, enabled, created_by, create_time, update_time @@ -103,6 +104,7 @@ ai_manual_url, reference_answer_name, reference_answer_url, + reference_answer_enabled, sort, enabled, created_by, @@ -131,6 +133,7 @@ #{aiManualUrl,jdbcType=VARCHAR}, #{referenceAnswerName,jdbcType=VARCHAR}, #{referenceAnswerUrl,jdbcType=VARCHAR}, + #{referenceAnswerEnabled,jdbcType=BIT}, #{sort,jdbcType=INTEGER}, #{enabled,jdbcType=BIT}, #{createdBy,jdbcType=VARCHAR}, @@ -160,6 +163,7 @@ ai_manual_url = #{aiManualUrl,jdbcType=VARCHAR}, reference_answer_name = #{referenceAnswerName,jdbcType=VARCHAR}, reference_answer_url = #{referenceAnswerUrl,jdbcType=VARCHAR}, + reference_answer_enabled = #{referenceAnswerEnabled,jdbcType=BIT}, sort = #{sort,jdbcType=INTEGER}, enabled = #{enabled,jdbcType=BIT}, update_time = #{updateTime,jdbcType=TIMESTAMP}, diff --git a/src/test/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreControllerTaskReviewTest.java b/src/test/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreControllerTaskReviewTest.java index 979f581..6084e55 100644 --- a/src/test/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreControllerTaskReviewTest.java +++ b/src/test/java/com/sztzjy/linkCommerce/controller/stu/TeaScoreControllerTaskReviewTest.java @@ -46,6 +46,17 @@ class TeaScoreControllerTaskReviewTest { "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 void batchScoreRejectsTaskThatIsNotAllocatedToTheClassBeforeWritingAnyScore() { TeaScoreController controller = controllerWithCompletedAnswer(); diff --git a/src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java b/src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java index d8af460..29391cf 100644 --- a/src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java +++ b/src/test/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImplTest.java @@ -128,6 +128,20 @@ class TrainingTaskServiceImplTest { 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 void importTaskRejectsUnknownTaskKeyBecauseTaskCountIsFixed() { TrainingTaskServiceImpl service = new TrainingTaskServiceImpl(); @@ -649,6 +663,8 @@ class TrainingTaskServiceImplTest { payload.setTaskName("Class Name"); payload.setKnowledge("Class Knowledge"); payload.setMaterialName("Class Material"); + payload.setReferenceAnswerName("参考答案.docx"); + payload.setReferenceAnswerUrl("/file/reference-answer.docx"); payload.setEnabled(Boolean.TRUE); service.saveClassTask("class-1", "new-product-survey", payload, "teacher-1"); @@ -665,6 +681,8 @@ class TrainingTaskServiceImplTest { assertEquals(Integer.valueOf(3), savedOverride.getSort()); assertEquals("Class Knowledge", savedOverride.getKnowledge()); assertEquals("Class Material", savedOverride.getMaterialName()); + assertEquals("参考答案.docx", savedOverride.getReferenceAnswerName()); + assertEquals("/file/reference-answer.docx", savedOverride.getReferenceAnswerUrl()); } @Test