This commit is contained in:
2025-12-26 10:31:41 +08:00
45 changed files with 10739 additions and 1665 deletions

View File

@@ -0,0 +1,127 @@
package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
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.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionAttachment;
import org.springblade.modules.martial.service.IMartialCompetitionAttachmentService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 赛事附件 控制器
*
* @author BladeX
*/
@RestController
@AllArgsConstructor
@RequestMapping("/martial/competition/attachment")
@Tag(name = "赛事附件管理", description = "赛事附件管理接口")
public class MartialCompetitionAttachmentController extends BladeController {
private final IMartialCompetitionAttachmentService attachmentService;
/**
* 详情
*/
@GetMapping("/detail")
@Operation(summary = "详情", description = "传入ID")
public R<MartialCompetitionAttachment> detail(@RequestParam Long id) {
MartialCompetitionAttachment detail = attachmentService.getById(id);
return R.data(detail);
}
/**
* 分页列表
*/
@GetMapping("/list")
@Operation(summary = "分页列表", description = "分页查询")
public R<IPage<MartialCompetitionAttachment>> list(MartialCompetitionAttachment attachment, Query query) {
IPage<MartialCompetitionAttachment> pages = attachmentService.page(Condition.getPage(query), Condition.getQueryWrapper(attachment));
return R.data(pages);
}
/**
* 根据赛事ID和类型获取附件列表
*/
@GetMapping("/getByType")
@Operation(summary = "根据赛事ID和类型获取附件列表", description = "传入赛事ID和附件类型")
public R<List<MartialCompetitionAttachment>> getByType(
@RequestParam Long competitionId,
@RequestParam String attachmentType) {
List<MartialCompetitionAttachment> list = attachmentService.getByCompetitionIdAndType(competitionId, attachmentType);
return R.data(list);
}
/**
* 根据赛事ID获取所有附件
*/
@GetMapping("/getByCompetition")
@Operation(summary = "根据赛事ID获取所有附件", description = "传入赛事ID")
public R<List<MartialCompetitionAttachment>> getByCompetition(@RequestParam Long competitionId) {
List<MartialCompetitionAttachment> list = attachmentService.getByCompetitionId(competitionId);
return R.data(list);
}
/**
* 新增或修改
*/
@PostMapping("/submit")
@Operation(summary = "新增或修改", description = "传入实体")
public R submit(@RequestBody MartialCompetitionAttachment attachment) {
// 设置默认状态为启用
if (attachment.getStatus() == null) {
attachment.setStatus(1);
}
// 设置默认排序
if (attachment.getOrderNum() == null) {
attachment.setOrderNum(0);
}
return R.status(attachmentService.saveOrUpdate(attachment));
}
/**
* 批量保存附件
*/
@PostMapping("/batchSubmit")
@Operation(summary = "批量保存附件", description = "传入附件列表")
public R batchSubmit(@RequestBody List<MartialCompetitionAttachment> attachments) {
for (MartialCompetitionAttachment attachment : attachments) {
if (attachment.getStatus() == null) {
attachment.setStatus(1);
}
if (attachment.getOrderNum() == null) {
attachment.setOrderNum(0);
}
}
return R.status(attachmentService.saveOrUpdateBatch(attachments));
}
/**
* 删除
*/
@PostMapping("/remove")
@Operation(summary = "删除", description = "传入ID")
public R remove(@RequestParam String ids) {
return R.status(attachmentService.removeByIds(Func.toLongList(ids)));
}
/**
* 删除赛事的指定类型附件
*/
@PostMapping("/removeByType")
@Operation(summary = "删除赛事的指定类型附件", description = "传入赛事ID和附件类型")
public R removeByType(
@RequestParam Long competitionId,
@RequestParam String attachmentType) {
return R.status(attachmentService.removeByCompetitionIdAndType(competitionId, attachmentType));
}
}

View File

