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 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<>();

View File

@@ -10,6 +10,8 @@ import java.util.List;
/**
* 小程序提交评分请求DTO
*
* 注意所有ID字段使用String类型避免JavaScript大数精度丢失问题
*
* @author BladeX
*/
@Data
@@ -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

@@ -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;
@@ -11,6 +13,9 @@ import java.math.BigDecimal;
/**
* 小程序选手列表VO
*
* 注意Long类型的ID字段使用ToStringSerializer序列化为字符串
* 避免JavaScript大数精度丢失问题
*
* @author BladeX
*/
@Data
@@ -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;