diff --git a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java index 6056d40..7d61d06 100644 --- a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java +++ b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java @@ -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 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 chiefJudgeIds = getChiefJudgeIds(venueId); + + // 3. 查询该选手在该项目的所有评分(排除裁判长的评分) LambdaQueryWrapper 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 scores = scoreService.list(scoreQuery); + + // 4. 判断是否所有裁判都已评分 + if (scores == null || scores.isEmpty()) { + return; + } - 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 - ); - - // 更新选手总分 + // 如果配置了裁判数量,检查是否评分完成 + 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); } } @@ -203,6 +240,65 @@ public class MartialMiniController extends BladeController { e.printStackTrace(); } } + + /** + * 获取场地应评分的裁判数量(普通裁判,不包括裁判长) + * 注意:使用 DISTINCT judge_id 来避免重复计数 + */ + private int getRequiredJudgeCount(Long venueId) { + if (venueId == null) { + return 0; + } + LambdaQueryWrapper judgeQuery = new LambdaQueryWrapper<>(); + judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId); + judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0); + judgeQuery.ne(MartialJudgeInvite::getRole, "chief_judge"); // 排除裁判长 + List judges = judgeInviteService.list(judgeQuery); + // 使用 distinct judge_id 来计算不重复的裁判数量 + return (int) judges.stream() + .map(MartialJudgeInvite::getJudgeId) + .filter(Objects::nonNull) + .distinct() + .count(); + } + + /** + * 计算总分 + * 算法:去掉一个最高分和一个最低分,取剩余分数的平均值 + * 特殊情况:裁判数量<3时,直接取平均分 + */ + private BigDecimal calculateTotalScore(List scores) { + if (scores == null || scores.isEmpty()) { + return null; + } + + // 提取所有分数并排序 + List 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 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 athletes = athleteService.list(athleteQuery); - // 2. 获取所有评分记录 + // 2. 获取该场地所有裁判长的judge_id列表 + List chiefJudgeIds = getChiefJudgeIds(venueId); + + // 3. 获取所有评分记录(排除裁判长的评分) LambdaQueryWrapper 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 allScores = scoreService.list(scoreQuery); // 按选手ID分组统计评分 java.util.Map> scoresByAthlete = allScores.stream() .collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId)); - // 3. 根据裁判类型处理选手列表 + // 4. 获取该场地的应评裁判数量 + int requiredJudgeCount = getRequiredJudgeCount(venueId); + + // 5. 根据裁判类型处理选手列表 List filteredList; if (refereeType == 1) { - // 裁判长:返回已有评分的选手 + // 裁判长:返回所有普通裁判都评分完成的选手 + final int finalRequiredCount = requiredJudgeCount; + + // 只返回评分数量等于普通裁判数量的选手 filteredList = athletes.stream() .filter(athlete -> { List 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 page = new Page<>(current, size, total); page.setRecords(pageRecords); return R.data(page); } + /** + * 获取场地所有裁判长的judge_id列表 + */ + private List getChiefJudgeIds(Long venueId) { + if (venueId == null) { + return new ArrayList<>(); + } + LambdaQueryWrapper judgeQuery = new LambdaQueryWrapper<>(); + judgeQuery.eq(MartialJudgeInvite::getVenueId, venueId); + judgeQuery.eq(MartialJudgeInvite::getIsDeleted, 0); + judgeQuery.eq(MartialJudgeInvite::getRole, "chief_judge"); + List 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 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 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 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 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() @@ -375,6 +571,23 @@ public class MartialMiniController extends BladeController { vo.setScored(false); 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; } diff --git a/src/main/java/org/springblade/modules/martial/pojo/vo/MiniAthleteListVO.java b/src/main/java/org/springblade/modules/martial/pojo/vo/MiniAthleteListVO.java index bf84b24..95cc266 100644 --- a/src/main/java/org/springblade/modules/martial/pojo/vo/MiniAthleteListVO.java +++ b/src/main/java/org/springblade/modules/martial/pojo/vo/MiniAthleteListVO.java @@ -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;