feat: 添加三级裁判评分系统(裁判员→主裁判→总裁)

- 新增总裁(裁判长)角色支持,referee_type=3
- MartialResult实体添加主裁判/总裁确认字段和score_status状态
- MartialJudgeInvite实体添加角色常量和判断方法
- MartialMiniController添加三级确认API和登录角色判断
- MartialResultServiceImpl实现三级确认业务逻辑
- MartialScoreServiceImpl主裁判确认时同步martial_result表

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
2025-12-28 15:49:11 +08:00
parent 491c8db26c
commit aab66f79fe
8 changed files with 454 additions and 6 deletions

View File

@@ -21,8 +21,11 @@ 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.dto.ChiefJudgeConfirmDTO;
import org.springblade.modules.martial.pojo.dto.GeneralJudgeConfirmDTO;
import org.springblade.modules.martial.pojo.entity.MtVenue;
import org.springblade.modules.martial.pojo.entity.MartialVenue;
import org.springblade.modules.martial.pojo.entity.MartialResult;
import org.springblade.core.redis.cache.BladeRedis;
import org.springframework.web.bind.annotation.*;
@@ -56,6 +59,7 @@ public class MartialMiniController extends BladeController {
private final IMartialAthleteService athleteService;
private final IMartialScoreService scoreService;
private final BladeRedis bladeRedis;
private final IMartialResultService resultService;
// Redis缓存key前缀
private static final String MINI_LOGIN_CACHE_PREFIX = "mini:login:";
@@ -121,7 +125,15 @@ public class MartialMiniController extends BladeController {
MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token);
vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub");
String role = invite.getRole();
Integer refereeType = invite.getRefereeType();
if ("general_judge".equals(role) || (refereeType != null && refereeType == 3)) {
vo.setUserRole("general");
} else if ("chief_judge".equals(role) || (refereeType != null && refereeType == 2)) {
vo.setUserRole("admin");
} else {
vo.setUserRole("pub");
}
vo.setMatchId(competition.getId());
vo.setMatchName(competition.getCompetitionName());
vo.setMatchTime(competition.getCompetitionStartTime() != null ?
@@ -517,7 +529,15 @@ public class MartialMiniController extends BladeController {
MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token);
vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub");
String role = invite.getRole();
Integer refereeType = invite.getRefereeType();
if ("general_judge".equals(role) || (refereeType != null && refereeType == 3)) {
vo.setUserRole("general");
} else if ("chief_judge".equals(role) || (refereeType != null && refereeType == 2)) {
vo.setUserRole("admin");
} else {
vo.setUserRole("pub");
}
vo.setMatchId(competition != null ? competition.getId() : null);
vo.setMatchName(competition != null ? competition.getCompetitionName() : null);
vo.setMatchTime(competition != null && competition.getCompetitionStartTime() != null ?
@@ -679,4 +699,71 @@ public class MartialMiniController extends BladeController {
return projects;
}
}
// ========== 三级裁判评分流程 API ==========
/**
* 主裁判确认/修改分数
*/
@PostMapping("/chief/confirm")
@Operation(summary = "主裁判确认分数", description = "主裁判确认或修改选手分数")
public R confirmByChiefJudge(@RequestBody ChiefJudgeConfirmDTO dto) {
Long resultId = parseLong(dto.getResultId());
Long chiefJudgeId = parseLong(dto.getChiefJudgeId());
if (resultId == null || chiefJudgeId == null) {
return R.fail("参数错误");
}
boolean success = resultService.confirmByChiefJudge(resultId, chiefJudgeId, dto.getScore(), dto.getNote());
return success ? R.success("确认成功") : R.fail("确认失败");
}
/**
* 总裁确认/修改分数
*/
@PostMapping("/general/confirm")
@Operation(summary = "总裁确认分数", description = "总裁确认或修改选手分数")
public R confirmByGeneralJudge(@RequestBody GeneralJudgeConfirmDTO dto) {
Long resultId = parseLong(dto.getResultId());
Long generalJudgeId = parseLong(dto.getGeneralJudgeId());
if (resultId == null || generalJudgeId == null) {
return R.fail("参数错误");
}
boolean success = resultService.confirmByGeneralJudge(resultId, generalJudgeId, dto.getScore(), dto.getNote());
return success ? R.success("确认成功") : R.fail("确认失败");
}
/**
* 获取待主裁判确认的成绩列表
*/
@GetMapping("/chief/pending")
@Operation(summary = "待主裁判确认列表", description = "获取待主裁判确认的成绩列表")
public R<List<MartialResult>> getPendingChiefConfirmList(@RequestParam Long venueId) {
List<MartialResult> list = resultService.getPendingChiefConfirmList(venueId);
return R.data(list);
}
/**
* 获取待总裁确认的成绩列表
*/
@GetMapping("/general/pending")
@Operation(summary = "待总裁确认列表", description = "获取待总裁确认的成绩列表(所有场地)")
public R<List<MartialResult>> getPendingGeneralConfirmList(@RequestParam Long competitionId) {
List<MartialResult> list = resultService.getPendingGeneralConfirmList(competitionId);
return R.data(list);
}
/**
* 获取所有场地列表(总裁用)
*/
@GetMapping("/general/venues")
@Operation(summary = "获取所有场地", description = "总裁获取比赛的所有场地列表")
public R<List<MartialVenue>> getAllVenues(@RequestParam Long competitionId) {
LambdaQueryWrapper<MartialVenue> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(MartialVenue::getCompetitionId, competitionId);
wrapper.eq(MartialVenue::getIsDeleted, 0);
wrapper.orderByAsc(MartialVenue::getVenueName);
List<MartialVenue> venues = venueService.list(wrapper);
return R.data(venues);
}
}

View File

@@ -0,0 +1,26 @@
package org.springblade.modules.martial.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 主裁判确认评分DTO
*/
@Data
@Schema(description = "主裁判确认评分DTO")
public class ChiefJudgeConfirmDTO {
@Schema(description = "成绩ID")
private String resultId;
@Schema(description = "主裁判ID")
private String chiefJudgeId;
@Schema(description = "确认/修改后的分数null表示直接确认原分数")
private BigDecimal score;
@Schema(description = "备注")
private String note;
}

View File

@@ -0,0 +1,26 @@
package org.springblade.modules.martial.pojo.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
/**
* 总裁确认评分DTO
*/
@Data
@Schema(description = "总裁确认评分DTO")
public class GeneralJudgeConfirmDTO {
@Schema(description = "成绩ID")
private String resultId;
@Schema(description = "总裁ID")
private String generalJudgeId;
@Schema(description = "确认/修改后的分数null表示直接确认原分数")
private BigDecimal score;
@Schema(description = "备注")
private String note;
}

View File

@@ -37,6 +37,14 @@ public class MartialJudgeInvite extends TenantEntity {
private static final long serialVersionUID = 1L;
// ========== 角色常量 ==========
/** 裁判员 */
public static final String ROLE_JUDGE = "judge";
/** 主裁判 */
public static final String ROLE_CHIEF_JUDGE = "chief_judge";
/** 总裁(裁判长) */
public static final String ROLE_GENERAL_JUDGE = "general_judge";
/**
* 赛事ID
*/
@@ -56,13 +64,13 @@ public class MartialJudgeInvite extends TenantEntity {
private String inviteCode;
/**
* 角色(judge-裁判员,chief_judge-主裁判)
* 角色(judge-裁判员, chief_judge-主裁判, general_judge-总裁)
*/
@Schema(description = "角色")
private String role;
/**
* 分配场地ID
* 分配场地ID (总裁时为null表示负责所有场地)
*/
@Schema(description = "分配场地ID")
private Long venueId;
@@ -169,4 +177,25 @@ public class MartialJudgeInvite extends TenantEntity {
@Schema(description = "裁判类型")
private Integer refereeType;
/**
* 判断是否为裁判员
*/
public boolean isJudge() {
return ROLE_JUDGE.equals(this.role);
}
/**
* 判断是否为主裁判
*/
public boolean isChiefJudge() {
return ROLE_CHIEF_JUDGE.equals(this.role);
}
/**
* 判断是否为总裁
*/
public boolean isGeneralJudge() {
return ROLE_GENERAL_JUDGE.equals(this.role);
}
}

View File

@@ -38,6 +38,14 @@ public class MartialResult extends TenantEntity {
private static final long serialVersionUID = 1L;
// ========== 评分状态常量 ==========
/** 评分状态:裁判员评分中 */
public static final int SCORE_STATUS_JUDGING = 0;
/** 评分状态:主裁判已确认 */
public static final int SCORE_STATUS_CHIEF_CONFIRMED = 1;
/** 评分状态:总裁已确认 */
public static final int SCORE_STATUS_GENERAL_CONFIRMED = 2;
/**
* 赛事ID
*/
@@ -158,4 +166,62 @@ public class MartialResult extends TenantEntity {
@Schema(description = "发布时间")
private LocalDateTime publishTime;
// ========== 主裁判确认相关字段 ==========
/**
* 主裁判确认/修改后的分数
*/
@Schema(description = "主裁判确认/修改后的分数")
private BigDecimal chiefJudgeScore;
/**
* 主裁判ID
*/
@Schema(description = "主裁判ID")
private Long chiefJudgeId;
/**
* 主裁判确认时间
*/
@Schema(description = "主裁判确认时间")
private LocalDateTime chiefJudgeTime;
/**
* 主裁判备注
*/
@Schema(description = "主裁判备注")
private String chiefJudgeNote;
// ========== 总裁确认相关字段 ==========
/**
* 总裁确认/修改后的分数
*/
@Schema(description = "总裁确认/修改后的分数")
private BigDecimal generalJudgeScore;
/**
* 总裁ID
*/
@Schema(description = "总裁ID")
private Long generalJudgeId;
/**
* 总裁确认时间
*/
@Schema(description = "总裁确认时间")
private LocalDateTime generalJudgeTime;
/**
* 总裁备注
*/
@Schema(description = "总裁备注")
private String generalJudgeNote;
/**
* 评分状态: 0-裁判员评分中, 1-主裁判已确认, 2-总裁已确认
*/
@Schema(description = "评分状态: 0-裁判员评分中, 1-主裁判已确认, 2-总裁已确认")
private Integer scoreStatus;
}

View File

@@ -70,4 +70,27 @@ public interface IMartialResultService extends IService<MartialResult> {
*/
List<CertificateVO> batchGenerateCertificates(Long projectId);
// ========== 三级裁判评分流程方法 ==========
/**
* 主裁判确认/修改分数
*/
boolean confirmByChiefJudge(Long resultId, Long chiefJudgeId, BigDecimal score, String note);
/**
* 总裁确认/修改分数
*/
boolean confirmByGeneralJudge(Long resultId, Long generalJudgeId, BigDecimal score, String note);
/**
* 获取待主裁判确认的成绩列表
*/
List<MartialResult> getPendingChiefConfirmList(Long venueId);
/**
* 获取待总裁确认的成绩列表
*/
List<MartialResult> getPendingGeneralConfirmList(Long competitionId);
}

View File

@@ -559,4 +559,158 @@ public class MartialResultServiceImpl extends ServiceImpl<MartialResultMapper, M
return certificates;
}
// ========== 三级裁判评分流程方法 ==========
/**
* 主裁判确认/修改分数
*
* @param resultId 成绩ID
* @param chiefJudgeId 主裁判ID
* @param score 确认/修改后的分数null表示直接确认原分数
* @param note 备注
* @return 是否成功
*/
@Transactional(rollbackFor = Exception.class)
public boolean confirmByChiefJudge(Long resultId, Long chiefJudgeId, BigDecimal score, String note) {
MartialResult result = this.getById(resultId);
if (result == null) {
throw new ServiceException("成绩记录不存在");
}
// 检查状态:只有裁判员评分完成后才能确认
if (result.getScoreStatus() != null && result.getScoreStatus() >= MartialResult.SCORE_STATUS_CHIEF_CONFIRMED) {
throw new ServiceException("该成绩已被主裁判确认,无法重复操作");
}
// 确定最终分数
BigDecimal finalScore = score != null ? score : result.getTotalScore();
// 验证分数范围(如果有修改)
if (score != null && result.getTotalScore() != null) {
BigDecimal minAllowed = result.getTotalScore().subtract(new BigDecimal("0.050"));
BigDecimal maxAllowed = result.getTotalScore().add(new BigDecimal("0.050"));
if (score.compareTo(minAllowed) < 0 || score.compareTo(maxAllowed) > 0) {
throw new ServiceException("修改分数只能在原始总分±0.050范围内");
}
}
// 更新主裁判确认信息
result.setChiefJudgeScore(finalScore);
result.setChiefJudgeId(chiefJudgeId);
result.setChiefJudgeTime(LocalDateTime.now());
result.setChiefJudgeNote(note);
result.setScoreStatus(MartialResult.SCORE_STATUS_CHIEF_CONFIRMED);
// 更新最终得分
result.setFinalScore(finalScore);
boolean success = this.updateById(result);
log.info("主裁判确认成绩 - 成绩ID:{}, 主裁判ID:{}, 原分数:{}, 确认分数:{}, 备注:{}",
resultId, chiefJudgeId, result.getTotalScore(), finalScore, note);
return success;
}
/**
* 总裁确认/修改分数
*
* @param resultId 成绩ID
* @param generalJudgeId 总裁ID
* @param score 确认/修改后的分数null表示直接确认原分数
* @param note 备注
* @return 是否成功
*/
@Transactional(rollbackFor = Exception.class)
public boolean confirmByGeneralJudge(Long resultId, Long generalJudgeId, BigDecimal score, String note) {
MartialResult result = this.getById(resultId);
if (result == null) {
throw new ServiceException("成绩记录不存在");
}
// 检查状态:必须主裁判已确认
if (result.getScoreStatus() == null || result.getScoreStatus() < MartialResult.SCORE_STATUS_CHIEF_CONFIRMED) {
throw new ServiceException("主裁判尚未确认,总裁无法操作");
}
if (result.getScoreStatus() >= MartialResult.SCORE_STATUS_GENERAL_CONFIRMED) {
throw new ServiceException("该成绩已被总裁确认,无法重复操作");
}
// 确定最终分数(基于主裁判确认的分数)
BigDecimal baseScore = result.getChiefJudgeScore() != null ? result.getChiefJudgeScore() : result.getTotalScore();
BigDecimal finalScore = score != null ? score : baseScore;
// 验证分数范围(如果有修改)
if (score != null && baseScore != null) {
BigDecimal minAllowed = baseScore.subtract(new BigDecimal("0.050"));
BigDecimal maxAllowed = baseScore.add(new BigDecimal("0.050"));
if (score.compareTo(minAllowed) < 0 || score.compareTo(maxAllowed) > 0) {
throw new ServiceException("修改分数只能在主裁判确认分数±0.050范围内");
}
}
// 更新总裁确认信息
result.setGeneralJudgeScore(finalScore);
result.setGeneralJudgeId(generalJudgeId);
result.setGeneralJudgeTime(LocalDateTime.now());
result.setGeneralJudgeNote(note);
result.setScoreStatus(MartialResult.SCORE_STATUS_GENERAL_CONFIRMED);
// 更新最终得分
result.setFinalScore(finalScore);
boolean success = this.updateById(result);
log.info("总裁确认成绩 - 成绩ID:{}, 总裁ID:{}, 主裁判分数:{}, 确认分数:{}, 备注:{}",
resultId, generalJudgeId, baseScore, finalScore, note);
return success;
}
/**
* 获取待主裁判确认的成绩列表
*
* @param venueId 场地ID
* @return 成绩列表
*/
public List<MartialResult> getPendingChiefConfirmList(Long venueId) {
QueryWrapper<MartialResult> wrapper = new QueryWrapper<>();
wrapper.eq("venue_id", venueId);
wrapper.eq("is_deleted", 0);
wrapper.and(w -> w.isNull("score_status").or().eq("score_status", MartialResult.SCORE_STATUS_JUDGING));
wrapper.orderByAsc("create_time");
return this.list(wrapper);
}
/**
* 获取待总裁确认的成绩列表
*
* @param competitionId 比赛ID总裁可以看所有场地
* @return 成绩列表
*/
@Override
public List<MartialResult> getPendingGeneralConfirmList(Long competitionId) {
QueryWrapper<MartialResult> wrapper = new QueryWrapper<>();
wrapper.eq("competition_id", competitionId);
wrapper.eq("is_deleted", 0);
wrapper.eq("score_status", MartialResult.SCORE_STATUS_CHIEF_CONFIRMED);
wrapper.orderByAsc("create_time");
List<MartialResult> results = this.list(wrapper);
// 填充选手信息
for (MartialResult result : results) {
if (result.getAthleteId() != null) {
MartialAthlete athlete = athleteService.getById(result.getAthleteId());
if (athlete != null) {
result.setPlayerName(athlete.getPlayerName());
result.setTeamName(athlete.getTeamName());
}
}
}
return results;
}
}

View File

@@ -15,6 +15,8 @@ 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.springblade.modules.martial.service.IMartialResultService;
import org.springblade.modules.martial.pojo.entity.MartialResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -43,6 +45,9 @@ public class MartialScoreServiceImpl extends ServiceImpl<MartialScoreMapper, Mar
@Autowired
private IMartialJudgeService judgeService;
@Autowired
private IMartialResultService resultService;
/** 最低分 */
private static final BigDecimal MIN_SCORE = new BigDecimal("5.000");
/** 最高分 */
@@ -403,7 +408,39 @@ public class MartialScoreServiceImpl extends ServiceImpl<MartialScoreMapper, Mar
athlete.getId(), athlete.getPlayerName(), originalCalculatedScore, dto.getModifiedScore(), dto.getNote());
}
return athleteUpdated && recordSaved;
// 8. 同步更新 martial_result 表(三级裁判系统)
// 查找或创建该选手的成绩记录
LambdaQueryWrapper<MartialResult> resultQuery = new LambdaQueryWrapper<>();
resultQuery.eq(MartialResult::getAthleteId, athlete.getId())
.eq(MartialResult::getProjectId, athlete.getProjectId())
.eq(MartialResult::getIsDeleted, 0);
MartialResult result = resultService.getOne(resultQuery);
if (result == null) {
// 创建新的成绩记录
result = new MartialResult();
result.setCompetitionId(athlete.getCompetitionId());
result.setAthleteId(athlete.getId());
result.setProjectId(athlete.getProjectId());
result.setVenueId(dto.getVenueId());
result.setOriginalScore(originalCalculatedScore);
result.setFinalScore(dto.getModifiedScore());
}
// 更新主裁判确认信息
result.setChiefJudgeScore(dto.getModifiedScore());
result.setChiefJudgeId(dto.getModifierId());
result.setChiefJudgeTime(LocalDateTime.now());
result.setChiefJudgeNote(dto.getNote());
result.setScoreStatus(MartialResult.SCORE_STATUS_CHIEF_CONFIRMED);
result.setFinalScore(dto.getModifiedScore());
boolean resultSaved = resultService.saveOrUpdate(result);
log.info("主裁判确认成绩 - 选手ID:{}, 成绩ID:{}, 分数:{}, score_status:{}",
athlete.getId(), result.getId(), dto.getModifiedScore(), result.getScoreStatus());
return athleteUpdated && recordSaved && resultSaved;
}
}