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 org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import org.springblade.modules.martial.service.*; 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 org.springframework.web.bind.annotation.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -46,9 +50,16 @@ public class MartialMiniController extends BladeController {
private final IMartialJudgeService judgeService; private final IMartialJudgeService judgeService;
private final IMartialCompetitionService competitionService; private final IMartialCompetitionService competitionService;
private final IMartialVenueService venueService; private final IMartialVenueService venueService;
private final IMtVenueService mtVenueService;
private final IMartialProjectService projectService; private final IMartialProjectService projectService;
private final IMartialAthleteService athleteService; private final IMartialAthleteService athleteService;
private final IMartialScoreService scoreService; 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()); invite.setDeviceInfo(dto.getDeviceInfo());
judgeInviteService.updateById(invite); judgeInviteService.updateById(invite);
MartialVenue venue = null; // 从 mt_venue 表获取场地信息
MtVenue mtVenue = null;
if (invite.getVenueId() != null) { if (invite.getVenueId() != null) {
venue = venueService.getById(invite.getVenueId()); mtVenue = mtVenueService.getById(invite.getVenueId());
} }
List<MiniLoginVO.ProjectInfo> projects = parseProjects(invite.getProjects()); List<MiniLoginVO.ProjectInfo> projects = parseProjects(invite.getProjects());
@@ -108,10 +120,14 @@ public class MartialMiniController extends BladeController {
competition.getCompetitionStartTime().toString() : ""); competition.getCompetitionStartTime().toString() : "");
vo.setJudgeId(judge.getId()); vo.setJudgeId(judge.getId());
vo.setJudgeName(judge.getName()); vo.setJudgeName(judge.getName());
vo.setVenueId(venue != null ? venue.getId() : null); vo.setVenueId(mtVenue != null ? mtVenue.getId() : null);
vo.setVenueName(venue != null ? venue.getVenueName() : null); vo.setVenueName(mtVenue != null ? mtVenue.getVenueName() : null);
vo.setProjects(projects); vo.setProjects(projects);
// 将登录信息缓存到Redis服务重启后仍然有效
String cacheKey = MINI_LOGIN_CACHE_PREFIX + token;
bladeRedis.setEx(cacheKey, vo, LOGIN_CACHE_EXPIRE);
return R.data(vo); return R.data(vo);
} }
@@ -158,8 +174,9 @@ public class MartialMiniController extends BladeController {
if (success) { if (success) {
Long athleteId = parseLong(dto.getAthleteId()); Long athleteId = parseLong(dto.getAthleteId());
Long projectId = parseLong(dto.getProjectId()); Long projectId = parseLong(dto.getProjectId());
Long venueId = parseLong(dto.getVenueId());
if (athleteId != null && projectId != null) { 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 { try {
// 查询该选手在该项目的所有评分 // 1. 查询该场地的普通裁判数量
int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 2. 获取裁判长ID列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
// 3. 查询该选手在该项目的所有评分(排除裁判长的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getAthleteId, athleteId); scoreQuery.eq(MartialScore::getAthleteId, athleteId);
scoreQuery.eq(MartialScore::getProjectId, projectId); scoreQuery.eq(MartialScore::getProjectId, projectId);
scoreQuery.eq(MartialScore::getIsDeleted, 0); scoreQuery.eq(MartialScore::getIsDeleted, 0);
// 排除裁判长的所有评分(包括普通评分和修改记录)
if (!chiefJudgeIds.isEmpty()) {
scoreQuery.notIn(MartialScore::getJudgeId, chiefJudgeIds);
}
List<MartialScore> scores = scoreService.list(scoreQuery); List<MartialScore> scores = scoreService.list(scoreQuery);
// 4. 判断是否所有裁判都已评分
if (scores == null || scores.isEmpty()) {
return;
}
if (scores != null && !scores.isEmpty()) { // 如果配置了裁判数量,检查是否评分完成
// 计算平均分 if (requiredJudgeCount > 0 && scores.size() < requiredJudgeCount) {
BigDecimal totalScore = scores.stream() // 未完成评分,清空总分
.map(MartialScore::getScore) MartialAthlete athlete = athleteService.getById(athleteId);
.filter(s -> s != null) if (athlete != null && athlete.getTotalScore() != null) {
.reduce(BigDecimal.ZERO, BigDecimal::add); athlete.setTotalScore(null);
BigDecimal avgScore = totalScore.divide( athleteService.updateById(athlete);
new BigDecimal(scores.size()), }
3, return;
RoundingMode.HALF_UP }
);
// 4. 计算总分(去掉最高最低分取平均)
// 更新选手总分 BigDecimal totalScore = calculateTotalScore(scores);
// 5. 更新选手总分
if (totalScore != null) {
MartialAthlete athlete = athleteService.getById(athleteId); MartialAthlete athlete = athleteService.getById(athleteId);
if (athlete != null) { if (athlete != null) {
athlete.setTotalScore(avgScore); athlete.setTotalScore(totalScore);
athleteService.updateById(athlete); athleteService.updateById(athlete);
} }
} }
@@ -203,6 +240,65 @@ public class MartialMiniController extends BladeController {
e.printStackTrace(); e.printStackTrace();
} }
} }
/**
* 获取场地应评分的裁判数量(普通裁判,不包括裁判长)
* 注意:使用 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 * 安全地将String转换为Long
@@ -221,7 +317,7 @@ public class MartialMiniController extends BladeController {
/** /**
* 获取选手列表(支持分页) * 获取选手列表(支持分页)
* - 普通裁判:获取所有选手,标记是否已评分 * - 普通裁判:获取所有选手,标记是否已评分
* - 裁判长:获取已有评分的选手列表 * - 裁判长:获取所有普通裁判都评分完成的选手列表
*/ */
@GetMapping("/score/athletes") @GetMapping("/score/athletes")
@Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)") @Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
@@ -245,35 +341,56 @@ public class MartialMiniController extends BladeController {
List<MartialAthlete> athletes = athleteService.list(athleteQuery); List<MartialAthlete> athletes = athleteService.list(athleteQuery);
// 2. 获取所有评分记录 // 2. 获取该场地所有裁判长的judge_id列表
List<Long> chiefJudgeIds = getChiefJudgeIds(venueId);
// 3. 获取所有评分记录(排除裁判长的评分)
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getIsDeleted, 0); 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); List<MartialScore> allScores = scoreService.list(scoreQuery);
// 按选手ID分组统计评分 // 按选手ID分组统计评分
java.util.Map<Long, List<MartialScore>> scoresByAthlete = allScores.stream() java.util.Map<Long, List<MartialScore>> scoresByAthlete = allScores.stream()
.collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId)); .collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId));
// 3. 根据裁判类型处理选手列表 // 4. 获取该场地的应评裁判数量
int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 5. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList; List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
if (refereeType == 1) { if (refereeType == 1) {
// 裁判长:返回已有评分的选手 // 裁判长:返回所有普通裁判都评分完成的选手
final int finalRequiredCount = requiredJudgeCount;
// 只返回评分数量等于普通裁判数量的选手
filteredList = athletes.stream() filteredList = athletes.stream()
.filter(athlete -> { .filter(athlete -> {
List<MartialScore> scores = scoresByAthlete.get(athlete.getId()); 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()); .collect(java.util.stream.Collectors.toList());
} else { } else {
// 普通裁判:返回所有选手,标记是否已评分 // 普通裁判:返回所有选手,标记是否已评分
filteredList = athletes.stream() 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()); .collect(java.util.stream.Collectors.toList());
} }
// 4. 手动分页 // 6. 手动分页
int total = filteredList.size(); int total = filteredList.size();
int fromIndex = (current - 1) * size; int fromIndex = (current - 1) * size;
int toIndex = Math.min(fromIndex + size, total); int toIndex = Math.min(fromIndex + size, total);
@@ -285,13 +402,31 @@ public class MartialMiniController extends BladeController {
pageRecords = filteredList.subList(fromIndex, toIndex); pageRecords = filteredList.subList(fromIndex, toIndex);
} }
// 5. 构建分页结果 // 7. 构建分页结果
IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> page = new Page<>(current, size, total); IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> page = new Page<>(current, size, total);
page.setRecords(pageRecords); page.setRecords(pageRecords);
return R.data(page); 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") @PostMapping("/logout")
@Operation(summary = "退出登录", description = "清除登录状态") @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("退出成功"); return R.success("退出成功");
} }
/** /**
* Token验证 * Token验证从Redis恢复登录状态
*/ */
@GetMapping("/verify") @GetMapping("/verify")
@Operation(summary = "Token验证", description = "验证当前token是否有效") @Operation(summary = "Token验证", description = "验证token并返回登录信息,支持服务重启后恢复登录状态")
public R verify() { public R<MiniLoginVO> verify(@RequestHeader(value = "Authorization", required = false) String token) {
return R.success("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 * 转换选手实体为VO
* 新增:只有评分完成时才显示总分
*/ */
private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO( private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO(
MartialAthlete athlete, MartialAthlete athlete,
List<MartialScore> scores, 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(); org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO();
vo.setAthleteId(athlete.getId()); vo.setAthleteId(athlete.getId());
vo.setName(athlete.getPlayerName()); vo.setName(athlete.getPlayerName());
@@ -345,7 +537,9 @@ public class MartialMiniController extends BladeController {
vo.setTeam(athlete.getTeamName()); vo.setTeam(athlete.getTeamName());
vo.setOrderNum(athlete.getOrderNum()); vo.setOrderNum(athlete.getOrderNum());
vo.setCompetitionStatus(athlete.getCompetitionStatus()); vo.setCompetitionStatus(athlete.getCompetitionStatus());
vo.setTotalScore(athlete.getTotalScore());
// 设置应评分裁判数量
vo.setRequiredJudgeCount(requiredJudgeCount);
// 设置项目名称 // 设置项目名称
if (athlete.getProjectId() != null) { if (athlete.getProjectId() != null) {
@@ -356,8 +550,10 @@ public class MartialMiniController extends BladeController {
} }
// 设置评分状态 // 设置评分状态
int scoredCount = 0;
if (scores != null && !scores.isEmpty()) { if (scores != null && !scores.isEmpty()) {
vo.setScoredJudgeCount(scores.size()); scoredCount = scores.size();
vo.setScoredJudgeCount(scoredCount);
// 查找当前裁判的评分 // 查找当前裁判的评分
MartialScore myScore = scores.stream() MartialScore myScore = scores.stream()
@@ -375,6 +571,23 @@ public class MartialMiniController extends BladeController {
vo.setScored(false); vo.setScored(false);
vo.setScoredJudgeCount(0); 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; return vo;
} }

View File

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