fix: 修复评分后总分显示为-1的问题
问题根因: 1. submitScore方法只保存评分记录,未计算更新选手总分 2. BladeX框架将null的Number类型序列化为-1 修复内容: - 添加updateAthleteTotalScore方法,评分后计算平均分并更新选手总分 - 添加parseLong方法,安全地将String转换为Long(解决JS大数精度问题) - MiniScoreSubmitDTO的ID字段改为String类型 - MiniAthleteListVO的athleteId添加ToStringSerializer序列化 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import org.springblade.modules.martial.service.*;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -51,14 +52,10 @@ public class MartialMiniController extends BladeController {
|
||||
|
||||
/**
|
||||
* 登录验证
|
||||
*
|
||||
* @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);
|
||||
@@ -68,29 +65,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));
|
||||
@@ -100,16 +92,13 @@ public class MartialMiniController extends BladeController {
|
||||
invite.setDeviceInfo(dto.getDeviceInfo());
|
||||
judgeInviteService.updateById(invite);
|
||||
|
||||
// 7. 查询场地信息(裁判长没有固定场地)
|
||||
MartialVenue venue = null;
|
||||
if (invite.getVenueId() != null) {
|
||||
venue = venueService.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");
|
||||
@@ -128,59 +117,114 @@ public class MartialMiniController extends BladeController {
|
||||
|
||||
/**
|
||||
* 提交评分(评委)
|
||||
*
|
||||
* @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());
|
||||
if (athleteId != null && projectId != null) {
|
||||
updateAthleteTotalScore(athleteId, projectId);
|
||||
}
|
||||
}
|
||||
|
||||
return success ? R.success("评分提交成功") : R.fail("评分提交失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算并更新选手总分
|
||||
* 总分 = 所有裁判评分的平均值(保留3位小数)
|
||||
*/
|
||||
private void updateAthleteTotalScore(Long athleteId, Long projectId) {
|
||||
try {
|
||||
// 查询该选手在该项目的所有评分
|
||||
LambdaQueryWrapper<MartialScore> scoreQuery = new LambdaQueryWrapper<>();
|
||||
scoreQuery.eq(MartialScore::getAthleteId, athleteId);
|
||||
scoreQuery.eq(MartialScore::getProjectId, projectId);
|
||||
scoreQuery.eq(MartialScore::getIsDeleted, 0);
|
||||
List<MartialScore> scores = scoreService.list(scoreQuery);
|
||||
|
||||
if (scores != null && !scores.isEmpty()) {
|
||||
// 计算平均分
|
||||
BigDecimal totalScore = scores.stream()
|
||||
.map(MartialScore::getScore)
|
||||
.filter(s -> s != null)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal avgScore = totalScore.divide(
|
||||
new BigDecimal(scores.size()),
|
||||
3,
|
||||
RoundingMode.HALF_UP
|
||||
);
|
||||
|
||||
// 更新选手总分
|
||||
MartialAthlete athlete = athleteService.getById(athleteId);
|
||||
if (athlete != null) {
|
||||
athlete.setTotalScore(avgScore);
|
||||
athleteService.updateById(athlete);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 记录错误但不影响评分提交
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全地将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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取选手列表(支持分页)
|
||||
* - 普通裁判:获取待评分的选手列表(该裁判还未评分的选手)
|
||||
* - 裁判长:获取已有评分的选手列表(至少有一个裁判已评分的选手)
|
||||
*
|
||||
* @param judgeId 裁判ID
|
||||
* @param refereeType 裁判类型(1-裁判长, 2-普通裁判)
|
||||
* @param projectId 项目ID(可选,用于筛选特定项目的选手)
|
||||
* @param venueId 场地ID(可选,用于筛选特定场地的选手)
|
||||
* @param current 当前页码(默认1)
|
||||
* @param size 每页条数(默认10)
|
||||
* @return 分页选手列表
|
||||
* - 普通裁判:获取所有选手,标记是否已评分
|
||||
* - 裁判长:获取已有评分的选手列表
|
||||
*/
|
||||
@GetMapping("/score/athletes")
|
||||
@Operation(summary = "获取选手列表", description = "根据裁判类型获取不同的选手列表(支持分页)")
|
||||
@Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
|
||||
public R<IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> getAthletes(
|
||||
@RequestParam Long judgeId,
|
||||
@RequestParam Integer refereeType,
|
||||
@@ -196,9 +240,7 @@ public class MartialMiniController extends BladeController {
|
||||
if (projectId != null) {
|
||||
athleteQuery.eq(MartialAthlete::getProjectId, projectId);
|
||||
}
|
||||
// 注意:场地筛选需要通过评分记录的venueId来过滤,这里先查询所有选手
|
||||
|
||||
// 按出场顺序排序
|
||||
athleteQuery.orderByAsc(MartialAthlete::getOrderNum);
|
||||
|
||||
List<MartialAthlete> athletes = athleteService.list(athleteQuery);
|
||||
@@ -212,7 +254,7 @@ public class MartialMiniController extends BladeController {
|
||||
java.util.Map<Long, List<MartialScore>> scoresByAthlete = allScores.stream()
|
||||
.collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId));
|
||||
|
||||
// 3. 根据裁判类型筛选选手
|
||||
// 3. 根据裁判类型处理选手列表
|
||||
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
|
||||
|
||||
if (refereeType == 1) {
|
||||
@@ -222,20 +264,12 @@ public class MartialMiniController extends BladeController {
|
||||
List<MartialScore> scores = scoresByAthlete.get(athlete.getId());
|
||||
return scores != null && !scores.isEmpty();
|
||||
})
|
||||
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId())))
|
||||
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
} else {
|
||||
// 普通裁判:返回该裁判还未评分的选手
|
||||
// 普通裁判:返回所有选手,标记是否已评分
|
||||
filteredList = 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))
|
||||
.map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -260,9 +294,6 @@ public class MartialMiniController extends BladeController {
|
||||
|
||||
/**
|
||||
* 获取评分详情
|
||||
*
|
||||
* @param athleteId 选手ID
|
||||
* @return 评分详情(选手信息+所有评委的评分)
|
||||
*/
|
||||
@GetMapping("/score/detail/{athleteId}")
|
||||
@Operation(summary = "评分详情", description = "查看选手的所有评委评分")
|
||||
@@ -273,9 +304,6 @@ public class MartialMiniController extends BladeController {
|
||||
|
||||
/**
|
||||
* 修改评分(裁判长)
|
||||
*
|
||||
* @param dto 修改信息(选手ID、修改后的分数、修改原因)
|
||||
* @return 修改结果
|
||||
*/
|
||||
@PutMapping("/score/modify")
|
||||
@Operation(summary = "修改评分", description = "裁判长修改选手总分")
|
||||
@@ -286,42 +314,32 @@ public class MartialMiniController extends BladeController {
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*
|
||||
* @return 退出结果
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
@Operation(summary = "退出登录", description = "清除登录状态")
|
||||
public R logout() {
|
||||
// TODO: 实现真实的退出逻辑
|
||||
// 1. 清除token
|
||||
// 2. 清除session
|
||||
return R.success("退出成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* Token验证
|
||||
*
|
||||
* @return 验证结果
|
||||
*/
|
||||
@GetMapping("/verify")
|
||||
@Operation(summary = "Token验证", description = "验证当前token是否有效")
|
||||
public R verify() {
|
||||
// TODO: 实现真实的token验证逻辑
|
||||
// 1. 从请求头获取token
|
||||
// 2. 验证token是否有效
|
||||
// 3. 返回验证结果
|
||||
return R.success("Token有效");
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换选手实体为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) {
|
||||
org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO();
|
||||
vo.setAthleteId(athlete.getId());
|
||||
vo.setName(athlete.getPlayerName());
|
||||
// 调试日志
|
||||
System.out.println("DEBUG: athlete.getId()=" + athlete.getId() + ", idCard=" + athlete.getIdCard() + ", playerNo=" + athlete.getPlayerNo());
|
||||
vo.setIdCard(athlete.getIdCard());
|
||||
vo.setNumber(athlete.getPlayerNo());
|
||||
vo.setTeam(athlete.getTeamName());
|
||||
@@ -337,9 +355,25 @@ public class MartialMiniController extends BladeController {
|
||||
}
|
||||
}
|
||||
|
||||
// 设置已评分裁判数量(仅裁判长可见)
|
||||
if (scores != null) {
|
||||
// 设置评分状态
|
||||
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;
|
||||
@@ -356,11 +390,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 -> {
|
||||
@@ -371,7 +403,6 @@ public class MartialMiniController extends BladeController {
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 如果JSON解析失败,尝试按逗号分隔的ID字符串解析
|
||||
try {
|
||||
String[] ids = projectsJson.split(",");
|
||||
List<Long> projectIds = new ArrayList<>();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ 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;
|
||||
|
||||
@@ -10,6 +12,9 @@ import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 小程序选手列表VO
|
||||
*
|
||||
* 注意:Long类型的ID字段使用ToStringSerializer序列化为字符串,
|
||||
* 避免JavaScript大数精度丢失问题
|
||||
*
|
||||
* @author BladeX
|
||||
*/
|
||||
@@ -22,6 +27,7 @@ public class MiniAthleteListVO implements Serializable {
|
||||
|
||||
@Schema(description = "选手ID")
|
||||
@JsonProperty("athleteId")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long athleteId;
|
||||
|
||||
@Schema(description = "选手姓名")
|
||||
@@ -48,7 +54,15 @@ public class MiniAthleteListVO implements Serializable {
|
||||
@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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user