# 成绩计算引擎 - 详细任务清单 **优先级:** P0(最高) **预计工时:** 5天 **负责人:** 待分配 **创建时间:** 2025-11-30 **最后更新:** 2025-11-30 --- ## 📋 任务概述 成绩计算引擎是武术比赛系统的核心功能,负责从裁判评分到最终排名的自动化计算。 ### 核心流程 ``` 裁判打分 → 收集评分 → 去最高/最低分 → 计算平均分 ↓ 应用难度系数 → 计算最终得分 → 自动排名 → 分配奖牌 ``` --- ## ✅ 任务列表 ### 任务 1.1:多裁判评分平均分计算 🔴 **状态:** 未开始 **工时:** 0.5天 **文件位置:** `MartialResultServiceImpl.java` #### 需求描述 - 获取某运动员某项目的所有裁判评分 - 计算有效评分的平均值 - 记录最高分、最低分 #### 实现要点 ```java public BigDecimal calculateAverageScore(Long athleteId, Long projectId) { // 1. 查询所有裁判评分 List scores = scoreService.list( new QueryWrapper() .eq("athlete_id", athleteId) .eq("project_id", projectId) .eq("is_deleted", 0) ); // 2. 提取分数值 List scoreValues = scores.stream() .map(MartialScore::getScore) .collect(Collectors.toList()); // 3. 计算平均分(后续会去最高/最低) BigDecimal sum = scoreValues.stream() .reduce(BigDecimal.ZERO, BigDecimal::add); return sum.divide( new BigDecimal(scoreValues.size()), 3, RoundingMode.HALF_UP ); } ``` #### 测试用例 - [ ] 单个裁判评分 - [ ] 多个裁判评分(3-10人) - [ ] 边界值测试(5.000, 10.000) --- ### 任务 1.2:去最高分/去最低分逻辑 🔴 **状态:** 未开始 **工时:** 0.5天 **文件位置:** `MartialResultServiceImpl.java` #### 需求描述 - 从所有裁判评分中去掉一个最高分 - 去掉一个最低分 - 计算剩余有效评分的平均值 #### 实现要点 ```java public BigDecimal calculateValidAverageScore(Long athleteId, Long projectId) { // 1. 获取所有评分 List scores = scoreService.list(...); if (scores.size() < 3) { throw new ServiceException("裁判人数不足3人,无法去最高/最低分"); } // 2. 找出最高分和最低分 BigDecimal maxScore = scores.stream() .map(MartialScore::getScore) .max(Comparator.naturalOrder()) .orElse(BigDecimal.ZERO); BigDecimal minScore = scores.stream() .map(MartialScore::getScore) .min(Comparator.naturalOrder()) .orElse(BigDecimal.ZERO); // 3. 过滤有效评分(去掉一个最高、一个最低) List validScores = new ArrayList<>(); boolean maxRemoved = false; boolean minRemoved = false; for (MartialScore score : scores) { BigDecimal val = score.getScore(); if (!maxRemoved && val.equals(maxScore)) { maxRemoved = true; continue; } if (!minRemoved && val.equals(minScore)) { minRemoved = true; continue; } validScores.add(val); } // 4. 计算平均分 BigDecimal sum = validScores.stream() .reduce(BigDecimal.ZERO, BigDecimal::add); return sum.divide( new BigDecimal(validScores.size()), 3, RoundingMode.HALF_UP ); } ``` #### 测试用例 - [ ] 正常情况:5个裁判,去掉最高最低后剩3个 - [ ] 边界情况:3个裁判,去掉最高最低后剩1个 - [ ] 异常情况:少于3个裁判,抛出异常 --- ### 任务 1.3:难度系数应用 🔴 **状态:** 未开始 **工时:** 0.5天 **文件位置:** `MartialResultServiceImpl.java` #### 需求描述 - 从项目表获取难度系数 - 将平均分乘以难度系数 - 生成调整后的分数 #### 实现要点 ```java public BigDecimal applyDifficultyCoefficient( BigDecimal averageScore, Long projectId ) { // 1. 获取项目信息 MartialProject project = projectService.getById(projectId); if (project == null) { throw new ServiceException("项目不存在"); } // 2. 获取难度系数(默认1.00) BigDecimal coefficient = project.getDifficultyCoefficient(); if (coefficient == null) { coefficient = new BigDecimal("1.00"); } // 3. 应用系数 return averageScore.multiply(coefficient) .setScale(3, RoundingMode.HALF_UP); } ``` #### 数据库字段 ```sql -- martial_project 表需要添加字段(如果没有) ALTER TABLE martial_project ADD COLUMN difficulty_coefficient DECIMAL(5,2) DEFAULT 1.00 COMMENT '难度系数'; ``` #### 测试用例 - [ ] 系数 = 1.00(无调整) - [ ] 系数 = 1.20(加分) - [ ] 系数 = 0.80(减分) --- ### 任务 1.4:最终得分计算 🔴 **状态:** 未开始 **工时:** 1天 **文件位置:** `MartialResultServiceImpl.java` #### 需求描述 - 整合所有计算步骤 - 保存完整的成绩记录 - 记录计算明细(最高分、最低分、有效分数等) #### 实现要点 ```java public MartialResult calculateFinalScore(Long athleteId, Long projectId) { // 1. 获取所有裁判评分 List scores = scoreService.list( new QueryWrapper() .eq("athlete_id", athleteId) .eq("project_id", projectId) ); if (scores.isEmpty()) { throw new ServiceException("该运动员尚未有裁判评分"); } // 2. 找出最高分和最低分 BigDecimal maxScore = scores.stream() .map(MartialScore::getScore) .max(Comparator.naturalOrder()) .orElse(BigDecimal.ZERO); BigDecimal minScore = scores.stream() .map(MartialScore::getScore) .min(Comparator.naturalOrder()) .orElse(BigDecimal.ZERO); // 3. 去最高/最低分,计算平均分 BigDecimal averageScore = calculateValidAverageScore(athleteId, projectId); // 4. 应用难度系数 BigDecimal finalScore = applyDifficultyCoefficient(averageScore, projectId); // 5. 获取运动员和项目信息 MartialAthlete athlete = athleteService.getById(athleteId); MartialProject project = projectService.getById(projectId); // 6. 保存成绩记录 MartialResult result = new MartialResult(); result.setCompetitionId(athlete.getCompetitionId()); result.setAthleteId(athleteId); result.setProjectId(projectId); result.setPlayerName(athlete.getPlayerName()); result.setTeamName(athlete.getTeamName()); result.setTotalScore(averageScore); // 平均分 result.setMaxScore(maxScore); result.setMinScore(minScore); result.setValidScoreCount(scores.size() - 2); // 去掉最高最低 result.setDifficultyCoefficient(project.getDifficultyCoefficient()); result.setFinalScore(finalScore); // 最终得分 result.setIsFinal(0); // 初始为非最终成绩 this.saveOrUpdate(result); return result; } ``` #### 测试用例 - [ ] 完整流程测试(5个裁判评分) - [ ] 数据持久化验证 - [ ] 重复计算测试(更新而非新增) --- ### 任务 1.5:自动排名算法 🔴 **状态:** 未开始 **工时:** 1天 **文件位置:** `MartialResultServiceImpl.java` #### 需求描述 - 按项目对所有运动员进行排名 - 处理并列排名情况 - 更新排名到数据库 #### 实现要点 ```java public void autoRanking(Long projectId) { // 1. 获取该项目所有最终成绩,按分数降序 List results = this.list( new QueryWrapper() .eq("project_id", projectId) .eq("is_final", 1) // 只对最终成绩排名 .orderByDesc("final_score") ); if (results.isEmpty()) { throw new ServiceException("该项目尚无最终成绩"); } // 2. 分配排名(处理并列) int currentRank = 1; BigDecimal previousScore = null; int sameScoreCount = 0; for (int i = 0; i < results.size(); i++) { MartialResult result = results.get(i); BigDecimal currentScore = result.getFinalScore(); if (previousScore != null && currentScore.equals(previousScore)) { // 分数相同,并列 sameScoreCount++; } else { // 分数不同,更新排名 currentRank += sameScoreCount; sameScoreCount = 1; } result.setRanking(currentRank); previousScore = currentScore; } // 3. 批量更新 this.updateBatchById(results); } ``` #### 测试用例 - [ ] 无并列情况 - [ ] 有并列情况(2人同分) - [ ] 多人并列情况(3人同分) --- ### 任务 1.6:奖牌自动分配 🔴 **状态:** 未开始 **工时:** 0.5天 **文件位置:** `MartialResultServiceImpl.java` #### 需求描述 - 自动分配金银铜牌给前三名 - 处理并列情况(如并列第一名,两人都得金牌) - 更新奖牌字段 #### 实现要点 ```java public void assignMedals(Long projectId) { // 1. 获取前三名(按排名) List topResults = this.list( new QueryWrapper() .eq("project_id", projectId) .eq("is_final", 1) .le("ranking", 3) // 排名 <= 3 .orderByAsc("ranking") ); // 2. 分配奖牌 for (MartialResult result : topResults) { Integer ranking = result.getRanking(); if (ranking == 1) { result.setMedal(1); // 金牌 } else if (ranking == 2) { result.setMedal(2); // 银牌 } else if (ranking == 3) { result.setMedal(3); // 铜牌 } } // 3. 批量更新 this.updateBatchById(topResults); } ``` #### 测试用例 - [ ] 正常情况:前3名分配金银铜 - [ ] 并列第一:2人都得金牌,第3名得铜牌(跳过银牌) - [ ] 并列第二:第1名金牌,2人都得银牌 --- ### 任务 1.7:成绩复核机制 🔴 **状态:** 未开始 **工时:** 0.5天 **文件位置:** `MartialResultServiceImpl.java` #### 需求描述 - 提供成绩复核接口 - 记录复核原因和结果 - 支持成绩调整 #### 实现要点 ```java public void reviewResult(Long resultId, String reviewNote, BigDecimal adjustment) { MartialResult result = this.getById(resultId); if (result == null) { throw new ServiceException("成绩记录不存在"); } // 记录原始分数 result.setOriginalScore(result.getFinalScore()); // 应用调整 if (adjustment != null) { BigDecimal newScore = result.getFinalScore().add(adjustment); result.setAdjustedScore(newScore); result.setFinalScore(newScore); result.setAdjustRange(adjustment); } result.setAdjustNote(reviewNote); this.updateById(result); // 重新排名 autoRanking(result.getProjectId()); } ``` #### 测试用例 - [ ] 成绩上调 - [ ] 成绩下调 - [ ] 调整后重新排名 --- ### 任务 1.8:成绩发布审批流程 🔴 **状态:** 未开始 **工时:** 0.5天 **文件位置:** `MartialResultServiceImpl.java` #### 需求描述 - 成绩确认为最终成绩 - 记录发布时间 - 限制已发布成绩的修改 #### 实现要点 ```java public void publishResults(Long projectId) { List results = this.list( new QueryWrapper() .eq("project_id", projectId) ); for (MartialResult result : results) { result.setIsFinal(1); // 标记为最终成绩 result.setPublishTime(LocalDateTime.now()); } this.updateBatchById(results); } public void unpublishResults(Long projectId) { // 撤销发布(管理员权限) List results = this.list( new QueryWrapper() .eq("project_id", projectId) ); for (MartialResult result : results) { result.setIsFinal(0); result.setPublishTime(null); } this.updateBatchById(results); } ``` #### 测试用例 - [ ] 发布成绩 - [ ] 撤销发布 - [ ] 已发布成绩的权限控制 --- ## 🎯 Controller 层接口设计 ### 新增 API 接口 ```java @RestController @RequestMapping("/martial/result") public class MartialResultController extends BladeController { @Autowired private IMartialResultService resultService; /** * 计算运动员最终成绩 */ @PostMapping("/calculate") @Operation(summary = "计算最终成绩") public R calculateScore( @RequestParam Long athleteId, @RequestParam Long projectId ) { MartialResult result = resultService.calculateFinalScore(athleteId, projectId); return R.data(result); } /** * 项目自动排名 */ @PostMapping("/ranking") @Operation(summary = "自动排名") public R autoRanking(@RequestParam Long projectId) { resultService.autoRanking(projectId); return R.success("排名完成"); } /** * 分配奖牌 */ @PostMapping("/medals") @Operation(summary = "分配奖牌") public R assignMedals(@RequestParam Long projectId) { resultService.assignMedals(projectId); return R.success("奖牌分配完成"); } /** * 成绩复核 */ @PostMapping("/review") @Operation(summary = "成绩复核") public R reviewResult( @RequestParam Long resultId, @RequestParam String reviewNote, @RequestParam(required = false) BigDecimal adjustment ) { resultService.reviewResult(resultId, reviewNote, adjustment); return R.success("复核完成"); } /** * 发布成绩 */ @PostMapping("/publish") @Operation(summary = "发布成绩") public R publishResults(@RequestParam Long projectId) { resultService.publishResults(projectId); return R.success("成绩已发布"); } } ``` --- ## 📦 依赖配置 无需额外依赖,使用现有的: - MyBatis-Plus(数据访问) - Java BigDecimal(精度计算) --- ## 🧪 测试计划 ### 单元测试 - [ ] 平均分计算测试 - [ ] 去最高/最低分测试 - [ ] 难度系数应用测试 - [ ] 排名算法测试 - [ ] 奖牌分配测试 ### 集成测试 - [ ] 完整成绩计算流程 - [ ] 多项目并发计算 - [ ] 成绩发布流程 ### 性能测试 - [ ] 100个运动员同时计算 - [ ] 批量排名性能 --- ## 📝 开发注意事项 1. **精度处理:** 所有分数计算使用 `BigDecimal`,保留3位小数 2. **并发控制:** 成绩计算可能被多次触发,需要考虑幂等性 3. **数据一致性:** 成绩更新后需要触发排名重新计算 4. **异常处理:** 裁判人数不足、评分缺失等异常情况 5. **权限控制:** 成绩发布、复核等敏感操作需要权限验证 --- ## ✅ 验收标准 - [ ] 所有单元测试通过 - [ ] API接口文档完整(Swagger) - [ ] 成绩计算精度达到0.001 - [ ] 排名算法处理并列情况正确 - [ ] 已发布成绩不可随意修改 - [ ] 代码通过Code Review --- **下一步:** 完成后进入 [02-比赛日流程功能.md](./02-比赛日流程功能.md)