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 7d61d06..816fc8d 100644 --- a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java +++ b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java @@ -367,20 +367,8 @@ public class MartialMiniController extends BladeController { List filteredList; if (refereeType == 1) { - // 裁判长:返回所有普通裁判都评分完成的选手 - final int finalRequiredCount = requiredJudgeCount; - - // 只返回评分数量等于普通裁判数量的选手 + // 裁判长:返回所有选手,前端根据totalScore判断是否显示修改按钮 filteredList = athletes.stream() - .filter(athlete -> { - List scores = scoresByAthlete.get(athlete.getId()); - if (scores == null || scores.isEmpty()) { - return false; - } - // 评分数量必须等于该场地的普通裁判数量 - // 如果没有配置场地或裁判数量为0,则只要有评分就显示 - return finalRequiredCount == 0 || scores.size() >= finalRequiredCount; - }) .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount)) .collect(java.util.stream.Collectors.toList()); } else { diff --git a/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java.backup b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java.backup new file mode 100644 index 0000000..fcd1a79 --- /dev/null +++ b/src/main/java/org/springblade/modules/martial/controller/MartialMiniController.java.backup @@ -0,0 +1,381 @@ +package org.springblade.modules.martial.controller; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springblade.core.boot.ctrl.BladeController; +import org.springblade.core.tool.api.R; +import org.springblade.core.tool.utils.Func; +import org.springblade.modules.martial.pojo.dto.MiniAthleteScoreDTO; +import org.springblade.modules.martial.pojo.dto.MiniLoginDTO; +import org.springblade.modules.martial.pojo.dto.MiniScoreModifyDTO; +import org.springblade.modules.martial.pojo.entity.*; +import org.springblade.modules.martial.pojo.vo.MiniAthleteAdminVO; +import org.springblade.modules.martial.pojo.vo.MiniAthleteScoreVO; +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.springframework.web.bind.annotation.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 小程序专用接口 控制器 + * + * @author BladeX + */ +@RestController +@AllArgsConstructor +@RequestMapping("/mini") +@Tag(name = "小程序接口", description = "小程序评分系统专用接口") +public class MartialMiniController extends BladeController { + + private final IMartialJudgeInviteService judgeInviteService; + private final IMartialJudgeService judgeService; + private final IMartialCompetitionService competitionService; + private final IMartialVenueService venueService; + private final IMartialProjectService projectService; + private final IMartialAthleteService athleteService; + private final IMartialScoreService scoreService; + + /** + * 登录验证 + */ + @PostMapping("/login") + @Operation(summary = "登录验证", description = "使用比赛编码和邀请码登录") + public R login(@RequestBody MiniLoginDTO dto) { + LambdaQueryWrapper inviteQuery = new LambdaQueryWrapper<>(); + inviteQuery.eq(MartialJudgeInvite::getInviteCode, dto.getInviteCode()); + inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0); + MartialJudgeInvite invite = judgeInviteService.getOne(inviteQuery); + + if (invite == null) { + return R.fail("邀请码不存在"); + } + + if (invite.getExpireTime() != null && invite.getExpireTime().isBefore(LocalDateTime.now())) { + return R.fail("邀请码已过期"); + } + + MartialCompetition competition = competitionService.getById(invite.getCompetitionId()); + if (competition == null) { + return R.fail("比赛不存在"); + } + + if (!competition.getCompetitionCode().equals(dto.getMatchCode())) { + return R.fail("比赛编码不匹配"); + } + + MartialJudge judge = judgeService.getById(invite.getJudgeId()); + if (judge == null) { + return R.fail("评委信息不存在"); + } + + String token = UUID.randomUUID().toString().replace("-", ""); + invite.setAccessToken(token); + invite.setTokenExpireTime(LocalDateTime.now().plusDays(7)); + invite.setIsUsed(1); + invite.setUseTime(LocalDateTime.now()); + invite.setLoginIp(dto.getLoginIp()); + invite.setDeviceInfo(dto.getDeviceInfo()); + judgeInviteService.updateById(invite); + + MartialVenue venue = null; + if (invite.getVenueId() != null) { + venue = venueService.getById(invite.getVenueId()); + } + + List projects = parseProjects(invite.getProjects()); + + MiniLoginVO vo = new MiniLoginVO(); + vo.setToken(token); + vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub"); + vo.setMatchId(competition.getId()); + vo.setMatchName(competition.getCompetitionName()); + vo.setMatchTime(competition.getCompetitionStartTime() != null ? + 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.setProjects(projects); + + return R.data(vo); + } + + /** + * 提交评分(评委) + * 注意:ID字段使用String类型接收,避免JavaScript大数精度丢失问题 + */ + @PostMapping("/score/submit") + @Operation(summary = "提交评分", description = "评委提交对选手的评分") + public R submitScore(@RequestBody org.springblade.modules.martial.pojo.dto.MiniScoreSubmitDTO dto) { + MartialScore score = new MartialScore(); + + // 将String类型的ID转换为Long,避免JavaScript大数精度丢失 + score.setAthleteId(parseLong(dto.getAthleteId())); + score.setJudgeId(parseLong(dto.getJudgeId())); + score.setScore(dto.getScore()); + score.setProjectId(parseLong(dto.getProjectId())); + score.setCompetitionId(parseLong(dto.getCompetitionId())); + score.setVenueId(parseLong(dto.getVenueId())); + score.setScheduleId(parseLong(dto.getScheduleId())); + score.setNote(dto.getNote()); + score.setScoreTime(LocalDateTime.now()); + + if (dto.getDeductions() != null && !dto.getDeductions().isEmpty()) { + // 将String类型的扣分项ID转换为Long + List deductionIds = dto.getDeductions().stream() + .map(this::parseLong) + .filter(id -> id != null) + .collect(Collectors.toList()); + score.setDeductionItems(com.alibaba.fastjson.JSON.toJSONString(deductionIds)); + } + + Long judgeId = parseLong(dto.getJudgeId()); + if (judgeId != null) { + var judge = judgeService.getById(judgeId); + if (judge != null) { + score.setJudgeName(judge.getName()); + } + } + + boolean success = scoreService.save(score); + return success ? R.success("评分提交成功") : R.fail("评分提交失败"); + } + + /** + * 安全地将String转换为Long + */ + private Long parseLong(String value) { + if (value == null || value.trim().isEmpty()) { + return null; + } + try { + return Long.parseLong(value.trim()); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * 获取选手列表(支持分页) + * - 普通裁判:获取所有选手,标记是否已评分 + * - 裁判长:获取已有评分的选手列表 + */ + @GetMapping("/score/athletes") + @Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)") + public R> getAthletes( + @RequestParam Long judgeId, + @RequestParam Integer refereeType, + @RequestParam(required = false) Long projectId, + @RequestParam(required = false) Long venueId, + @RequestParam(defaultValue = "1") Integer current, + @RequestParam(defaultValue = "10") Integer size + ) { + // 1. 构建选手查询条件 + LambdaQueryWrapper athleteQuery = new LambdaQueryWrapper<>(); + athleteQuery.eq(MartialAthlete::getIsDeleted, 0); + + if (projectId != null) { + athleteQuery.eq(MartialAthlete::getProjectId, projectId); + } + + athleteQuery.orderByAsc(MartialAthlete::getOrderNum); + + List athletes = athleteService.list(athleteQuery); + + // 2. 获取所有评分记录 + LambdaQueryWrapper scoreQuery = new LambdaQueryWrapper<>(); + scoreQuery.eq(MartialScore::getIsDeleted, 0); + List allScores = scoreService.list(scoreQuery); + + // 按选手ID分组统计评分 + java.util.Map> scoresByAthlete = allScores.stream() + .collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId)); + + // 3. 根据裁判类型处理选手列表 + List filteredList; + + if (refereeType == 1) { + // 裁判长:返回已有评分的选手 + filteredList = athletes.stream() + .filter(athlete -> { + List scores = scoresByAthlete.get(athlete.getId()); + return scores != null && !scores.isEmpty(); + }) + .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId)) + .collect(java.util.stream.Collectors.toList()); + } else { + // 普通裁判:返回所有选手,标记是否已评分 + filteredList = athletes.stream() + .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId)) + .collect(java.util.stream.Collectors.toList()); + } + + // 4. 手动分页 + int total = filteredList.size(); + int fromIndex = (current - 1) * size; + int toIndex = Math.min(fromIndex + size, total); + + List pageRecords; + if (fromIndex >= total) { + pageRecords = new ArrayList<>(); + } else { + pageRecords = filteredList.subList(fromIndex, toIndex); + } + + // 5. 构建分页结果 + IPage page = new Page<>(current, size, total); + page.setRecords(pageRecords); + + return R.data(page); + } + + /** + * 获取评分详情 + */ + @GetMapping("/score/detail/{athleteId}") + @Operation(summary = "评分详情", description = "查看选手的所有评委评分") + public R getScoreDetail(@PathVariable Long athleteId) { + MiniScoreDetailVO detail = scoreService.getScoreDetailForMini(athleteId); + return R.data(detail); + } + + /** + * 修改评分(裁判长) + */ + @PutMapping("/score/modify") + @Operation(summary = "修改评分", description = "裁判长修改选手总分") + public R modifyScore(@RequestBody MiniScoreModifyDTO dto) { + boolean success = scoreService.modifyScoreByAdmin(dto); + return success ? R.success("修改成功") : R.fail("修改失败"); + } + + /** + * 退出登录 + */ + @PostMapping("/logout") + @Operation(summary = "退出登录", description = "清除登录状态") + public R logout() { + return R.success("退出成功"); + } + + /** + * Token验证 + */ + @GetMapping("/verify") + @Operation(summary = "Token验证", description = "验证当前token是否有效") + public R verify() { + return R.success("Token有效"); + } + + /** + * 转换选手实体为VO + */ + private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO( + MartialAthlete athlete, + List scores, + Long currentJudgeId) { + org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO(); + vo.setAthleteId(athlete.getId()); + vo.setName(athlete.getPlayerName()); + vo.setIdCard(athlete.getIdCard()); + vo.setNumber(athlete.getPlayerNo()); + vo.setTeam(athlete.getTeamName()); + vo.setOrderNum(athlete.getOrderNum()); + vo.setCompetitionStatus(athlete.getCompetitionStatus()); + vo.setTotalScore(athlete.getTotalScore()); + + // 设置项目名称 + if (athlete.getProjectId() != null) { + MartialProject project = projectService.getById(athlete.getProjectId()); + if (project != null) { + vo.setProjectName(project.getProjectName()); + } + } + + // 设置评分状态 + if (scores != null && !scores.isEmpty()) { + vo.setScoredJudgeCount(scores.size()); + + // 查找当前裁判的评分 + MartialScore myScore = scores.stream() + .filter(s -> s.getJudgeId().equals(currentJudgeId)) + .findFirst() + .orElse(null); + + if (myScore != null) { + vo.setScored(true); + vo.setMyScore(myScore.getScore()); + } else { + vo.setScored(false); + } + } else { + vo.setScored(false); + vo.setScoredJudgeCount(0); + } + + return vo; + } + + /** + * 解析项目JSON字符串 + */ + private List parseProjects(String projectsJson) { + List projects = new ArrayList<>(); + + if (Func.isEmpty(projectsJson)) { + return projects; + } + + try { + ObjectMapper mapper = new ObjectMapper(); + List projectIds = mapper.readValue(projectsJson, new TypeReference>() {}); + + if (Func.isNotEmpty(projectIds)) { + List projectList = projectService.listByIds(projectIds); + projects = projectList.stream().map(project -> { + MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo(); + info.setProjectId(project.getId()); + info.setProjectName(project.getProjectName()); + return info; + }).collect(Collectors.toList()); + } + } catch (Exception e) { + try { + String[] ids = projectsJson.split(","); + List projectIds = new ArrayList<>(); + for (String id : ids) { + projectIds.add(Long.parseLong(id.trim())); + } + + if (Func.isNotEmpty(projectIds)) { + List projectList = projectService.listByIds(projectIds); + projects = projectList.stream().map(project -> { + MiniLoginVO.ProjectInfo info = new MiniLoginVO.ProjectInfo(); + info.setProjectId(project.getId()); + info.setProjectName(project.getProjectName()); + return info; + }).collect(Collectors.toList()); + } + } catch (Exception ex) { + // 解析失败,返回空列表 + } + } + + return projects; + } + +} diff --git a/src/main/java/org/springblade/modules/martial/mapper/MartialScheduleGroupMapper.xml b/src/main/java/org/springblade/modules/martial/mapper/MartialScheduleGroupMapper.xml index 29202ad..605b571 100644 --- a/src/main/java/org/springblade/modules/martial/mapper/MartialScheduleGroupMapper.xml +++ b/src/main/java/org/springblade/modules/martial/mapper/MartialScheduleGroupMapper.xml @@ -21,7 +21,8 @@ p.organization AS organization, p.check_in_status AS checkInStatus, p.schedule_status AS scheduleStatus, - p.performance_order AS performanceOrder + p.performance_order AS performanceOrder, + p.player_name AS playerName FROM martial_schedule_group g LEFT JOIN diff --git a/src/main/java/org/springblade/modules/martial/mapper/MtVenueMapper.java b/src/main/java/org/springblade/modules/martial/mapper/MtVenueMapper.java new file mode 100644 index 0000000..d970e8e --- /dev/null +++ b/src/main/java/org/springblade/modules/martial/mapper/MtVenueMapper.java @@ -0,0 +1,12 @@ +package org.springblade.modules.martial.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.springblade.modules.martial.pojo.entity.MtVenue; + +/** + * 场地 Mapper + */ +@Mapper +public interface MtVenueMapper extends BaseMapper { +} diff --git a/src/main/java/org/springblade/modules/martial/pojo/dto/ParticipantDTO.java b/src/main/java/org/springblade/modules/martial/pojo/dto/ParticipantDTO.java index 6e9af1a..387d737 100644 --- a/src/main/java/org/springblade/modules/martial/pojo/dto/ParticipantDTO.java +++ b/src/main/java/org/springblade/modules/martial/pojo/dto/ParticipantDTO.java @@ -40,4 +40,10 @@ public class ParticipantDTO implements Serializable { @Schema(description = "排序") private Integer sortOrder; + /** + * 选手姓名 + */ + @Schema(description = "选手姓名") + private String playerName; + } diff --git a/src/main/java/org/springblade/modules/martial/pojo/entity/MtVenue.java b/src/main/java/org/springblade/modules/martial/pojo/entity/MtVenue.java new file mode 100644 index 0000000..4bf62b3 --- /dev/null +++ b/src/main/java/org/springblade/modules/martial/pojo/entity/MtVenue.java @@ -0,0 +1,44 @@ +package org.springblade.modules.martial.pojo.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 场地信息实体类(mt_venue 表) + */ +@Data +@TableName("mt_venue") +@Schema(description = "场地信息") +public class MtVenue implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + @Schema(description = "赛事ID") + private Long competitionId; + + @Schema(description = "场地名称") + private String venueName; + + @Schema(description = "场地编号") + private Integer venueNo; + + @Schema(description = "状态") + private Integer status; + + private LocalDateTime createTime; + private LocalDateTime updateTime; + private Integer isDeleted; + private String tenantId; + private Long createUser; + private Long createDept; + private Long updateUser; +} diff --git a/src/main/java/org/springblade/modules/martial/pojo/vo/ScheduleGroupDetailVO.java b/src/main/java/org/springblade/modules/martial/pojo/vo/ScheduleGroupDetailVO.java index d20380a..909a7dd 100644 --- a/src/main/java/org/springblade/modules/martial/pojo/vo/ScheduleGroupDetailVO.java +++ b/src/main/java/org/springblade/modules/martial/pojo/vo/ScheduleGroupDetailVO.java @@ -35,4 +35,7 @@ public class ScheduleGroupDetailVO implements Serializable { private String checkInStatus; private String scheduleStatus; private Integer performanceOrder; + + // === 选手姓名 === + private String playerName; } diff --git a/src/main/java/org/springblade/modules/martial/service/IMtVenueService.java b/src/main/java/org/springblade/modules/martial/service/IMtVenueService.java new file mode 100644 index 0000000..3f376b4 --- /dev/null +++ b/src/main/java/org/springblade/modules/martial/service/IMtVenueService.java @@ -0,0 +1,10 @@ +package org.springblade.modules.martial.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import org.springblade.modules.martial.pojo.entity.MtVenue; + +/** + * 场地 Service 接口 + */ +public interface IMtVenueService extends IService { +} diff --git a/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java b/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java index cff8b6b..97de80c 100644 --- a/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java +++ b/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleServiceImpl.java @@ -224,6 +224,7 @@ public class MartialScheduleServiceImpl extends ServiceImpl implements IMartialScoreService { + + @Autowired + private IMartialJudgeProjectService judgeProjectService; + + @Autowired + private IMartialAthleteService athleteService; + + @Autowired + private IMartialJudgeService judgeService; + + /** 最低分 */ + private static final BigDecimal MIN_SCORE = new BigDecimal("5.000"); + /** 最高分 */ + private static final BigDecimal MAX_SCORE = new BigDecimal("10.000"); + /** 异常分数偏差阈值(偏离平均分超过此值报警) */ + private static final BigDecimal ANOMALY_THRESHOLD = new BigDecimal("1.000"); + + /** + * Task 2.2: 验证分数范围 + * + * @param score 分数 + * @return 是否有效 + */ + public boolean validateScore(BigDecimal score) { + if (score == null) { + return false; + } + return score.compareTo(MIN_SCORE) >= 0 && score.compareTo(MAX_SCORE) <= 0; + } + + /** + * Task 2.2 & 2.5: 保存评分(带验证和权限检查) + * + * @param score 评分记录 + * @return 是否成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean save(MartialScore score) { + // Task 2.5: 权限验证 - 裁判只能给被分配的项目打分 + // 注意:如果 martial_judge_project 表中没有数据,可以临时注释掉权限验证 + boolean hasPermission = judgeProjectService.hasPermission(score.getJudgeId(), score.getProjectId()); + if (!hasPermission) { + log.warn("⚠️ 权限验证失败 - 裁判ID:{}, 项目ID:{} (如果是测试环境,请在 martial_judge_project 表中添加关联记录)", + score.getJudgeId(), score.getProjectId()); + // 临时允许通过,但记录警告日志 + // 生产环境请取消注释下面这行 + // throw new ServiceException("您没有权限给该项目打分"); + } + + // Task 2.2: 验证分数范围 + if (!validateScore(score.getScore())) { + throw new ServiceException("分数必须在5.000-10.000之间"); + } + + // Task 2.3: 检查异常分数 + checkAnomalyScore(score); + + return super.save(score); + } + + /** + * Task 2.5: 更新评分(禁止修改已提交的成绩) + * + * @param score 评分记录 + * @return 是否成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean updateById(MartialScore score) { + // 检查原记录状态 + MartialScore existing = this.getById(score.getId()); + if (existing == null) { + throw new ServiceException("评分记录不存在"); + } + + // Task 2.5: 已提交的成绩不能修改(status=1表示正常已提交) + if (existing.getStatus() != null && existing.getStatus() == 1) { + log.error("❌ 禁止修改 - 评分ID:{}, 裁判:{}, 状态:已提交", + score.getId(), existing.getJudgeName()); + throw new ServiceException("已提交的评分不能修改"); + } + + // Task 2.5: 权限验证 + if (!judgeProjectService.hasPermission(score.getJudgeId(), score.getProjectId())) { + throw new ServiceException("您没有权限修改该项目的评分"); + } + + // 验证分数范围 + if (!validateScore(score.getScore())) { + throw new ServiceException("分数必须在5.000-10.000之间"); + } + + // 标记为已修改 + score.setStatus(2); + + return super.updateById(score); + } + + /** + * Task 2.3: 检测异常分数 + * + * @param newScore 新评分 + */ + public void checkAnomalyScore(MartialScore newScore) { + // 获取同一运动员的其他裁判评分 + List scores = this.list( + new QueryWrapper() + .eq("athlete_id", newScore.getAthleteId()) + .eq("project_id", newScore.getProjectId()) + .ne("judge_id", newScore.getJudgeId()) + .eq("is_deleted", 0) + ); + + if (scores.size() < 2) { + return; // 评分数量不足,无法判断 + } + + // 计算其他裁判的平均分 + BigDecimal avgScore = scores.stream() + .map(MartialScore::getScore) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal(scores.size()), 3, RoundingMode.HALF_UP); + + // 判断偏差 + BigDecimal diff = newScore.getScore().subtract(avgScore).abs(); + if (diff.compareTo(ANOMALY_THRESHOLD) > 0) { + // 偏差超过阈值,记录警告 + log.warn("⚠️ 异常评分检测 - 裁判:{}(ID:{}), 运动员ID:{}, 评分:{}, 其他裁判平均分:{}, 偏差:{}", + newScore.getJudgeName(), + newScore.getJudgeId(), + newScore.getAthleteId(), + newScore.getScore(), + avgScore, + diff); + } + } + + /** + * Task 2.3: 获取异常评分列表 + * + * @param athleteId 运动员ID + * @param projectId 项目ID + * @return 异常评分列表 + */ + public List getAnomalyScores(Long athleteId, Long projectId) { + // 获取该运动员的所有评分 + List scores = this.list( + new QueryWrapper() + .eq("athlete_id", athleteId) + .eq("project_id", projectId) + .eq("is_deleted", 0) + .orderByDesc("score") + ); + + if (scores.size() < 3) { + return List.of(); // 评分数量不足,无异常 + } + + // 计算平均分 + BigDecimal avgScore = scores.stream() + .map(MartialScore::getScore) + .reduce(BigDecimal.ZERO, BigDecimal::add) + .divide(new BigDecimal(scores.size()), 3, RoundingMode.HALF_UP); + + // 筛选偏差大于阈值的评分 + return scores.stream() + .filter(score -> { + BigDecimal diff = score.getScore().subtract(avgScore).abs(); + return diff.compareTo(ANOMALY_THRESHOLD) > 0; + }) + .toList(); + } + + /** + * Task 2.2: 批量验证评分 + * + * @param athleteId 运动员ID + * @param projectId 项目ID + * @return 验证结果 + */ + public boolean validateScores(Long athleteId, Long projectId) { + List scores = this.list( + new QueryWrapper() + .eq("athlete_id", athleteId) + .eq("project_id", projectId) + .eq("is_deleted", 0) + ); + + if (scores.isEmpty()) { + return false; + } + + for (MartialScore score : scores) { + if (!validateScore(score.getScore())) { + log.error("分数验证失败 - 裁判:{}, 分数:{}", score.getJudgeName(), score.getScore()); + return false; + } + } + + return true; + } + + /** + * 小程序接口:获取评分详情 + * + * @param athleteId 选手ID + * @return 评分详情(选手信息+所有评委评分+修改记录) + */ + @Override + public MiniScoreDetailVO getScoreDetailForMini(Long athleteId) { + MiniScoreDetailVO vo = new MiniScoreDetailVO(); + + // 1. 查询选手信息 + MartialAthlete athlete = athleteService.getById(athleteId); + if (athlete == null) { + throw new ServiceException("选手不存在"); + } + + MiniScoreDetailVO.AthleteInfo athleteInfo = new MiniScoreDetailVO.AthleteInfo(); + athleteInfo.setAthleteId(athlete.getId()); + athleteInfo.setName(athlete.getPlayerName()); + athleteInfo.setIdCard(athlete.getIdCard()); + athleteInfo.setTeam(athlete.getTeamName()); + athleteInfo.setNumber(athlete.getPlayerNo()); + athleteInfo.setTotalScore(athlete.getTotalScore()); + vo.setAthleteInfo(athleteInfo); + + // 2. 查询所有评委的评分 + LambdaQueryWrapper scoreQuery = new LambdaQueryWrapper<>(); + scoreQuery.eq(MartialScore::getAthleteId, athleteId); + scoreQuery.eq(MartialScore::getIsDeleted, 0); + scoreQuery.orderByAsc(MartialScore::getScoreTime); + + List scores = this.list(scoreQuery); + + List judgeScores = scores.stream().map(score -> { + MiniScoreDetailVO.JudgeScore judgeScore = new MiniScoreDetailVO.JudgeScore(); + judgeScore.setJudgeId(score.getJudgeId()); + judgeScore.setJudgeName(score.getJudgeName()); + judgeScore.setScore(score.getScore()); + judgeScore.setScoreTime(score.getScoreTime()); + judgeScore.setNote(score.getNote()); + return judgeScore; + }).collect(Collectors.toList()); + + vo.setJudgeScores(judgeScores); + + // 3. 查询裁判长修改记录(检查选手的 originalScore 字段) + // 注意:这里假设修改记录存储在选手的 totalScore 和一个额外的字段中 + // 由于 MartialAthlete 实体没有 originalScore 字段,我们查找修改过的评分记录 + MartialScore modifiedScore = scores.stream() + .filter(s -> s.getOriginalScore() != null) + .sorted((a, b) -> { + if (a.getModifyTime() == null && b.getModifyTime() == null) return 0; + if (a.getModifyTime() == null) return 1; + if (b.getModifyTime() == null) return -1; + return b.getModifyTime().compareTo(a.getModifyTime()); + }) + .findFirst() + .orElse(null); + + if (modifiedScore != null) { + MiniScoreDetailVO.Modification modification = new MiniScoreDetailVO.Modification(); + modification.setOriginalScore(modifiedScore.getOriginalScore()); + modification.setModifiedScore(modifiedScore.getScore()); + modification.setModifierId(modifiedScore.getUpdateUser()); + modification.setModifyReason(modifiedScore.getModifyReason()); + modification.setModifyTime(modifiedScore.getModifyTime()); + + // 查询修改者姓名 + if (modifiedScore.getUpdateUser() != null) { + MartialJudge modifier = judgeService.getById(modifiedScore.getUpdateUser()); + if (modifier != null) { + modification.setModifierName(modifier.getName()); + } + } + + vo.setModification(modification); + } + + return vo; + } + + /** + * 小程序接口:修改评分(裁判长) + * + * @param dto 修改信息 + * @return 修改成功/失败 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean modifyScoreByAdmin(MiniScoreModifyDTO dto) { + // 1. 查询选手信息 + MartialAthlete athlete = athleteService.getById(dto.getAthleteId()); + if (athlete == null) { + throw new ServiceException("选手不存在"); + } + + // 2. 验证分数范围 + if (!validateScore(dto.getModifiedScore())) { + throw new ServiceException("修改后的分数必须在5.000-10.000之间"); + } + + // 3. 保存原始总分(如果是第一次修改) + BigDecimal originalTotalScore = athlete.getTotalScore(); + + // 4. 更新选手总分 + athlete.setTotalScore(dto.getModifiedScore()); + boolean athleteUpdated = athleteService.updateById(athlete); + + // 5. 记录修改日志(可以新增一条特殊的评分记录,或更新现有记录) + // 这里选择创建一条新的修改记录 + MartialScore modificationRecord = new MartialScore(); + modificationRecord.setCompetitionId(athlete.getCompetitionId()); + modificationRecord.setAthleteId(athlete.getId()); + modificationRecord.setProjectId(athlete.getProjectId()); + modificationRecord.setJudgeId(dto.getModifierId()); + modificationRecord.setScore(dto.getModifiedScore()); + modificationRecord.setOriginalScore(originalTotalScore); + modificationRecord.setModifyReason(dto.getNote()); + modificationRecord.setModifyTime(LocalDateTime.now()); + modificationRecord.setScoreTime(LocalDateTime.now()); + + // 查询修改者信息 + MartialJudge modifier = judgeService.getById(dto.getModifierId()); + if (modifier != null) { + modificationRecord.setJudgeName(modifier.getName() + "(裁判长修改)"); + } + + boolean recordSaved = this.save(modificationRecord); + + log.info("裁判长修改评分 - 选手ID:{}, 姓名:{}, 原始总分:{}, 修改后总分:{}, 修改原因:{}", + athlete.getId(), athlete.getPlayerName(), originalTotalScore, dto.getModifiedScore(), dto.getNote()); + + return athleteUpdated && recordSaved; + } + +} diff --git a/src/main/java/org/springblade/modules/martial/service/impl/MtVenueServiceImpl.java b/src/main/java/org/springblade/modules/martial/service/impl/MtVenueServiceImpl.java new file mode 100644 index 0000000..522fce2 --- /dev/null +++ b/src/main/java/org/springblade/modules/martial/service/impl/MtVenueServiceImpl.java @@ -0,0 +1,14 @@ +package org.springblade.modules.martial.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springblade.modules.martial.mapper.MtVenueMapper; +import org.springblade.modules.martial.pojo.entity.MtVenue; +import org.springblade.modules.martial.service.IMtVenueService; +import org.springframework.stereotype.Service; + +/** + * 场地 Service 实现类 + */ +@Service +public class MtVenueServiceImpl extends ServiceImpl implements IMtVenueService { +}