All checks were successful
continuous-integration/drone/push Build is passing
本次提交完成了武术比赛系统的核心功能模块,包括: ## 1. 成绩计算引擎 (Tasks 1.1-1.8) ✅ - 实现多裁判评分平均分计算(去最高/最低分) - 支持难度系数应用 - 自动排名算法(支持并列) - 奖牌自动分配(金银铜) - 成绩复核机制 - 成绩发布/撤销审批流程 ## 2. 比赛日流程功能 (Tasks 2.1-2.6) ✅ - 运动员签到/检录系统 - 评分有效性验证(范围检查0-10分) - 异常分数警告机制(偏差>2.0) - 异常情况记录和处理 - 检录长角色权限管理 - 比赛状态流转管理 ## 3. 导出打印功能 (Tasks 3.1-3.4) ✅ - 成绩单Excel导出(EasyExcel) - 运动员名单Excel导出 - 赛程表Excel导出 - 证书生成(HTML模板+数据接口) ## 4. 单元测试 ✅ - MartialResultServiceTest: 10个测试用例 - MartialScoreServiceTest: 10个测试用例 - MartialAthleteServiceTest: 14个测试用例 - 测试通过率: 100% (34/34) ## 技术实现 - 使用BigDecimal进行精度计算(保留3位小数) - EasyExcel实现Excel导出 - HTML证书模板(支持浏览器打印为PDF) - JUnit 5 + Mockito单元测试框架 ## 新增文件 - 3个新控制器:MartialExportController, MartialExceptionEventController, MartialJudgeProjectController - 3个Excel VO类:ResultExportExcel, AthleteExportExcel, ScheduleExportExcel - CertificateVO证书数据对象 - 证书HTML模板 - 3个测试类(676行测试代码) - 任务文档(docs/tasks/) - 数据库迁移脚本 ## 项目进度 已完成: 64% (18/28 任务) - ✅ 成绩计算引擎: 100% - ✅ 比赛日流程: 100% - ✅ 导出打印功能: 80% 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
15 KiB
15 KiB
成绩计算引擎 - 详细任务清单
优先级: P0(最高) 预计工时: 5天 负责人: 待分配 创建时间: 2025-11-30 最后更新: 2025-11-30
📋 任务概述
成绩计算引擎是武术比赛系统的核心功能,负责从裁判评分到最终排名的自动化计算。
核心流程
裁判打分 → 收集评分 → 去最高/最低分 → 计算平均分
↓
应用难度系数 → 计算最终得分 → 自动排名 → 分配奖牌
✅ 任务列表
任务 1.1:多裁判评分平均分计算 🔴
状态: 未开始
工时: 0.5天
文件位置: MartialResultServiceImpl.java
需求描述
- 获取某运动员某项目的所有裁判评分
- 计算有效评分的平均值
- 记录最高分、最低分
实现要点
public BigDecimal calculateAverageScore(Long athleteId, Long projectId) {
// 1. 查询所有裁判评分
List<MartialScore> scores = scoreService.list(
new QueryWrapper<MartialScore>()
.eq("athlete_id", athleteId)
.eq("project_id", projectId)
.eq("is_deleted", 0)
);
// 2. 提取分数值
List<BigDecimal> 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
需求描述
- 从所有裁判评分中去掉一个最高分
- 去掉一个最低分
- 计算剩余有效评分的平均值
实现要点
public BigDecimal calculateValidAverageScore(Long athleteId, Long projectId) {
// 1. 获取所有评分
List<MartialScore> 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<BigDecimal> 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
需求描述
- 从项目表获取难度系数
- 将平均分乘以难度系数
- 生成调整后的分数
实现要点
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);
}
数据库字段
-- 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
需求描述
- 整合所有计算步骤
- 保存完整的成绩记录
- 记录计算明细(最高分、最低分、有效分数等)
实现要点
public MartialResult calculateFinalScore(Long athleteId, Long projectId) {
// 1. 获取所有裁判评分
List<MartialScore> scores = scoreService.list(
new QueryWrapper<MartialScore>()
.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
需求描述
- 按项目对所有运动员进行排名
- 处理并列排名情况
- 更新排名到数据库
实现要点
public void autoRanking(Long projectId) {
// 1. 获取该项目所有最终成绩,按分数降序
List<MartialResult> results = this.list(
new QueryWrapper<MartialResult>()
.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
需求描述
- 自动分配金银铜牌给前三名
- 处理并列情况(如并列第一名,两人都得金牌)
- 更新奖牌字段
实现要点
public void assignMedals(Long projectId) {
// 1. 获取前三名(按排名)
List<MartialResult> topResults = this.list(
new QueryWrapper<MartialResult>()
.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
需求描述
- 提供成绩复核接口
- 记录复核原因和结果
- 支持成绩调整
实现要点
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
需求描述
- 成绩确认为最终成绩
- 记录发布时间
- 限制已发布成绩的修改
实现要点
public void publishResults(Long projectId) {
List<MartialResult> results = this.list(
new QueryWrapper<MartialResult>()
.eq("project_id", projectId)
);
for (MartialResult result : results) {
result.setIsFinal(1); // 标记为最终成绩
result.setPublishTime(LocalDateTime.now());
}
this.updateBatchById(results);
}
public void unpublishResults(Long projectId) {
// 撤销发布(管理员权限)
List<MartialResult> results = this.list(
new QueryWrapper<MartialResult>()
.eq("project_id", projectId)
);
for (MartialResult result : results) {
result.setIsFinal(0);
result.setPublishTime(null);
}
this.updateBatchById(results);
}
测试用例
- 发布成绩
- 撤销发布
- 已发布成绩的权限控制
🎯 Controller 层接口设计
新增 API 接口
@RestController
@RequestMapping("/martial/result")
public class MartialResultController extends BladeController {
@Autowired
private IMartialResultService resultService;
/**
* 计算运动员最终成绩
*/
@PostMapping("/calculate")
@Operation(summary = "计算最终成绩")
public R<MartialResult> 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个运动员同时计算
- 批量排名性能
📝 开发注意事项
- 精度处理: 所有分数计算使用
BigDecimal,保留3位小数 - 并发控制: 成绩计算可能被多次触发,需要考虑幂等性
- 数据一致性: 成绩更新后需要触发排名重新计算
- 异常处理: 裁判人数不足、评分缺失等异常情况
- 权限控制: 成绩发布、复核等敏感操作需要权限验证
✅ 验收标准
- 所有单元测试通过
- API接口文档完整(Swagger)
- 成绩计算精度达到0.001
- 排名算法处理并列情况正确
- 已发布成绩不可随意修改
- 代码通过Code Review
下一步: 完成后进入 02-比赛日流程功能.md