@@ -1,6 +1,9 @@
package org.springblade.modules.martial.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
@@ -9,10 +12,15 @@ import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.Func;
import org.springblade.modules.martial.pojo.entity.MartialAthlete;
import org.springblade.modules.martial.pojo.entity.MartialCompetition;
import org.springblade.modules.martial.service.IMartialAthleteService;
import org.springblade.modules.martial.service.IMartialCompetitionService;
import org.springblade.modules.system.pojo.entity.User;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 赛事信息 控制器
*
@@ -25,6 +33,7 @@ import org.springframework.web.bind.annotation.*;
public class MartialCompetitionController extends BladeController {
private final IMartialCompetitionService competitionService;
private final IMartialAthleteService martialAthleteService;
/**
* 详情
@@ -43,6 +52,14 @@ public class MartialCompetitionController extends BladeController {
@Operation(summary = "分页列表", description = "分页查询")
public R<IPage<MartialCompetition>> list(MartialCompetition competition, Query query) {
IPage<MartialCompetition> pages = competitionService.page(Condition.getPage(query), Condition.getQueryWrapper(competition));
List<MartialCompetition> pagelist = pages.getRecords();
for (MartialCompetition martialCompetition : pagelist) {
Long cnt = martialAthleteService.count(Wrappers.<MartialAthlete>query().lambda()
.eq(MartialAthlete::getCompetitionId, martialCompetition.getId())
.eq(MartialAthlete::getIsDeleted, 0)
);
martialCompetition.setTotalParticipants(cnt.intValue());
}
return R.data(pages);
}

View File

@@ -1,6 +1,8 @@
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;
@@ -19,12 +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;
@@ -43,20 +50,23 @@ 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);
/**
* 登录验证
*
* @param dto 登录信息(比赛编码+邀请码)
* @return 登录结果token、用户信息、分配的场地和项目
*/
@PostMapping("/login")
@Operation(summary = "登录验证", description = "使用比赛编码和邀请码登录")
public R<MiniLoginVO> login(@RequestBody MiniLoginDTO dto) {
// 1. 根据邀请码查询邀请信息
LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>();
inviteQuery.eq(MartialJudgeInvite::getInviteCode, dto.getInviteCode());
inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
@@ -66,29 +76,24 @@ public class MartialMiniController extends BladeController {
return R.fail("邀请码不存在");
}
// 2. 验证邀请码是否过期
if (invite.getExpireTime() != null && invite.getExpireTime().isBefore(LocalDateTime.now())) {
return R.fail("邀请码已过期");
}
// 3. 查询比赛信息
MartialCompetition competition = competitionService.getById(invite.getCompetitionId());
if (competition == null) {
return R.fail("比赛不存在");
}
// 4. 验证比赛编码
if (!competition.getCompetitionCode().equals(dto.getMatchCode())) {
return R.fail("比赛编码不匹配");
}
// 5. 查询评委信息
MartialJudge judge = judgeService.getById(invite.getJudgeId());
if (judge == null) {
return R.fail("评委信息不存在");
}
// 6. 生成访问令牌
String token = UUID.randomUUID().toString().replace("-", "");
invite.setAccessToken(token);
invite.setTokenExpireTime(LocalDateTime.now().plusDays(7));
@@ -98,16 +103,14 @@ public class MartialMiniController extends BladeController {
invite.setDeviceInfo(dto.getDeviceInfo());
judgeInviteService.updateById(invite);
// 7. 查询场地信息(裁判长没有固定场地)
MartialVenue venue = null;
// 从 mt_venue 表获取场地信息
MtVenue mtVenue = null;
if (invite.getVenueId() != null) {
venue = venueService.getById(invite.getVenueId());
mtVenue = mtVenueService.getById(invite.getVenueId());
}
// 8. 解析分配的项目
List<MiniLoginVO.ProjectInfo> projects = parseProjects(invite.getProjects());
// 9. 构造返回结果
MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token);
vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub");
@@ -117,71 +120,214 @@ 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);
}
/**
* 提交评分(评委)
*
* @param dto 评分信息
* @return 提交结果
* 注意ID字段使用String类型接收避免JavaScript大数精度丢失问题
*/
@PostMapping("/score/submit")
@Operation(summary = "提交评分", description = "评委提交对选手的评分")
public R submitScore(@RequestBody org.springblade.modules.martial.pojo.dto.MiniScoreSubmitDTO dto) {
// 转换DTO为实体
MartialScore score = new MartialScore();
score.setAthleteId(dto.getAthleteId());
score.setJudgeId(dto.getJudgeId());
// 将String类型的ID转换为Long避免JavaScript大数精度丢失
score.setAthleteId(parseLong(dto.getAthleteId()));
score.setJudgeId(parseLong(dto.getJudgeId()));
score.setScore(dto.getScore());
score.setProjectId(dto.getProjectId());
score.setCompetitionId(dto.getCompetitionId());
score.setVenueId(dto.getVenueId());
score.setScheduleId(dto.getScheduleId());
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());
// 将扣分项列表转换为JSON字符串存储
if (dto.getDeductions() != null && !dto.getDeductions().isEmpty()) {
score.setDeductionItems(com.alibaba.fastjson.JSON.toJSONString(dto.getDeductions()));
// 将String类型的扣分项ID转换为Long
List<Long> deductionIds = dto.getDeductions().stream()
.map(this::parseLong)
.filter(id -> id != null)
.collect(Collectors.toList());
score.setDeductionItems(com.alibaba.fastjson.JSON.toJSONString(deductionIds));
}
// 获取评委姓名
if (dto.getJudgeId() != null) {
var judge = judgeService.getById(dto.getJudgeId());
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);
// 评分保存成功后,计算并更新选手总分
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, venueId);
}
}
return success ? R.success("评分提交成功") : R.fail("评分提交失败");
}
/**
* 计算并更新选手总分
* 总分算法:去掉一个最高分和一个最低分,取剩余分数的平均值
* 特殊情况:裁判数量<3时直接取平均分
* 只有所有裁判都评分完成后才更新总分
*/
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);
// 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(totalScore);
athleteService.updateById(athlete);
}
}
} catch (Exception e) {
// 记录错误但不影响评分提交
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);
}
/**
* 获取选手列表
* - 普通裁判:获取待评分的选手列表(该裁判还未评分的选手)
* - 裁判长:获取已有评分的选手列表(至少有一个裁判已评分的选手)
*
* @param judgeId 裁判ID
* @param refereeType 裁判类型1-裁判长, 2-普通裁判)
* @param projectId 项目ID可选用于筛选特定项目的选手
* @param venueId 场地ID可选用于筛选特定场地的选手
* @return 选手列表
* 安全地将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<List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> getAthletes(
@Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
public R<IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> getAthletes(
@RequestParam Long judgeId,
@RequestParam Integer refereeType,
@RequestParam(required = false) Long projectId,
@RequestParam(required = false) Long venueId
@RequestParam(required = false) Long venueId,
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size
) {
// 1. 构建选手查询条件
LambdaQueryWrapper<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>();
@@ -190,57 +336,91 @@ public class MartialMiniController extends BladeController {
if (projectId != null) {
athleteQuery.eq(MartialAthlete::getProjectId, projectId);
}
// 注意场地筛选需要通过评分记录的venueId来过滤这里先查询所有选手
// 按出场顺序排序
athleteQuery.orderByAsc(MartialAthlete::getOrderNum);
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 (venueId != null) {
scoreQuery.eq(MartialScore::getVenueId, venueId);
}
// 排除裁判长的评分
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. 根据裁判类型筛选选手
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> result;
// 4. 获取该场地的应评裁判数量
int requiredJudgeCount = getRequiredJudgeCount(venueId);
// 5. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
if (refereeType == 1) {
// 裁判长:返回已有评分的选手
result = athletes.stream()
.filter(athlete -> {
List<MartialScore> scores = scoresByAthlete.get(athlete.getId());
return scores != null && !scores.isEmpty();
})
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId())))
// 裁判长:返回所有选手前端根据totalScore判断是否显示修改按钮
filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList());
} else {
// 普通裁判:返回该裁判还未评分的选手
result = athletes.stream()
.filter(athlete -> {
List<MartialScore> scores = scoresByAthlete.get(athlete.getId());
if (scores == null) {
return true; // 没有任何评分,可以评
}
// 检查该裁判是否已评分
return scores.stream().noneMatch(s -> s.getJudgeId().equals(judgeId));
})
.map(athlete -> convertToAthleteListVO(athlete, null))
// 普通裁判:返回所有选手,标记是否已评分
filteredList = athletes.stream()
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId, requiredJudgeCount))
.collect(java.util.stream.Collectors.toList());
}
return R.data(result);
// 6. 手动分页
int total = filteredList.size();
int fromIndex = (current - 1) * size;
int toIndex = Math.min(fromIndex + size, total);
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> pageRecords;
if (fromIndex >= total) {
pageRecords = new ArrayList<>();
} else {
pageRecords = filteredList.subList(fromIndex, toIndex);
}
// 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());
}
/**
* 获取评分详情
*
* @param athleteId 选手ID
* @return 评分详情(选手信息+所有评委的评分)
*/
@GetMapping("/score/detail/{athleteId}")
@Operation(summary = "评分详情", description = "查看选手的所有评委评分")
@@ -251,9 +431,6 @@ public class MartialMiniController extends BladeController {
/**
* 修改评分(裁判长)
*
* @param dto 修改信息选手ID、修改后的分数、修改原因
* @return 修改结果
*/
@PutMapping("/score/modify")
@Operation(summary = "修改评分", description = "裁判长修改选手总分")
@@ -264,45 +441,97 @@ public class MartialMiniController extends BladeController {
/**
* 退出登录
*
* @return 退出结果
*/
@PostMapping("/logout")
@Operation(summary = "退出登录", description = "清除登录状态")
public R logout() {
// TODO: 实现真实的退出逻辑
// 1. 清除token
// 2. 清除session
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验证
*
* @return 验证结果
* Token验证从Redis恢复登录状态
*/
@GetMapping("/verify")
@Operation(summary = "Token验证", description = "验证当前token是否有效")
public R verify() {
// TODO: 实现真实的token验证逻辑
// 1. 从请求头获取token
// 2. 验证token是否有效
// 3. 返回验证结果
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) {
private org.springblade.modules.martial.pojo.vo.MiniAthleteListVO convertToAthleteListVO(
MartialAthlete athlete,
List<MartialScore> scores,
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());
vo.setIdCard(athlete.getIdCard());
vo.setNumber(athlete.getPlayerNo());
vo.setTeam(athlete.getTeamName());
vo.setOrderNum(athlete.getOrderNum());
vo.setCompetitionStatus(athlete.getCompetitionStatus());
vo.setTotalScore(athlete.getTotalScore());
// 设置应评分裁判数量
vo.setRequiredJudgeCount(requiredJudgeCount);
// 设置项目名称
if (athlete.getProjectId() != null) {
@@ -312,9 +541,44 @@ public class MartialMiniController extends BladeController {
}
}
// 设置评分裁判数量(仅裁判长可见)
if (scores != null) {
vo.setScoredJudgeCount(scores.size());
// 设置评分状态
int scoredCount = 0;
if (scores != null && !scores.isEmpty()) {
scoredCount = scores.size();
vo.setScoredJudgeCount(scoredCount);
// 查找当前裁判的评分
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);
}
// 判断评分是否完成(所有裁判都已评分)
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;
@@ -331,11 +595,9 @@ public class MartialMiniController extends BladeController {
}
try {
// 解析JSON数组格式为 [{"projectId": 1, "projectName": "太极拳"}, ...]
ObjectMapper mapper = new ObjectMapper();
List<Long> projectIds = mapper.readValue(projectsJson, new TypeReference<List<Long>>() {});
// 查询项目详情
if (Func.isNotEmpty(projectIds)) {
List<MartialProject> projectList = projectService.listByIds(projectIds);
projects = projectList.stream().map(project -> {
@@ -346,7 +608,6 @@ public class MartialMiniController extends BladeController {
}).collect(Collectors.toList());
}
} catch (Exception e) {
// 如果JSON解析失败尝试按逗号分隔的ID字符串解析
try {
String[] ids = projectsJson.split(",");
List<Long> projectIds = new ArrayList<>();

View File

@@ -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<MiniLoginVO> login(@RequestBody MiniLoginDTO dto) {
LambdaQueryWrapper<MartialJudgeInvite> 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<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.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<Long> 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<IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> 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<MartialAthlete> athleteQuery = new LambdaQueryWrapper<>();
athleteQuery.eq(MartialAthlete::getIsDeleted, 0);
if (projectId != null) {
athleteQuery.eq(MartialAthlete::getProjectId, projectId);
}
athleteQuery.orderByAsc(MartialAthlete::getOrderNum);
List<MartialAthlete> athletes = athleteService.list(athleteQuery);
// 2. 获取所有评分记录
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getIsDeleted, 0);
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. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
if (refereeType == 1) {
// 裁判长:返回已有评分的选手
filteredList = athletes.stream()
.filter(athlete -> {
List<MartialScore> 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<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> pageRecords;
if (fromIndex >= total) {
pageRecords = new ArrayList<>();
} else {
pageRecords = filteredList.subList(fromIndex, toIndex);
}
// 5. 构建分页结果
IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> page = new Page<>(current, size, total);
page.setRecords(pageRecords);
return R.data(page);
}
/**
* 获取评分详情
*/
@GetMapping("/score/detail/{athleteId}")
@Operation(summary = "评分详情", description = "查看选手的所有评委评分")
public R<MiniScoreDetailVO> 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<MartialScore> 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<MiniLoginVO.ProjectInfo> parseProjects(String projectsJson) {
List<MiniLoginVO.ProjectInfo> projects = new ArrayList<>();
if (Func.isEmpty(projectsJson)) {
return projects;
}
try {
ObjectMapper mapper = new ObjectMapper();
List<Long> projectIds = mapper.readValue(projectsJson, new TypeReference<List<Long>>() {});
if (Func.isNotEmpty(projectIds)) {
List<MartialProject> 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<Long> projectIds = new ArrayList<>();
for (String id : ids) {
projectIds.add(Long.parseLong(id.trim()));
}
if (Func.isNotEmpty(projectIds)) {
List<MartialProject> 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;
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the dreamlu.net developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: Chill 庄骞 (smallchill@163.com)
*/
package org.springblade.modules.martial.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionAttachment;
/**
* 赛事附件 Mapper 接口
*
* @author BladeX
*/
public interface MartialCompetitionAttachmentMapper extends BaseMapper<MartialCompetitionAttachment> {
}

View File

@@ -17,11 +17,13 @@
d.venue_name AS venueName,
d.time_slot AS timeSlot,
d.time_slot_index AS timeSlotIndex,
d.schedule_date AS scheduleDate,
p.id AS participantId,
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

View File

@@ -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<MtVenue> {
}

View File

@@ -28,4 +28,7 @@ public class MiniScoreModifyDTO implements Serializable {
@Schema(description = "修改原因/备注")
private String note;
@Schema(description = "场地ID")
private Long venueId;
}

View File

@@ -9,6 +9,8 @@ import java.util.List;
/**
* 小程序提交评分请求DTO
*
* 注意所有ID字段使用String类型避免JavaScript大数精度丢失问题
*
* @author BladeX
*/
@@ -19,29 +21,29 @@ public class MiniScoreSubmitDTO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "选手ID")
private Long athleteId;
private String athleteId;
@Schema(description = "评委ID")
private Long judgeId;
private String judgeId;
@Schema(description = "评分")
private BigDecimal score;
@Schema(description = "扣分项ID列表")
private List<Long> deductions;
private List<String> deductions;
@Schema(description = "备注")
private String note;
@Schema(description = "项目ID")
private Long projectId;
private String projectId;
@Schema(description = "赛事ID")
private Long competitionId;
private String competitionId;
@Schema(description = "场地ID")
private Long venueId;
private String venueId;
@Schema(description = "赛程ID")
private Long scheduleId;
private String scheduleId;
}

View File

@@ -40,4 +40,10 @@ public class ParticipantDTO implements Serializable {
@Schema(description = "排序")
private Integer sortOrder;
/**
* 选手姓名
*/
@Schema(description = "选手姓名")
private String playerName;
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright (c) 2018-2028, Chill Zhuang All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the dreamlu.net developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: Chill 庄骞 (smallchill@163.com)
*/
package org.springblade.modules.martial.pojo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
/**
* 赛事附件实体类(通用)
* 支持多种附件类型:赛事发布、赛事规程、活动日程、成绩、奖牌榜、图片直播
*
* @author BladeX
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("martial_competition_attachment")
@Schema(description = "赛事附件")
public class MartialCompetitionAttachment extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
* 赛事ID
*/
@Schema(description = "赛事ID")
private Long competitionId;
/**
* 附件类型
* info-赛事发布, rules-赛事规程, schedule-活动日程,
* results-成绩, medals-奖牌榜, photos-图片直播
*/
@Schema(description = "附件类型info-赛事发布, rules-赛事规程, schedule-活动日程, results-成绩, medals-奖牌榜, photos-图片直播")
private String attachmentType;
/**
* 文件名称
*/
@Schema(description = "文件名称")
private String fileName;
/**
* 文件URL
*/
@Schema(description = "文件URL")
private String fileUrl;
/**
* 文件大小(字节)
*/
@Schema(description = "文件大小(字节)")
private Long fileSize;
/**
* 文件类型(扩展名)
*/
@Schema(description = "文件类型pdf/doc/docx/xls/xlsx/jpg/png等")
private String fileType;
/**
* 排序序号
*/
@Schema(description = "排序序号")
private Integer orderNum;
/**
* 状态1-启用 0-禁用)
*/
@Schema(description = "状态1-启用 0-禁用)")
private Integer status;
}

View File

@@ -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;
}

View File

@@ -1,5 +1,9 @@
package org.springblade.modules.martial.pojo.vo;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -8,39 +12,73 @@ import java.math.BigDecimal;
/**
* 小程序选手列表VO
*
* 注意Long类型的ID字段使用ToStringSerializer序列化为字符串
* 避免JavaScript大数精度丢失问题
*
* @author BladeX
*/
@Data
@Schema(description = "小程序选手列表")
@JsonInclude(JsonInclude.Include.ALWAYS)
public class MiniAthleteListVO implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "选手ID")
@JsonProperty("athleteId")
@JsonSerialize(using = ToStringSerializer.class)
private Long athleteId;
@Schema(description = "选手姓名")
@JsonProperty("name")
private String name;
@Schema(description = "身份证号")
@JsonProperty("idCard")
private String idCard = "";
@Schema(description = "参赛编号")
@JsonProperty("number")
private String number;
@Schema(description = "队伍名称")
@JsonProperty("team")
private String team;
@Schema(description = "项目名称")
@JsonProperty("projectName")
private String projectName;
@Schema(description = "出场顺序")
@JsonProperty("orderNum")
private Integer orderNum;
@Schema(description = "总分(裁判长可见")
@Schema(description = "是否已评分(当前裁判")
@JsonProperty("scored")
private Boolean scored = false;
@Schema(description = "我的评分(当前裁判的评分)")
@JsonProperty("myScore")
private BigDecimal myScore;
@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;
}

View File

@@ -29,10 +29,16 @@ public class ScheduleGroupDetailVO implements Serializable {
private String timeSlot;
private Integer timeSlotIndex; // 时间段索引(0=第1天上午,1=第1天下午,2=第2天上午,...)
// === 比赛日期 ===
private String scheduleDate;
// === 参赛者信息 ===
private Long participantId;
private String organization;
private String checkInStatus;
private String scheduleStatus;
private Integer performanceOrder;
// === 选手姓名 ===
private String playerName;
}

View File

@@ -0,0 +1,41 @@
package org.springblade.modules.martial.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionAttachment;
import java.util.List;
/**
* 赛事附件 服务类
*
* @author BladeX
*/
public interface IMartialCompetitionAttachmentService extends IService<MartialCompetitionAttachment> {
/**
* 根据赛事ID和附件类型获取附件列表
*
* @param competitionId 赛事ID
* @param attachmentType 附件类型
* @return 附件列表
*/
List<MartialCompetitionAttachment> getByCompetitionIdAndType(Long competitionId, String attachmentType);
/**
* 根据赛事ID获取所有附件
*
* @param competitionId 赛事ID
* @return 附件列表
*/
List<MartialCompetitionAttachment> getByCompetitionId(Long competitionId);
/**
* 删除赛事的指定类型附件
*
* @param competitionId 赛事ID
* @param attachmentType 附件类型
* @return 是否成功
*/
boolean removeByCompetitionIdAndType(Long competitionId, String attachmentType);
}

View File

@@ -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<MtVenue> {
}

View File

@@ -0,0 +1,48 @@
package org.springblade.modules.martial.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springblade.modules.martial.mapper.MartialCompetitionAttachmentMapper;
import org.springblade.modules.martial.pojo.entity.MartialCompetitionAttachment;
import org.springblade.modules.martial.service.IMartialCompetitionAttachmentService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 赛事附件 服务实现类
*
* @author BladeX
*/
@Service
public class MartialCompetitionAttachmentServiceImpl extends ServiceImpl<MartialCompetitionAttachmentMapper, MartialCompetitionAttachment> implements IMartialCompetitionAttachmentService {
@Override
public List<MartialCompetitionAttachment> getByCompetitionIdAndType(Long competitionId, String attachmentType) {
LambdaQueryWrapper<MartialCompetitionAttachment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialCompetitionAttachment::getCompetitionId, competitionId)
.eq(MartialCompetitionAttachment::getAttachmentType, attachmentType)
.eq(MartialCompetitionAttachment::getStatus, 1)
.orderByAsc(MartialCompetitionAttachment::getOrderNum);
return this.list(wrapper);
}
@Override
public List<MartialCompetitionAttachment> getByCompetitionId(Long competitionId) {
LambdaQueryWrapper<MartialCompetitionAttachment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialCompetitionAttachment::getCompetitionId, competitionId)
.eq(MartialCompetitionAttachment::getStatus, 1)
.orderByAsc(MartialCompetitionAttachment::getAttachmentType)
.orderByAsc(MartialCompetitionAttachment::getOrderNum);
return this.list(wrapper);
}
@Override
public boolean removeByCompetitionIdAndType(Long competitionId, String attachmentType) {
LambdaQueryWrapper<MartialCompetitionAttachment> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialCompetitionAttachment::getCompetitionId, competitionId)
.eq(MartialCompetitionAttachment::getAttachmentType, attachmentType);
return this.remove(wrapper);
}
}

View File

@@ -744,7 +744,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
participant.setScheduleDetailId(detailId);
participant.setScheduleGroupId(groupId);
participant.setParticipantId(athlete.getId());
participant.setOrganization(athlete.getOrganization());
participant.setOrganization(athlete.getTeamName() != null && !athlete.getTeamName().isEmpty() ? athlete.getTeamName() : athlete.getOrganization());
participant.setPlayerName(athlete.getPlayerName());
participant.setProjectName(groupData.getGroupName());
participant.setCategory(athlete.getCategory());

View File

@@ -60,77 +60,69 @@ public class MartialScheduleServiceImpl extends ServiceImpl<MartialScheduleMappe
*/
@Override
public List<ScheduleExportExcel> exportSchedule(Long competitionId) {
// 1. 查询该赛事的所有赛程
List<MartialSchedule> schedules = this.list(
new QueryWrapper<MartialSchedule>()
.eq("competition_id", competitionId)
.eq("is_deleted", 0)
.orderByAsc("schedule_date", "start_time")
);
List<ScheduleExportExcel> exportList = new ArrayList<>();
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 2. 遍历每个赛程
for (MartialSchedule schedule : schedules) {
// 3. 获取该赛程的所有运动员
List<MartialScheduleAthlete> scheduleAthletes = scheduleAthleteService.list(
new QueryWrapper<MartialScheduleAthlete>()
.eq("schedule_id", schedule.getId())
.eq("is_deleted", 0)
.orderByAsc("order_num")
);
// 使用与 getScheduleResult 相同的数据源
List<ScheduleGroupDetailVO> details = scheduleGroupMapper.selectScheduleGroupDetails(competitionId);
// 4. 获取项目和场地信息(一次查询,避免重复)
MartialProject project = schedule.getProjectId() != null
? projectService.getById(schedule.getProjectId())
: null;
MartialVenue venue = schedule.getVenueId() != null
? venueService.getById(schedule.getVenueId())
: null;
if (details.isEmpty()) {
return exportList;
}
// 5. 如果没有运动员,创建一条基础记录
if (scheduleAthletes.isEmpty()) {
// 按分组ID分组然后按 displayOrder 排序
Map<Long, List<ScheduleGroupDetailVO>> groupMap = details.stream()
.collect(Collectors.groupingBy(ScheduleGroupDetailVO::getGroupId));
// 获取分组的排序顺序
List<Long> sortedGroupIds = details.stream()
.collect(Collectors.toMap(
ScheduleGroupDetailVO::getGroupId,
d -> d.getDisplayOrder() != null ? d.getDisplayOrder() : 999,
(a, b) -> a
))
.entrySet().stream()
.sorted(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.collect(Collectors.toList());
// 按排序顺序遍历分组
for (Long groupId : sortedGroupIds) {
List<ScheduleGroupDetailVO> groupDetails = groupMap.get(groupId);
if (groupDetails == null || groupDetails.isEmpty()) {
continue;
}
ScheduleGroupDetailVO firstDetail = groupDetails.get(0);
// 过滤出有参赛者的记录,并按出场顺序排序
List<ScheduleGroupDetailVO> participants = groupDetails.stream()
.filter(d -> d.getParticipantId() != null)
.sorted(Comparator.comparing(d -> d.getPerformanceOrder() != null ? d.getPerformanceOrder() : 999))
.collect(Collectors.toList());
if (participants.isEmpty()) {
// 没有参赛者,创建一条基础记录
ScheduleExportExcel excel = new ScheduleExportExcel();
excel.setScheduleDate(schedule.getScheduleDate() != null
? schedule.getScheduleDate().format(dateFormatter)
: "");
excel.setTimeSlot(schedule.getTimeSlot());
excel.setVenueName(venue != null ? venue.getVenueName() : "");
excel.setProjectName(project != null ? project.getProjectName() : "");
excel.setCategory(schedule.getGroupTitle());
excel.setStatus(schedule.getIsConfirmed() != null && schedule.getIsConfirmed() == 1
? "已确认" : "未确认");
excel.setScheduleDate(firstDetail.getScheduleDate() != null ? firstDetail.getScheduleDate() : "");
excel.setTimeSlot(firstDetail.getTimeSlot());
excel.setVenueName(firstDetail.getVenueName());
excel.setProjectName(firstDetail.getGroupName());
excel.setCategory(firstDetail.getCategory());
excel.setStatus("completed".equals(firstDetail.getScheduleStatus()) ? "已完成" : "草稿");
exportList.add(excel);
} else {
// 6. 为每个运动员创建导出记录
for (MartialScheduleAthlete scheduleAthlete : scheduleAthletes) {
MartialAthlete athlete = athleteService.getById(scheduleAthlete.getAthleteId());
if (athlete == null) {
continue;
}
// 为每个参赛者创建导出记录
for (ScheduleGroupDetailVO detail : participants) {
ScheduleExportExcel excel = new ScheduleExportExcel();
excel.setScheduleDate(schedule.getScheduleDate() != null
? schedule.getScheduleDate().format(dateFormatter)
: "");
excel.setTimeSlot(schedule.getTimeSlot());
excel.setVenueName(venue != null ? venue.getVenueName() : "");
excel.setProjectName(project != null ? project.getProjectName() : "");
excel.setCategory(schedule.getGroupTitle());
excel.setAthleteName(athlete.getPlayerName());
excel.setTeamName(athlete.getTeamName());
excel.setSortOrder(scheduleAthlete.getOrderNum());
// 状态转换
if (scheduleAthlete.getIsCompleted() != null && scheduleAthlete.getIsCompleted() == 1) {
excel.setStatus("已完赛");
} else if (schedule.getIsConfirmed() != null && schedule.getIsConfirmed() == 1) {
excel.setStatus("已确认");
} else {
excel.setStatus("待确认");
}
excel.setScheduleDate(detail.getScheduleDate() != null ? detail.getScheduleDate() : "");
excel.setTimeSlot(detail.getTimeSlot());
excel.setVenueName(detail.getVenueName());
excel.setProjectName(detail.getGroupName());
excel.setCategory(detail.getCategory());
excel.setAthleteName(detail.getPlayerName());
excel.setTeamName(detail.getOrganization());
excel.setSortOrder(detail.getPerformanceOrder());
excel.setStatus(detail.getCheckInStatus() != null ? detail.getCheckInStatus() : "未签到");
exportList.add(excel);
}
}
@@ -224,6 +216,7 @@ public class MartialScheduleServiceImpl extends ServiceImpl<MartialScheduleMappe
dto.setSchoolUnit(d.getOrganization());
dto.setStatus(d.getCheckInStatus() != null ? d.getCheckInStatus() : "未签到");
dto.setSortOrder(d.getPerformanceOrder());
dto.setPlayerName(d.getPlayerName());
return dto;
})
.collect(Collectors.toList());

View File

@@ -285,7 +285,13 @@ public class MartialScoreServiceImpl extends ServiceImpl<MartialScoreMapper, Mar
// 注意:这里假设修改记录存储在选手的 totalScore 和一个额外的字段中
// 由于 MartialAthlete 实体没有 originalScore 字段,我们查找修改过的评分记录
MartialScore modifiedScore = scores.stream()
.filter(s -> s.getOriginalScore() != null && s.getModifyReason() != null)
.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);
@@ -326,42 +332,77 @@ public class MartialScoreServiceImpl extends ServiceImpl<MartialScoreMapper, Mar
throw new ServiceException("选手不存在");
}
// 2. 验证分数范围
// 2. 验证分数基本范围
if (!validateScore(dto.getModifiedScore())) {
throw new ServiceException("修改后的分数必须在5.000-10.000之间");
}
// 3. 保存原始总分(如果是第一次修改)
BigDecimal originalTotalScore = athlete.getTotalScore();
// 3. 查找是否已存在裁判长的修改记录
LambdaQueryWrapper<MartialScore> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MartialScore::getAthleteId, dto.getAthleteId())
.like(MartialScore::getJudgeName, "裁判长修改");
MartialScore existingRecord = this.getOne(queryWrapper);
// 4. 更新选手总分
// 4. 确定原始计算总分(用于范围验证)
// 如果已有修改记录,使用记录中保存的原始分数;否则使用当前总分
BigDecimal originalCalculatedScore;
if (existingRecord != null && existingRecord.getOriginalScore() != null) {
// 已有修改记录,使用记录中的原始计算总分
originalCalculatedScore = existingRecord.getOriginalScore();
} else {
// 首次修改,当前总分就是原始计算总分
originalCalculatedScore = athlete.getTotalScore();
}
// 5. 验证修改范围基于原始计算总分±0.050
if (originalCalculatedScore != null) {
BigDecimal minAllowed = originalCalculatedScore.subtract(new BigDecimal("0.050"));
BigDecimal maxAllowed = originalCalculatedScore.add(new BigDecimal("0.050"));
if (dto.getModifiedScore().compareTo(minAllowed) < 0 || dto.getModifiedScore().compareTo(maxAllowed) > 0) {
throw new ServiceException("修改分数只能在原始计算总分(" + originalCalculatedScore + ")±0.050范围内");
}
}
// 6. 更新选手总分
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());
boolean recordSaved;
if (existingRecord != null) {
// 7a. 更新现有的裁判长修改记录(保持原始计算总分不变)
existingRecord.setScore(dto.getModifiedScore());
// originalScore 保持不变,始终是系统计算的原始总分
existingRecord.setModifyReason(dto.getNote());
existingRecord.setModifyTime(LocalDateTime.now());
// 直接使用 baseMapper.updateById 绕过 Service 层的状态检查,裁判长可以无限次修改
recordSaved = this.baseMapper.updateById(existingRecord) > 0;
log.info("裁判长更新评分记录 - 选手ID:{}, 姓名:{}, 原始计算总分:{}, 修改后总分:{}, 修改原因:{}",
athlete.getId(), athlete.getPlayerName(), originalCalculatedScore, dto.getModifiedScore(), dto.getNote());
} else {
// 7b. 创建新的裁判长修改记录
MartialScore modificationRecord = new MartialScore();
modificationRecord.setCompetitionId(athlete.getCompetitionId());
modificationRecord.setAthleteId(athlete.getId());
modificationRecord.setProjectId(athlete.getProjectId());
modificationRecord.setVenueId(dto.getVenueId()); // 设置场地ID
modificationRecord.setJudgeId(dto.getModifierId());
modificationRecord.setScore(dto.getModifiedScore());
modificationRecord.setOriginalScore(originalCalculatedScore); // 保存原始计算总分
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() + "(裁判长修改)");
// 查询修改者信息
MartialJudge modifier = judgeService.getById(dto.getModifierId());
if (modifier != null) {
modificationRecord.setJudgeName(modifier.getName() + "(裁判长修改)");
}
recordSaved = this.save(modificationRecord);
log.info("裁判长新增评分记录 - 选手ID:{}, 姓名:{}, 原始计算总分:{}, 修改后总分:{}, 修改原因:{}",
athlete.getId(), athlete.getPlayerName(), originalCalculatedScore, dto.getModifiedScore(), dto.getNote());
}
boolean recordSaved = this.save(modificationRecord);
log.info("裁判长修改评分 - 选手ID:{}, 姓名:{}, 原始总分:{}, 修改后总分:{}, 修改原因:{}",
athlete.getId(), athlete.getPlayerName(), originalTotalScore, dto.getModifiedScore(), dto.getNote());
return athleteUpdated && recordSaved;
}

View File

@@ -0,0 +1,374 @@
package org.springblade.modules.martial.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.modules.martial.pojo.dto.MiniScoreModifyDTO;
import org.springblade.modules.martial.pojo.entity.MartialAthlete;
import org.springblade.modules.martial.pojo.entity.MartialJudge;
import org.springblade.modules.martial.pojo.entity.MartialScore;
import org.springblade.modules.martial.mapper.MartialScoreMapper;
import org.springblade.modules.martial.pojo.vo.MiniScoreDetailVO;
import org.springblade.modules.martial.service.IMartialAthleteService;
import org.springblade.modules.martial.service.IMartialJudgeProjectService;
import org.springblade.modules.martial.service.IMartialJudgeService;
import org.springblade.modules.martial.service.IMartialScoreService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* Score 服务实现类
*
* @author BladeX
*/
@Slf4j
@Service
public class MartialScoreServiceImpl extends ServiceImpl<MartialScoreMapper, MartialScore> 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<MartialScore> scores = this.list(
new QueryWrapper<MartialScore>()
.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<MartialScore> getAnomalyScores(Long athleteId, Long projectId) {
// 获取该运动员的所有评分
List<MartialScore> scores = this.list(
new QueryWrapper<MartialScore>()
.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<MartialScore> scores = this.list(
new QueryWrapper<MartialScore>()
.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<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
scoreQuery.eq(MartialScore::getAthleteId, athleteId);
scoreQuery.eq(MartialScore::getIsDeleted, 0);
scoreQuery.orderByAsc(MartialScore::getScoreTime);
List<MartialScore> scores = this.list(scoreQuery);
List<MiniScoreDetailVO.JudgeScore> 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;
}
}

View File

@@ -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<MtVenueMapper, MtVenue> implements IMtVenueService {
}