# 成绩计算引擎 - 详细任务清单

**优先级：** P0（最高）
**预计工时：** 5天
**负责人：** 待分配
**创建时间：** 2025-11-30
**最后更新：** 2025-11-30

---

## 📋 任务概述

成绩计算引擎是武术比赛系统的核心功能，负责从裁判评分到最终排名的自动化计算。

### 核心流程
```
裁判打分 → 收集评分 → 去最高/最低分 → 计算平均分
    ↓
应用难度系数 → 计算最终得分 → 自动排名 → 分配奖牌
```

---

## ✅ 任务列表

### 任务 1.1：多裁判评分平均分计算 🔴

**状态：** 未开始
**工时：** 0.5天
**文件位置：** `MartialResultServiceImpl.java`

#### 需求描述
- 获取某运动员某项目的所有裁判评分
- 计算有效评分的平均值
- 记录最高分、最低分

#### 实现要点
```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`

#### 需求描述
- 从所有裁判评分中去掉一个最高分
- 去掉一个最低分
- 计算剩余有效评分的平均值

#### 实现要点
```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`

#### 需求描述
- 从项目表获取难度系数
- 将平均分乘以难度系数
- 生成调整后的分数

#### 实现要点
```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<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`

#### 需求描述
- 按项目对所有运动员进行排名
- 处理并列排名情况
- 更新排名到数据库

#### 实现要点
```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`

#### 需求描述
- 自动分配金银铜牌给前三名
- 处理并列情况（如并列第一名，两人都得金牌）
- 更新奖牌字段

#### 实现要点
```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`

#### 需求描述
- 提供成绩复核接口
- 记录复核原因和结果
- 支持成绩调整

#### 实现要点
```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<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 接口

```java
@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个运动员同时计算
- [ ] 批量排名性能

---

## 📝 开发注意事项

1. **精度处理：** 所有分数计算使用 `BigDecimal`，保留3位小数
2. **并发控制：** 成绩计算可能被多次触发，需要考虑幂等性
3. **数据一致性：** 成绩更新后需要触发排名重新计算
4. **异常处理：** 裁判人数不足、评分缺失等异常情况
5. **权限控制：** 成绩发布、复核等敏感操作需要权限验证

---

## ✅ 验收标准

- [ ] 所有单元测试通过
- [ ] API接口文档完整（Swagger）
- [ ] 成绩计算精度达到0.001
- [ ] 排名算法处理并列情况正确
- [ ] 已发布成绩不可随意修改
- [ ] 代码通过Code Review

---

**下一步：** 完成后进入 [02-比赛日流程功能.md](./02-比赛日流程功能.md)
