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:
2025-12-19 18:29:25 +08:00
parent 0f0beaf62e
commit cc4a01ea28
3 changed files with 134 additions and 87 deletions

View File

@@ -24,6 +24,7 @@ import org.springblade.modules.martial.service.*;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -51,14 +52,10 @@ public class MartialMiniController extends BladeController {
/** /**
* 登录验证 * 登录验证
*
* @param dto 登录信息(比赛编码+邀请码)
* @return 登录结果token、用户信息、分配的场地和项目
*/ */
@PostMapping("/login") @PostMapping("/login")
@Operation(summary = "登录验证", description = "使用比赛编码和邀请码登录") @Operation(summary = "登录验证", description = "使用比赛编码和邀请码登录")
public R<MiniLoginVO> login(@RequestBody MiniLoginDTO dto) { public R<MiniLoginVO> login(@RequestBody MiniLoginDTO dto) {
// 1. 根据邀请码查询邀请信息
LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>(); LambdaQueryWrapper<MartialJudgeInvite> inviteQuery = new LambdaQueryWrapper<>();
inviteQuery.eq(MartialJudgeInvite::getInviteCode, dto.getInviteCode()); inviteQuery.eq(MartialJudgeInvite::getInviteCode, dto.getInviteCode());
inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0); inviteQuery.eq(MartialJudgeInvite::getIsDeleted, 0);
@@ -68,29 +65,24 @@ public class MartialMiniController extends BladeController {
return R.fail("邀请码不存在"); return R.fail("邀请码不存在");
} }
// 2. 验证邀请码是否过期
if (invite.getExpireTime() != null && invite.getExpireTime().isBefore(LocalDateTime.now())) { if (invite.getExpireTime() != null && invite.getExpireTime().isBefore(LocalDateTime.now())) {
return R.fail("邀请码已过期"); return R.fail("邀请码已过期");
} }
// 3. 查询比赛信息
MartialCompetition competition = competitionService.getById(invite.getCompetitionId()); MartialCompetition competition = competitionService.getById(invite.getCompetitionId());
if (competition == null) { if (competition == null) {
return R.fail("比赛不存在"); return R.fail("比赛不存在");
} }
// 4. 验证比赛编码
if (!competition.getCompetitionCode().equals(dto.getMatchCode())) { if (!competition.getCompetitionCode().equals(dto.getMatchCode())) {
return R.fail("比赛编码不匹配"); return R.fail("比赛编码不匹配");
} }
// 5. 查询评委信息
MartialJudge judge = judgeService.getById(invite.getJudgeId()); MartialJudge judge = judgeService.getById(invite.getJudgeId());
if (judge == null) { if (judge == null) {
return R.fail("评委信息不存在"); return R.fail("评委信息不存在");
} }
// 6. 生成访问令牌
String token = UUID.randomUUID().toString().replace("-", ""); String token = UUID.randomUUID().toString().replace("-", "");
invite.setAccessToken(token); invite.setAccessToken(token);
invite.setTokenExpireTime(LocalDateTime.now().plusDays(7)); invite.setTokenExpireTime(LocalDateTime.now().plusDays(7));
@@ -100,16 +92,13 @@ public class MartialMiniController extends BladeController {
invite.setDeviceInfo(dto.getDeviceInfo()); invite.setDeviceInfo(dto.getDeviceInfo());
judgeInviteService.updateById(invite); judgeInviteService.updateById(invite);
// 7. 查询场地信息(裁判长没有固定场地)
MartialVenue venue = null; MartialVenue venue = null;
if (invite.getVenueId() != null) { if (invite.getVenueId() != null) {
venue = venueService.getById(invite.getVenueId()); venue = venueService.getById(invite.getVenueId());
} }
// 8. 解析分配的项目
List<MiniLoginVO.ProjectInfo> projects = parseProjects(invite.getProjects()); List<MiniLoginVO.ProjectInfo> projects = parseProjects(invite.getProjects());
// 9. 构造返回结果
MiniLoginVO vo = new MiniLoginVO(); MiniLoginVO vo = new MiniLoginVO();
vo.setToken(token); vo.setToken(token);
vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub"); vo.setUserRole("chief_judge".equals(invite.getRole()) ? "admin" : "pub");
@@ -128,59 +117,114 @@ public class MartialMiniController extends BladeController {
/** /**
* 提交评分(评委) * 提交评分(评委)
* * 注意ID字段使用String类型接收避免JavaScript大数精度丢失问题
* @param dto 评分信息
* @return 提交结果
*/ */
@PostMapping("/score/submit") @PostMapping("/score/submit")
@Operation(summary = "提交评分", description = "评委提交对选手的评分") @Operation(summary = "提交评分", description = "评委提交对选手的评分")
public R submitScore(@RequestBody org.springblade.modules.martial.pojo.dto.MiniScoreSubmitDTO dto) { public R submitScore(@RequestBody org.springblade.modules.martial.pojo.dto.MiniScoreSubmitDTO dto) {
// 转换DTO为实体
MartialScore score = new MartialScore(); 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.setScore(dto.getScore());
score.setProjectId(dto.getProjectId()); score.setProjectId(parseLong(dto.getProjectId()));
score.setCompetitionId(dto.getCompetitionId()); score.setCompetitionId(parseLong(dto.getCompetitionId()));
score.setVenueId(dto.getVenueId()); score.setVenueId(parseLong(dto.getVenueId()));
score.setScheduleId(dto.getScheduleId()); score.setScheduleId(parseLong(dto.getScheduleId()));
score.setNote(dto.getNote()); score.setNote(dto.getNote());
score.setScoreTime(LocalDateTime.now()); score.setScoreTime(LocalDateTime.now());
// 将扣分项列表转换为JSON字符串存储
if (dto.getDeductions() != null && !dto.getDeductions().isEmpty()) { 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));
} }
// 获取评委姓名 Long judgeId = parseLong(dto.getJudgeId());
if (dto.getJudgeId() != null) { if (judgeId != null) {
var judge = judgeService.getById(dto.getJudgeId()); var judge = judgeService.getById(judgeId);
if (judge != null) { if (judge != null) {
score.setJudgeName(judge.getName()); score.setJudgeName(judge.getName());
} }
} }
// 保存评分(会自动进行权限验证和分数范围验证)
boolean success = scoreService.save(score); 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("评分提交失败"); 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") @GetMapping("/score/athletes")
@Operation(summary = "获取选手列表", description = "根据裁判类型获取不同的选手列表(支持分页)") @Operation(summary = "获取选手列表", description = "根据裁判类型获取选手列表(支持分页)")
public R<IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> getAthletes( public R<IPage<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO>> getAthletes(
@RequestParam Long judgeId, @RequestParam Long judgeId,
@RequestParam Integer refereeType, @RequestParam Integer refereeType,
@@ -196,9 +240,7 @@ public class MartialMiniController extends BladeController {
if (projectId != null) { if (projectId != null) {
athleteQuery.eq(MartialAthlete::getProjectId, projectId); athleteQuery.eq(MartialAthlete::getProjectId, projectId);
} }
// 注意场地筛选需要通过评分记录的venueId来过滤这里先查询所有选手
// 按出场顺序排序
athleteQuery.orderByAsc(MartialAthlete::getOrderNum); athleteQuery.orderByAsc(MartialAthlete::getOrderNum);
List<MartialAthlete> athletes = athleteService.list(athleteQuery); List<MartialAthlete> athletes = athleteService.list(athleteQuery);
@@ -212,7 +254,7 @@ public class MartialMiniController extends BladeController {
java.util.Map<Long, List<MartialScore>> scoresByAthlete = allScores.stream() java.util.Map<Long, List<MartialScore>> scoresByAthlete = allScores.stream()
.collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId)); .collect(java.util.stream.Collectors.groupingBy(MartialScore::getAthleteId));
// 3. 根据裁判类型筛选选手 // 3. 根据裁判类型处理选手列表
List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList; List<org.springblade.modules.martial.pojo.vo.MiniAthleteListVO> filteredList;
if (refereeType == 1) { if (refereeType == 1) {
@@ -222,20 +264,12 @@ public class MartialMiniController extends BladeController {
List<MartialScore> scores = scoresByAthlete.get(athlete.getId()); List<MartialScore> scores = scoresByAthlete.get(athlete.getId());
return scores != null && !scores.isEmpty(); 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()); .collect(java.util.stream.Collectors.toList());
} else { } else {
// 普通裁判:返回该裁判还未评分的选手 // 普通裁判:返回所有选手,标记是否已评分
filteredList = athletes.stream() filteredList = athletes.stream()
.filter(athlete -> { .map(athlete -> convertToAthleteListVO(athlete, scoresByAthlete.get(athlete.getId()), judgeId))
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))
.collect(java.util.stream.Collectors.toList()); .collect(java.util.stream.Collectors.toList());
} }
@@ -260,9 +294,6 @@ public class MartialMiniController extends BladeController {
/** /**
* 获取评分详情 * 获取评分详情
*
* @param athleteId 选手ID
* @return 评分详情(选手信息+所有评委的评分)
*/ */
@GetMapping("/score/detail/{athleteId}") @GetMapping("/score/detail/{athleteId}")
@Operation(summary = "评分详情", description = "查看选手的所有评委评分") @Operation(summary = "评分详情", description = "查看选手的所有评委评分")
@@ -273,9 +304,6 @@ public class MartialMiniController extends BladeController {
/** /**
* 修改评分(裁判长) * 修改评分(裁判长)
*
* @param dto 修改信息选手ID、修改后的分数、修改原因
* @return 修改结果
*/ */
@PutMapping("/score/modify") @PutMapping("/score/modify")
@Operation(summary = "修改评分", description = "裁判长修改选手总分") @Operation(summary = "修改评分", description = "裁判长修改选手总分")
@@ -286,42 +314,32 @@ public class MartialMiniController extends BladeController {
/** /**
* 退出登录 * 退出登录
*
* @return 退出结果
*/ */
@PostMapping("/logout") @PostMapping("/logout")
@Operation(summary = "退出登录", description = "清除登录状态") @Operation(summary = "退出登录", description = "清除登录状态")
public R logout() { public R logout() {
// TODO: 实现真实的退出逻辑
// 1. 清除token
// 2. 清除session
return R.success("退出成功"); return R.success("退出成功");
} }
/** /**
* Token验证 * Token验证
*
* @return 验证结果
*/ */
@GetMapping("/verify") @GetMapping("/verify")
@Operation(summary = "Token验证", description = "验证当前token是否有效") @Operation(summary = "Token验证", description = "验证当前token是否有效")
public R verify() { public R verify() {
// TODO: 实现真实的token验证逻辑
// 1. 从请求头获取token
// 2. 验证token是否有效
// 3. 返回验证结果
return R.success("Token有效"); return R.success("Token有效");
} }
/** /**
* 转换选手实体为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) {
org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO(); org.springblade.modules.martial.pojo.vo.MiniAthleteListVO vo = new org.springblade.modules.martial.pojo.vo.MiniAthleteListVO();
vo.setAthleteId(athlete.getId()); vo.setAthleteId(athlete.getId());
vo.setName(athlete.getPlayerName()); vo.setName(athlete.getPlayerName());
// 调试日志
System.out.println("DEBUG: athlete.getId()=" + athlete.getId() + ", idCard=" + athlete.getIdCard() + ", playerNo=" + athlete.getPlayerNo());
vo.setIdCard(athlete.getIdCard()); vo.setIdCard(athlete.getIdCard());
vo.setNumber(athlete.getPlayerNo()); vo.setNumber(athlete.getPlayerNo());
vo.setTeam(athlete.getTeamName()); 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()); 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; return vo;
@@ -356,11 +390,9 @@ public class MartialMiniController extends BladeController {
} }
try { try {
// 解析JSON数组格式为 [{"projectId": 1, "projectName": "太极拳"}, ...]
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
List<Long> projectIds = mapper.readValue(projectsJson, new TypeReference<List<Long>>() {}); List<Long> projectIds = mapper.readValue(projectsJson, new TypeReference<List<Long>>() {});
// 查询项目详情
if (Func.isNotEmpty(projectIds)) { if (Func.isNotEmpty(projectIds)) {
List<MartialProject> projectList = projectService.listByIds(projectIds); List<MartialProject> projectList = projectService.listByIds(projectIds);
projects = projectList.stream().map(project -> { projects = projectList.stream().map(project -> {
@@ -371,7 +403,6 @@ public class MartialMiniController extends BladeController {
}).collect(Collectors.toList()); }).collect(Collectors.toList());
} }
} catch (Exception e) { } catch (Exception e) {
// 如果JSON解析失败尝试按逗号分隔的ID字符串解析
try { try {
String[] ids = projectsJson.split(","); String[] ids = projectsJson.split(",");
List<Long> projectIds = new ArrayList<>(); List<Long> projectIds = new ArrayList<>();

View File

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

View File

@@ -2,6 +2,8 @@ package org.springblade.modules.martial.pojo.vo;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; 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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -11,6 +13,9 @@ import java.math.BigDecimal;
/** /**
* 小程序选手列表VO * 小程序选手列表VO
* *
* 注意Long类型的ID字段使用ToStringSerializer序列化为字符串
* 避免JavaScript大数精度丢失问题
*
* @author BladeX * @author BladeX
*/ */
@Data @Data
@@ -22,6 +27,7 @@ public class MiniAthleteListVO implements Serializable {
@Schema(description = "选手ID") @Schema(description = "选手ID")
@JsonProperty("athleteId") @JsonProperty("athleteId")
@JsonSerialize(using = ToStringSerializer.class)
private Long athleteId; private Long athleteId;
@Schema(description = "选手姓名") @Schema(description = "选手姓名")
@@ -48,7 +54,15 @@ public class MiniAthleteListVO implements Serializable {
@JsonProperty("orderNum") @JsonProperty("orderNum")
private Integer 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") @JsonProperty("totalScore")
private BigDecimal totalScore; private BigDecimal totalScore;