fix: 优化评分系统总分显示逻辑
Some checks are pending
continuous-integration/drone/push Build is pending

1. 修复 updateAthleteTotalScore 方法,使用 getChiefJudgeIds() 排除裁判长的所有评分
2. 修复 getRequiredJudgeCount 方法,使用 distinct 去重统计普通裁判数量
3. 新增 scoringComplete、scoredJudgeCount、requiredJudgeCount 字段
4. 总分只在所有普通裁判评分完成后才显示
5. 总分算法:去掉最高最低分取平均,裁判数<3时直接取平均

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-23 23:09:31 +08:00
parent 1d6c3d9df5
commit abb1391b2f
2 changed files with 262 additions and 41 deletions

View File

@@ -21,13 +21,17 @@ import org.springblade.modules.martial.pojo.vo.MiniLoginVO;
import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO;
import com.alibaba.fastjson.JSON;
import org.springblade.modules.martial.service.*;
import org.springblade.modules.martial.pojo.entity.MtVenue;
import org.springblade.core.redis.cache.BladeRedis;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -46,9 +50,16 @@ public class MartialMiniController extends BladeController {
private final IMartialJudgeService judgeService;
private final IMartialCompetitionService competitionService;
private final IMartialVenueService venueService;
private final IMtVenueService mtVenueService;
private final IMartialProjectService projectService;
private final IMartialAthleteService athleteService;
private final IMartialScoreService scoreService;
private final BladeRedis bladeRedis;
// Redis缓存key前缀
private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:";
// 登录缓存过期时间7天
private static final Duration LOGIN_CACHE_EXPIRE = Duration.ofDays(7);
/**
* 登录验证
@@ -92,9 +103,10 @@ public class MartialMiniController extends BladeController {
invite.setDeviceInfo(dto.getDeviceInfo());
judgeInviteService.updateById(invite);
MartialVenue venue = null;
// 从 mt_venue 表获取场地信息
MtVenue mtVenue = null;
if (invite.getVenueId() != null) {
venue = venueService.getById(invite.getVenueId());
mtVenue = mtVenueService.getById(invite.getVenueId());
}
List<MiniLoginVO.ProjectInfo> projects = parseProjects(invite.getProjects());
@@ -108,10 +120,14 @@ public class MartialMiniController extends BladeController {
competition.getCompetitionStartTime().toString() : "");
vo.setJudgeId(judge.getId());
vo.setJudgeName(judge.getName());
vo.setVenueId(venue != null ? venue.getId() : null);
vo.setVenueName(venue != null ? venue.getVenueName() : null);
vo.setVenueId(mtVenue != null ? mtVenue.getId() : null);
vo.setVenueName(mtVenue != null ? mtVenue.getVenueName() : null);
vo.setProjects(projects);
// 将登录信息缓存到Redis服务重启后仍然有效
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE);
return R.data(vo);
}
@@ -158,8 +174,9 @@ public class MartialMiniController extends BladeController {
if (success) {
Long athleteId = parseLong(dto.getAthleteId());
Long projectId = parseLong(dto.getProjectId());
Long venueId = parseLong(dto.getVenueId());
if (athleteId != null && projectId != null) {
updateAthleteTotalScore(athleteId, projectId);
updateAthleteTotalScore(athleteId, projectId, venueId);
}
}
@@ -168,33 +185,53 @@ public class MartialMiniController extends BladeController {
/**
* 计算并更新选手总分
* 总分 = 所有裁判评分的平均值保留3位小数
* 总分算法:去掉一个最高分和一个最低分,取剩余分数的平均值
* 特殊情况:裁判数量<3时直接取平均分
* 只有所有裁判都评分完成后才更新总分
*/
private void updateAthleteTotalScore(Long athleteId, Long projectId) {
private void updateAthleteTotalScore(Long athleteId, Long projectId, Long venueId) {
try {
// 查询该选手在该项目的所有评分
// 1. 查询该场地的普通裁判数量
int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 2. 获取裁判长ID列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
// 3. 查询该选手在该项目的所有评分(排除裁判长的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getAthleteId, athleteId);
scoreQuery.eq(MartialScore::getProjectId, projectId);
scoreQuery.eq(MartialScore::getIsDeleted, 0);
// 排除裁判长的所有评分(包括普通评分和修改记录)
if (!chiefJudgeIds.isEmpty()) {
scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds);
}
List<MartialScore> scores = scoreService.list(scoreQuery);
if (scores != null && !scores.isEmpty()) {
// 计算平均分
BigDecimal totalScore = scores.stream()
.map(MartialScore::getScore)
.filter(s -> s != null)
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal avgScore = totalScore.divide(
new BigDecimal(scores.size()),
3,
RoundingMode.HALF_UP
);
// 4. 判断是否所有裁判都已评分
if (scores == null || scores.isEmpty()) {
return;
}
// 更新选手总分
// 如果配置了裁判数量,检查是否评分完成
if (requiredJudgeCount > 0 && scores.size() < requiredJudgeCount) {
// 未完成评分,清空总分
MartialAthlete athlete = athleteService.getById(athleteId);
if (athlete != null && athlete.getTotalScore() != null) {
athlete.setTotalScore(null);
athleteService.updateById(athlete);
}
return;
}
// 4. 计算总分(去掉最高最低分取平均)
BigDecimal totalScore = calculateTotalScore(scores);
// 5. 更新选手总分
if (totalScore != null) {
MartialAthlete athlete = athleteService.getById(athleteId);
if (athlete != null) {
athlete.setTotalScore(avgScore);
athlete.setTotalScore(totalScore);
athleteService.updateById(athlete);
}
}
@@ -204,6 +241,65 @@ public class MartialMiniController extends BladeController {
}
}
/**
* 获取场地应评分的裁判数量(普通裁判,不包括裁判长)
* 注意:使用 DISTINCT judge_id 来避免重复计数
*/
private int getRequiredJudgeCount(Long venueId) {
if (venueId == null) {
return 0;
}
LambdaQueryWrapper<MartialJudgeInvite> judgeQuery = new LambdaQueryWrapper<>();
judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId);
judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
judgeQuery.ne(MartialJudgeInvite::getRole, "chief_judge"); // 排除裁判长
List<MartialJudgeInvite> judges = judgeInviteService.list(judgeQuery);
// 使用 distinct judge_id 来计算不重复的裁判数量
return (int) judges.stream()
.map(MartialJudgeInvite::getJudgeId)
.filter(Objects::nonNull)
.distinct()
.count();
}
/**
* 计算总分
* 算法:去掉一个最高分和一个最低分,取剩余分数的平均值
* 特殊情况:裁判数量<3时直接取平均分
*/
private BigDecimal calculateTotalScore(List<MartialScore> scores) {
if (scores == null || scores.isEmpty()) {
return null;
}
// 提取所有分数并排序
List<BigDecimal> scoreValues = scores.stream()
.map(MartialScore::getScore)
.filter(Objects::nonNull)
.sorted()
.collect(Collectors.toList());
int count = scoreValues.size();
if (count == 0) {
return null;
}
if (count < 3) {
// 裁判数量<3直接取平均分
BigDecimal sum = scoreValues.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(new BigDecimal(count), 3, RoundingMode.HALF_UP);
}
// 去掉最高分和最低分(已排序,去掉第一个和最后一个)
List<BigDecimal> middleScores = scoreValues.subList(1, count - 1);
// 计算平均分
BigDecimal sum = middleScores.stream()
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(new BigDecimal(middleScores.size()), 3, RoundingMode.HALF_UP);
}
/**
* 安全地将String转换为Long
*/
@@ -221,7 +317,7 @@ public class MartialMiniController extends BladeController {
/**
* 获取选手列表(支持分页)
* - 普通裁判:获取所有选手,标记是否已评分
* - 裁判长:获取已有评分的选手列表
* - 裁判长:获取所有普通裁判都评分完成的选手列表
*/
@GetMapping("/score/athletes")
@Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
@@ -245,35 +341,56 @@ public class MartialMiniController extends BladeController {
List<MartialAthlete> athletes = athleteService.list(athleteQuery);
// 2. 获取所有评分记录
// 2. 获取该场地所有裁判长的judge_id列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
// 3. 获取所有评分记录(排除裁判长的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getIsDeleted, 0);
if (projectId != null) {
scoreQuery.eq(MartialScore::getProjectId, projectId);
}
// 排除裁判长的评分
if (!chiefJudgeIds.isEmpty()) {
scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds);
}
List<MartialScore> allScores = scoreService.list(scoreQuery);
// 按选手ID分组统计评分
java.util.Map<Long, List<MartialScore>> scoresByAthlete = allScores.stream()
.collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId));
// 3. 根据裁判类型处理选手列表
// 4. 获取该场地的应评裁判数量
int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 5. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
if (refereeType == 1) {
// 裁判长:返回已有评分的选手
// 裁判长:返回所有普通裁判都评分完成的选手
final int finalRequiredCount = requiredJudgeCount;
// 只返回评分数量等于普通裁判数量的选手
filteredList = athletes.stream()
.filter(athlete -> {
List<MartialScore> scores = scoresByAthlete.get(athlete.getId());
return scores != null && !scores.isEmpty();
if (scores == null || scores.isEmpty()) {
return false;
}
// 评分数量必须等于该场地的普通裁判数量
// 如果没有配置场地或裁判数量为0则只要有评分就显示
return finalRequiredCount == 0 || scores.size() >= finalRequiredCount;
})
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId))
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList());
} else {
// 普通裁判:返回所有选手,标记是否已评分
filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId))
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList());
}
// 4. 手动分页
// 6. 手动分页
int total = filteredList.size();
int fromIndex = (current - 1) * size;
int toIndex = Math.min(fromIndex + size, total);
@@ -285,13 +402,31 @@ public class MartialMiniController extends BladeController {
pageRecords = filteredList.subList(fromIndex, toIndex);
}
// 5. 构建分页结果
// 7. 构建分页结果
IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> page = new Page<>(current, size, total);
page.setRecords(pageRecords);
return R.data(page);
}
/**
* 获取场地所有裁判长的judge_id列表
*/
private List<Long> getChiefJudgeIds(Long venueId) {
if (venueId == null) {
return new ArrayList<>();
}
LambdaQueryWrapper<MartialJudgeInvite> judgeQuery = new LambdaQueryWrapper<>();
judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId);
judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
judgeQuery.eq(MartialJudgeInvite::getRole, "chief_judge");
List<MartialJudgeInvite> chiefJudges = judgeInviteService.list(judgeQuery);
return chiefJudges.stream()
.map(MartialJudgeInvite::getJudgeId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
/**
* 获取评分详情
*/
@@ -317,26 +452,83 @@ public class MartialMiniController extends BladeController {
*/
@PostMapping("/logout")
@Operation(summary = "退出登录", description = "清除登录状态")
public R logout() {
public R logout(@RequestHeader(value = "Authorization", required = false) String token) {
// 从Redis删除登录缓存
if (token != null && !token.isEmpty()) {
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
bladeRedis.del(cacheKey);
}
return R.success("退出成功");
}
/**
* Token验证
* Token验证从Redis恢复登录状态
*/
@GetMapping("/verify")
@Operation(summary = "Token验证", description = "验证当前token是否有效")
public R verify() {
return R.success("Token有效");
@Operation(summary = "Token验证", description = "验证token并返回登录信息,支持服务重启后恢复登录状态")
public R<MiniLoginVO> verify(@RequestHeader(value = "Authorization", required = false) String token) {
if (token == null || token.isEmpty()) {
return R.fail("Token不能为空");
}
// 从Redis获取登录信息
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
MiniLoginVO loginInfo = bladeRedis.get(cacheKey);
if (loginInfo != null) {
// 刷新缓存过期时间
bladeRedis.setEx(cacheKey, loginInfo, LOGIN_CACHE_EXPIRE);
return R.data(loginInfo);
}
// Redis中没有尝试从数据库恢复
LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>();
inviteQuery.eq(MartialJudgeInvite::getAccessToken, token);
inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
MartialJudgeInvite invite = judgeInviteService.getOne(inviteQuery);
if (invite == null) {
return R.fail("Token无效");
}
if (invite.getTokenExpireTime() != null && invite.getTokenExpireTime().isBefore(LocalDateTime.now())) {
return R.fail("Token已过期");
}
// 重建登录信息
MartialCompetition competition = competitionService.getById(invite.getCompetitionId());
MartialJudge judge = judgeService.getById(invite.getJudgeId());
MtVenue mtVenue = invite.getVenueId() != null ? mtVenueService.getById(invite.getVenueId()) : null;
List<MiniLoginVO.ProjectInfo> projects = parseProjects(invite.getProjects());
MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token);
vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub");
vo.setMatchId(competition != null ? competition.getId() : null);
vo.setMatchName(competition != null ? competition.getCompetitionName() : null);
vo.setMatchTime(competition != null && competition.getCompetitionStartTime() != null ?
competition.getCompetitionStartTime().toString() : "");
vo.setJudgeId(judge != null ? judge.getId() : null);
vo.setJudgeName(judge != null ? judge.getName() : null);
vo.setVenueId(mtVenue != null ? mtVenue.getId() : null);
vo.setVenueName(mtVenue != null ? mtVenue.getVenueName() : null);
vo.setProjects(projects);
// 重新缓存到Redis
bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE);
return R.data(vo);
}
/**
* 转换选手实体为VO
* 新增:只有评分完成时才显示总分
*/
private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO(
MartialAthlete athlete,
List<MartialScore> scores,
Long currentJudgeId) {
Long currentJudgeId,
int requiredJudgeCount) {
org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO();
vo.setAthleteId(athlete.getId());
vo.setName(athlete.getPlayerName());
@@ -345,7 +537,9 @@ public class MartialMiniController extends BladeController {
vo.setTeam(athlete.getTeamName());
vo.setOrderNum(athlete.getOrderNum());
vo.setCompetitionStatus(athlete.getCompetitionStatus());
vo.setTotalScore(athlete.getTotalScore());
// 设置应评分裁判数量
vo.setRequiredJudgeCount(requiredJudgeCount);
// 设置项目名称
if (athlete.getProjectId() != null) {
@@ -356,8 +550,10 @@ public class MartialMiniController extends BladeController {
}
// 设置评分状态
int scoredCount = 0;
if (scores != null && !scores.isEmpty()) {
vo.setScoredJudgeCount(scores.size());
scoredCount = scores.size();
vo.setScoredJudgeCount(scoredCount);
// 查找当前裁判的评分
MartialScore myScore = scores.stream()
@@ -376,6 +572,23 @@ public class MartialMiniController extends BladeController {
vo.setScoredJudgeCount(0);
}
// 判断评分是否完成(所有裁判都已评分)
boolean scoringComplete = false;
if (requiredJudgeCount > 0) {
scoringComplete = scoredCount >= requiredJudgeCount;
} else {
// 如果没有配置裁判数量,只要有评分就算完成
scoringComplete = scoredCount > 0;
}
vo.setScoringComplete(scoringComplete);
// 只有评分完成时才显示总分
if (scoringComplete) {
vo.setTotalScore(athlete.getTotalScore());
} else {
vo.setTotalScore(null);
}
return vo;
}

View File

@@ -62,14 +62,22 @@ public class MiniAthleteListVO implements Serializable {
@JsonProperty("myScore")
private BigDecimal myScore;
@Schema(description = "总分")
@Schema(description = "总分(只有所有裁判评分完成后才显示)")
@JsonProperty("totalScore")
private BigDecimal totalScore;
@Schema(description = "已评分裁判数量(裁判长可见)")
@Schema(description = "已评分裁判数量")
@JsonProperty("scoredJudgeCount")
private Integer scoredJudgeCount;
@Schema(description = "应评分裁判总数")
@JsonProperty("requiredJudgeCount")
private Integer requiredJudgeCount;
@Schema(description = "评分是否完成(所有裁判都已评分)")
@JsonProperty("scoringComplete")
private Boolean scoringComplete = false;
@Schema(description = "比赛状态0-待出场,1-进行中,2-已完成)")
@JsonProperty("competitionStatus")
private Integer competitionStatus;