Ver Fonte

feat(game): 重构成绩计算逻辑并优化字典查询性能

- 移除faultB字段,统一使用faultA作为失误次数统计
- 新增completeTime字段用于记录比赛完成用时
- 修改成绩比较逻辑,将计时类项目的用时作为第三级排序条件
- 优化字典查询,实现批量查询避免多次数据库交互
- 在客户端控制器中添加租户忽略注解
- 优化成绩表单数据结构,调整字段命名规范
- 修复计时成绩格式转换的安全性问题
- 添加成绩提交前的数据校验机制
zhou há 1 semana atrás
pai
commit
c27ac9268f
14 ficheiros alterados com 206 adições e 99 exclusões
  1. 12 11
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/app_client/ToClientController.java
  2. 6 2
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/GameScore.java
  3. 3 3
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GameScoreBo.java
  4. 2 4
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/ScoreSubmitDetailBo.java
  5. 5 4
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/ScoreSubmitItemBo.java
  6. 6 4
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameScoreVo.java
  7. 2 4
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/ScoreSheetDetailVo.java
  8. 5 4
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/ScoreSheetItemVo.java
  9. 3 3
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/ScoreSheetVo.java
  10. 29 14
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameScoreServiceImpl.java
  11. 103 44
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/app/ToClientServiceImpl.java
  12. 2 2
      ruoyi-modules/ruoyi-game-event/src/main/resources/mapper/system/app/ToClientMapper.xml
  13. 8 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysDictTypeService.java
  14. 20 0
      ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDictTypeServiceImpl.java

+ 12 - 11
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/app_client/ToClientController.java

@@ -5,6 +5,7 @@ import org.dromara.common.core.domain.R;
 import org.dromara.common.idempotent.annotation.RepeatSubmit;
 import org.dromara.common.log.annotation.Log;
 import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.tenant.helper.TenantHelper;
 import org.dromara.system.domain.GameAppEvent;
 import org.dromara.system.domain.GameAppProject;
 import org.dromara.system.domain.bo.ClientLoginBo;
@@ -38,7 +39,7 @@ public class ToClientController {
     @Log(title = "app客户端登录", businessType = BusinessType.OTHER)
     @PostMapping("/login")
     public R<ClientLoginVo> login(@Validated @RequestBody ClientLoginBo bo) {
-        return R.ok(toClientService.login(bo));
+        return R.ok(TenantHelper.ignore(()  -> toClientService.login(bo)));
     }
 
     /**
@@ -46,7 +47,7 @@ public class ToClientController {
      */
     @GetMapping("/eventList/{refereeId}")
     public R<List<GameAppEvent>> eventList(@PathVariable Long refereeId) {
-        return R.ok(toClientService.getEventList(refereeId));
+        return R.ok(TenantHelper.ignore(() -> toClientService.getEventList(refereeId)));
     }
 
     /**
@@ -56,7 +57,7 @@ public class ToClientController {
     @Log(title = "app客户端删除赛事项", businessType = BusinessType.DELETE)
     @DeleteMapping("/eventRemove/{eventId}")
     public R<Void> eventRemove(@PathVariable Long eventId) {
-        toClientService.removeEvent(eventId);
+        TenantHelper.ignore(() -> toClientService.removeEvent(eventId));
         return R.ok("删除任务已提交,正在后台处理");
     }
 
@@ -67,7 +68,7 @@ public class ToClientController {
     @RepeatSubmit()
     @PostMapping("/eventSaveOrUpdate")
     public R<Void> eventSaveOrUpdate(@Validated @RequestBody ClientProjectSaveBo bo) {
-        toClientService.saveOrUpdateEventFromClient(bo);
+        TenantHelper.ignore(() -> toClientService.saveOrUpdateEventFromClient(bo));
         return R.ok();
     }
 
@@ -76,7 +77,7 @@ public class ToClientController {
      */
     @GetMapping("/projectList")
     public R<List<GameAppProject>> projectList(@RequestParam Long eventId, @RequestParam Long refereeId) {
-        return R.ok(toClientService.getProjectList(eventId, refereeId));
+        return R.ok(TenantHelper.ignore(() -> toClientService.getProjectList(eventId, refereeId)));
     }
 
     /**
@@ -84,7 +85,7 @@ public class ToClientController {
      */
     @GetMapping("/scoreSheet")
     public R<ScoreSheetVo> scoreSheet(@RequestParam Long eventId, @RequestParam Long projectId) {
-        return R.ok(toClientService.getScoreSheet(eventId, projectId));
+        return R.ok(TenantHelper.ignore(() -> toClientService.getScoreSheet(eventId, projectId)));
     }
 
     /**
@@ -94,7 +95,7 @@ public class ToClientController {
     @RepeatSubmit()
     @PostMapping("/scoreSubmit")
     public R<Void> scoreSubmit(@Validated @RequestBody ScoreSubmitBo bo) {
-        toClientService.submitScoreSheet(bo);
+        TenantHelper.ignore(() -> toClientService.submitScoreSheet(bo));
         return R.ok();
     }
 
@@ -105,7 +106,7 @@ public class ToClientController {
     @RepeatSubmit()
     @PostMapping("/endProject")
     public R<Void> endProject(@RequestParam Long projectId) {
-        toClientService.endProject(projectId);
+        TenantHelper.ignore(() -> toClientService.endProject(projectId));
         return R.ok();
     }
 
@@ -114,7 +115,7 @@ public class ToClientController {
      */
     @GetMapping("/scorePreviewList")
     public R<List<ScorePreviewVo>> scorePreviewList(@RequestParam Long projectId) {
-        return R.ok(toClientService.getScorePreviewList(projectId));
+        return R.ok(TenantHelper.ignore(() -> toClientService.getScorePreviewList(projectId)));
     }
 
     /**
@@ -124,7 +125,7 @@ public class ToClientController {
     @RepeatSubmit()
     @PostMapping("/scorePreviewUpdate")
     public R<Void> scorePreviewUpdate(@Validated @RequestBody ScorePreviewUpdateBo bo) {
-        toClientService.updateScorePreview(bo);
+        TenantHelper.ignore(() -> toClientService.updateScorePreview(bo));
         return R.ok();
     }
 
@@ -134,7 +135,7 @@ public class ToClientController {
     @Log(title = "app客户端删除预览成绩", businessType = BusinessType.DELETE)
     @DeleteMapping("/scorePreviewRemove/{scoreId}")
     public R<Void> scorePreviewRemove(@PathVariable Long scoreId) {
-        toClientService.removeScorePreview(scoreId);
+        TenantHelper.ignore(() -> toClientService.removeScorePreview(scoreId));
         return R.ok();
     }
 

+ 6 - 2
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/GameScore.java

@@ -89,14 +89,18 @@ public class GameScore extends TenantEntity {
     private Integer extraPoint;
 
     /**
-     * 失误次数A
+     * 失误次数
      */
     private Integer faultA;
 
     /**
-     * 失误次数B
+     * 失误次数B--更新线上时删除
      */
     private Integer faultB;
+    /**
+     * 比赛完成用时
+     */
+    private BigDecimal completeTime;
 
     /**
      * 成绩状态(0等待处理1处理完毕)

+ 3 - 3
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GameScoreBo.java

@@ -95,14 +95,14 @@ public class GameScoreBo extends BaseEntity {
     private Integer extraPoint;
 
     /**
-     * 失误次数A
+     * 失误次数
      */
     private Integer faultA;
 
     /**
-     * 失误次数B
+     * 比赛完成用时
      */
-    private Integer faultB;
+    private BigDecimal completeTime;
 
     /**
      * 成绩明细列表

+ 2 - 4
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/ScoreSubmitDetailBo.java

@@ -20,8 +20,6 @@ public class ScoreSubmitDetailBo implements Serializable {
     private Integer attemptIndex;
     /** 成绩值 */
     private BigDecimal num;
-    /** 失误次数A */
-    private Integer shiwuA;
-    /** 失误次数B */
-    private Integer shiwuB;
+    /** 失误次数 */
+    private Integer shiwu;
 }

+ 5 - 4
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/ScoreSubmitItemBo.java

@@ -4,6 +4,7 @@ import lombok.Data;
 
 import java.io.Serial;
 import java.io.Serializable;
+import java.math.BigDecimal;
 import java.util.List;
 
 /**
@@ -30,10 +31,10 @@ public class ScoreSubmitItemBo implements Serializable {
     private Long scoreId;
     /** 成绩 (支持复杂格式字符串) */
     private String score;
-    /** 失误次数A */
-    private Integer faultA;
-    /** 失误次数B */
-    private Integer faultB;
+    /** 失误次数 */
+    private Integer shiwu;
+    /** 完成用时 */
+    private BigDecimal yongshi;
 
     /** 成绩明细列表 (APP传回已有明细ID以避免冗余) */
     private List<ScoreSubmitDetailBo> chengji;

+ 6 - 4
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameScoreVo.java

@@ -104,16 +104,18 @@ public class GameScoreVo implements Serializable {
     private Integer extraPoint;
 
     /**
-     * 失误次数A
+     * 失误次数
      */
-    // @ExcelProperty(value = "失误次数A")
     private Integer faultA;
 
     /**
-     * 失误次数B
+     * 失误次数B--更新线上时删除
      */
-    // @ExcelProperty(value = "失误次数B")
     private Integer faultB;
+    /**
+     * 比赛完成用时
+     */
+    private BigDecimal completeTime;
 
     /**
      * 更新时间

+ 2 - 4
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/ScoreSheetDetailVo.java

@@ -21,10 +21,8 @@ public class ScoreSheetDetailVo implements Serializable {
     private Integer attemptIndex;
     /** 成绩值 */
     private String num;
-    /** 失误A */
-    private Integer shiwuA;
-    /** 失误B */
-    private Integer shiwuB;
+    /** 失误次数 */
+    private Integer shiwu;
     /** 运动员ID (逻辑关联使用) */
     private Long Id;
     /** 队伍ID (逻辑关联使用) */

+ 5 - 4
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/ScoreSheetItemVo.java

@@ -4,6 +4,7 @@ import lombok.Data;
 
 import java.io.Serial;
 import java.io.Serializable;
+import java.math.BigDecimal;
 import java.util.List;
 
 /**
@@ -30,10 +31,10 @@ public class ScoreSheetItemVo implements Serializable {
     private Long scoreId;
     /** 成绩 */
     private String score;
-    /** 失误次数A */
-    private Integer faultA;
-    /** 失误次数B */
-    private Integer faultB;
+    /** 失误次数 */
+    private Integer shiwu;
+    /** 比赛完成用时 */
+    private String yongshi;
 
     /** 成绩明细列表 */
     private List<ScoreSheetDetailVo> chengji;

+ 3 - 3
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/ScoreSheetVo.java

@@ -21,7 +21,7 @@ public class ScoreSheetVo implements Serializable {
     /** 成绩类型--字典game_score_type
      1-计时类;2-距离类;3-单次计数类;4-多次计数类;5-排名类;6-远度距离类;7-高度距离类  */
     private String chengjiType;
-    /** 计算规则 0-升序(1...9);1-降序(9...1);2-失误次数A(1...9);
+    /** 计算规则(game_order_type) 0-升序(1...9);1-降序(9...1);2-失误次数A(1...9);
      * 3-失误次数B(9...1);4-求和 ;5-最大值;6-最小值;7-平均值 */
     private String paiMing;
     /** 项目名称 */
@@ -36,9 +36,9 @@ public class ScoreSheetVo implements Serializable {
     private String sex;
     /** 参赛组别 */
     private String zu;
-    /** 比赛阶段 */
+    /** 比赛阶段(game_stage) */
     private String jieduan;
-    /** 比赛轮次 */
+    /** 比赛轮次(game_round) */
     private String lunci;
     /** 完赛人数/对数 */
     private Integer finishedParticipants;

+ 29 - 14
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameScoreServiceImpl.java

@@ -590,10 +590,8 @@ public class GameScoreServiceImpl implements IGameScoreService {
         String rule = project.getScoreRule();
 
         // 1. 汇总失误次数 (通常是所有轮次的累加)
-        int totalFaultA = details.stream().mapToInt(d -> d.getFaultA() != null ? d.getFaultA() : 0).sum();
-        int totalFaultB = details.stream().mapToInt(d -> d.getFaultB() != null ? d.getFaultB() : 0).sum();
-        bo.setFaultA(totalFaultA);
-        bo.setFaultB(totalFaultB);
+        int totalFault = details.stream().mapToInt(d -> d.getFaultA() != null ? d.getFaultA() : 0).sum();
+        bo.setFaultA(totalFault);
 
         // 2. 汇总主成绩
         List<BigDecimal> values = details.stream()
@@ -698,7 +696,6 @@ public class GameScoreServiceImpl implements IGameScoreService {
                 athleteScore.setIndividualPerformance(BigDecimal.ZERO);
                 athleteScore.setTeamPerformance(bo.getTeamPerformance());
                 athleteScore.setFaultA(bo.getFaultA());
-                athleteScore.setFaultB(bo.getFaultB());
                 athleteScore.setScoreType("team");
                 athleteScore.setStatusFlag("0");
                 athleteScore.setStatus("0");
@@ -749,6 +746,8 @@ public class GameScoreServiceImpl implements IGameScoreService {
         if (project == null)
             return false;
 
+        boolean isTiming = isTimingProject(project);
+
         String orderType = project.getOrderType() != null ? project.getOrderType() : "1";
         String scoreValueStr = project.getScoreValue();
         List<Integer> pointConfig = StringUtils.isEmpty(scoreValueStr) ? new ArrayList<>()
@@ -770,13 +769,13 @@ public class GameScoreServiceImpl implements IGameScoreService {
         if (ProjectClassification.SINGLE.getValue().equals(project.getClassification())) {
             // 排除零分
             // --- 个人项目排名 ---
-            allScores.sort((a, b) -> compareScores(a, b, orderType, ProjectClassification.SINGLE.getValue()));
+            allScores.sort((a, b) -> compareScores(a, b, orderType, ProjectClassification.SINGLE.getValue(), isTiming));
 
             int currentRank = 1;
             for (int i = 0; i < allScores.size(); i++) {
                 GameScoreVo current = allScores.get(i);
                 if (i > 0 && compareScores(current, allScores.get(i - 1), orderType,
-                        ProjectClassification.SINGLE.getValue()) != 0) {
+                        ProjectClassification.SINGLE.getValue(), isTiming) != 0) {
                     currentRank = i + 1;
                 }
 
@@ -804,7 +803,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
             sortedTeamIds.sort((id1, id2) -> {
                 GameScoreVo score1 = teamGroups.get(id1).get(0);
                 GameScoreVo score2 = teamGroups.get(id2).get(0);
-                return compareScores(score1, score2, orderType, ProjectClassification.TEAM.getValue());
+                return compareScores(score1, score2, orderType, ProjectClassification.TEAM.getValue(), isTiming);
             });
 
             // 3.3 分配名次并分发给队员
@@ -815,7 +814,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
                     GameScoreVo currentTeamScore = teamGroups.get(teamId).get(0);
                     GameScoreVo prevTeamScore = teamGroups.get(sortedTeamIds.get(i - 1)).get(0);
                     if (compareScores(currentTeamScore, prevTeamScore, orderType,
-                            ProjectClassification.TEAM.getValue()) != 0) {
+                            ProjectClassification.TEAM.getValue(), isTiming) != 0) {
                         currentRank = i + 1;
                     }
                 }
@@ -858,7 +857,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
     /**
      * 统一的分数比较器
      */
-    private int compareScores(GameScoreVo a, GameScoreVo b, String orderType, String classification) {
+    private int compareScores(GameScoreVo a, GameScoreVo b, String orderType, String classification, boolean isTiming) {
         // 第一级:主成绩 (0-个人, 1-团队)
         BigDecimal perfA = ProjectClassification.SINGLE.getValue().equals(classification) ? a.getIndividualPerformance()
                 : a.getTeamPerformance();
@@ -891,11 +890,27 @@ public class GameScoreServiceImpl implements IGameScoreService {
             if (res != 0)
                 return res;
         }
-
-        // 第三级:失误次数B (越多越好)
+        // 第二级:失误次数B (越多越好)
         if (orderType.contains("3")) {
-            res = Integer.compare(b.getFaultB() != null ? b.getFaultB() : 0,
-                    a.getFaultB() != null ? a.getFaultB() : 0);
+            res = Integer.compare(b.getFaultA() != null ? b.getFaultA() : 0,
+                    a.getFaultA() != null ? a.getFaultA() : 0);
+            if (res != 0)
+                return res;
+        }
+
+        // 第三级:比赛完成用时 (越短越好,仅计时类项目比较)
+        if (isTiming) {
+            BigDecimal timeA = a.getCompleteTime();
+            BigDecimal timeB = b.getCompleteTime();
+            if (timeA == null) timeA = BigDecimal.ZERO;
+            if (timeB == null) timeB = BigDecimal.ZERO;
+
+            boolean aIsZero = timeA.compareTo(BigDecimal.ZERO) == 0;
+            boolean bIsZero = timeB.compareTo(BigDecimal.ZERO) == 0;
+            if (aIsZero && !bIsZero) return 1;
+            if (!aIsZero && bIsZero) return -1;
+
+            res = timeA.compareTo(timeB);
             if (res != 0)
                 return res;
         }

+ 103 - 44
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/app/ToClientServiceImpl.java

@@ -286,12 +286,20 @@ public class ToClientServiceImpl implements IToClientService {
         if (vo == null) {
             throw new RuntimeException("该赛事下不存在此项目");
         }
-        List<SysDictDataVo> dictDataVos = dictService.selectDictDataByType("game_project_type");
-        if (!dictDataVos.isEmpty()){
-            dictDataVos.stream()
-                .filter(d -> d.getDictValue().equals(vo.getType()))
-                .findFirst().map(SysDictDataVo::getDictLabel).ifPresent(vo::setType);
-        }
+
+        // 批量查询字典数据,避免多次调用 Redis/MySQL 交互
+        Map<String, List<SysDictDataVo>> dictMap = dictService.selectDictDataByTypes(List.of(
+                "game_project_type", "game_score_type", "game_order_type",
+                "sys_group_sex", "game_stage", "game_round"
+        ));
+
+        vo.setType(getDictLabelFromMap(dictMap, "game_project_type", vo.getType()));
+        vo.setChengjiType(getDictLabelFromMap(dictMap, "game_score_type", vo.getChengjiType()));
+        vo.setPaiMing(getDictLabelFromMap(dictMap, "game_order_type", vo.getPaiMing()));
+        vo.setSex(getDictLabelFromMap(dictMap, "sys_group_sex", vo.getSex()));
+        vo.setJieduan(getDictLabelFromMap(dictMap, "game_stage", vo.getJieduan()));
+        vo.setLunci(getDictLabelFromMap(dictMap, "game_round", vo.getLunci()));
+
         // 2. 查询该项目下所有参赛人员和队伍信息 (包括已录入成绩和未录入的)
         List<ScoreSheetItemVo> items = baseMapper.selectScoreSheetItems(eventId, projectId);
 
@@ -300,50 +308,55 @@ public class ToClientServiceImpl implements IToClientService {
                 .eq(GameScoreDetail::getProjectId, projectId)
                 .eq(GameScoreDetail::getDelFlag, "0"));
 
-        if (!allDetails.isEmpty()) {
-            Map<Long, List<ScoreSheetDetailVo>> detailMap;
+        if (CollectionUtils.isNotEmpty(allDetails)) {
+            // 一次性转换所有 detail,避免在循环和分支中重复转换
+            List<ScoreSheetDetailVo> detailVos = allDetails.stream()
+                    .map(this::convertToDetailVo)
+                    .toList();
+
             if (ProjectClassification.TEAM.getValue().equals(vo.getClassification())) {
                 // 团体项目:按 teamId 分组
-                detailMap = allDetails.stream()
+                Map<Long, List<ScoreSheetDetailVo>> teamDetailMap = detailVos.stream()
                         .filter(d -> d.getTeamId() != null)
-                        .map(this::convertToDetailVo)
                         .collect(Collectors.groupingBy(ScoreSheetDetailVo::getTeamId));
 
-                items.forEach(item -> item.setChengji(detailMap.get(item.getTeamId())));
+                items.forEach(item -> item.setChengji(teamDetailMap.get(item.getTeamId())));
             } else {
-                // 个人项目:优先按 athleteId 分组 (如果存储时没存 athleteId 则按 scoreId)
-                detailMap = allDetails.stream()
-                        .filter(d -> d.getAthleteId() != null)
-                        .map(this::convertToDetailVo)
+                // 个人项目:优先按 athleteId (即 detailVo.id) 分组,同时生成按 scoreId 的分组以便 O(1) 匹配
+                Map<Long, List<ScoreSheetDetailVo>> athleteDetailMap = detailVos.stream()
+                        .filter(d -> d.getId() != null)
                         .collect(Collectors.groupingBy(ScoreSheetDetailVo::getId));
 
-                items.forEach(item -> item.setChengji(detailMap.get(item.getId())));
+                Map<Long, List<ScoreSheetDetailVo>> scoreDetailMap = detailVos.stream()
+                        .filter(d -> d.getScoreId() != null)
+                        .collect(Collectors.groupingBy(ScoreSheetDetailVo::getScoreId));
 
-                // 兜底:处理部分仅关联 scoreId 的旧数据
-                items.stream().filter(i -> i.getChengji() == null && i.getScoreId() != null).forEach(item -> {
-                    List<ScoreSheetDetailVo> scoreDetails = allDetails.stream()
-                            .filter(d -> Objects.equals(d.getScoreId(), item.getScoreId()))
-                            .map(this::convertToDetailVo)
-                            .toList();
-                    item.setChengji(scoreDetails);
+                items.forEach(item -> {
+                    List<ScoreSheetDetailVo> chengji = athleteDetailMap.get(item.getId());
+                    if (CollectionUtils.isEmpty(chengji) && item.getScoreId() != null) {
+                        // 兜底:处理部分仅关联 scoreId 的旧数据,使用预分组的 Map 进行 O(1) 匹配,消除了原来的 O(N*M) 过滤
+                        chengji = scoreDetailMap.get(item.getScoreId());
+                    }
+                    item.setChengji(chengji);
                 });
             }
         }
 
-        // 4. 处理计时格式转换
-        if (vo.getGuize() != null && StringUtils.isNotBlank(vo.getGuize())) {
+        // 4. 处理计时格式转换 (直接传入 String 进行安全解析)
+        if (StringUtils.isNotBlank(vo.getGuize())) {
             items.forEach(item -> {
                 // 格式化主成绩
-                if (StringUtils.isNotBlank(item.getScore())) {
-                    item.setScore(convertDecimalToTimeScore(new BigDecimal(item.getScore()), vo.getGuize()));
+                if (StringUtils.isNotBlank(item.getScore()) && vo.getChengjiType().contains("计时")) {
+                    item.setScore(convertDecimalToTimeScore(item.getScore(), vo.getGuize()));
+                }
+                if (StringUtils.isNotBlank(item.getYongshi())){
+                    item.setYongshi(convertDecimalToTimeScore(item.getYongshi(), vo.getGuize()));
                 }
                 // 格式化明细成绩
-                if (item.getChengji() != null) {
+                if (CollectionUtils.isNotEmpty(item.getChengji()) && vo.getChengjiType().contains("计时")) {
                     item.getChengji().forEach(detail -> {
                         if (StringUtils.isNotBlank(detail.getNum())) {
-                            detail.setNum(convertDecimalToTimeScore(
-                                    new BigDecimal(detail.getNum()),
-                                    vo.getGuize()));
+                            detail.setNum(convertDecimalToTimeScore(detail.getNum(), vo.getGuize()));
                         }
                     });
                 }
@@ -354,14 +367,31 @@ public class ToClientServiceImpl implements IToClientService {
         return vo;
     }
 
+    /**
+     * 从预查的字典Map中获取指定类型和值的字典标签,若未匹配则返回原值
+     */
+    private String getDictLabelFromMap(Map<String, List<SysDictDataVo>> dictMap, String dictType, String dictValue) {
+        if (StringUtils.isBlank(dictValue) || dictMap == null) {
+            return dictValue;
+        }
+        List<SysDictDataVo> list = dictMap.get(dictType);
+        if (CollectionUtils.isNotEmpty(list)) {
+            return list.stream()
+                    .filter(d -> dictValue.equals(d.getDictValue()))
+                    .findFirst()
+                    .map(SysDictDataVo::getDictLabel)
+                    .orElse(dictValue);
+        }
+        return dictValue;
+    }
+
     private ScoreSheetDetailVo convertToDetailVo(GameScoreDetail d) {
         ScoreSheetDetailVo dvo = new ScoreSheetDetailVo();
         dvo.setDetailId(d.getDetailId());
         dvo.setScoreId(d.getScoreId());
         dvo.setAttemptIndex(d.getAttemptIndex());
         dvo.setNum(d.getPerformanceValue() != null ? d.getPerformanceValue().toString() : null);
-        dvo.setShiwuA(d.getFaultA());
-        dvo.setShiwuB(d.getFaultB());
+        dvo.setShiwu(d.getFaultA());
         // 传递关联 ID 供分组使用
         dvo.setId(d.getAthleteId());
         dvo.setTeamId(d.getTeamId());
@@ -371,12 +401,24 @@ public class ToClientServiceImpl implements IToClientService {
     /**
      * 将小数格式的成绩转换成时间格式显示
      *
-     * @param decimalScore 以秒为单位的小数值
-     * @param format       格式 (HH:mm:ss.SSS 或 mm:ss.SSS)
+     * @param scoreStr 以秒为单位的小数值字符串
+     * @param format   格式 (HH:mm:ss.SSS 或 mm:ss.SSS)
      * @return 时间格式字符串
      */
-    private String convertDecimalToTimeScore(BigDecimal decimalScore, String format) {
-        if (decimalScore == null || decimalScore.compareTo(BigDecimal.ZERO) < 0) {
+    private String convertDecimalToTimeScore(String scoreStr, String format) {
+        if (StringUtils.isBlank(scoreStr)) {
+            return "0".equals(format) ? "00:00:00.000" : "00:00.000";
+        }
+
+        BigDecimal decimalScore;
+        try {
+            decimalScore = new BigDecimal(scoreStr.trim());
+        } catch (Exception e) {
+            log.warn("成绩数字解析失败: scoreStr={}", scoreStr, e);
+            return scoreStr; // 解析失败安全降级,返回原字符串而非崩溃
+        }
+
+        if (decimalScore.compareTo(BigDecimal.ZERO) < 0) {
             return "0".equals(format) ? "00:00:00.000" : "00:00.000";
         }
 
@@ -397,7 +439,7 @@ public class ToClientServiceImpl implements IToClientService {
             }
         } catch (Exception e) {
             log.warn("转换成绩格式失败: score={}, format={}", decimalScore, format, e);
-            return decimalScore.toString();
+            return scoreStr;
         }
     }
 
@@ -418,6 +460,24 @@ public class ToClientServiceImpl implements IToClientService {
             return;
         }
 
+        // 校验成绩是否为空
+        for (ScoreSubmitItemBo item : items) {
+            String displayName = StringUtils.isNotBlank(item.getName()) ? item.getName() :
+                    (StringUtils.isNotBlank(item.getNum()) ? item.getNum() : "未知选手");
+            if (CollectionUtils.isNotEmpty(item.getChengji())) {
+                for (ScoreSubmitDetailBo detail : item.getChengji()) {
+                    if (detail.getNum() == null) {
+                        String attemptStr = detail.getAttemptIndex() != null ? "第 " + detail.getAttemptIndex() + " 轮" : "明细";
+                        throw new RuntimeException("提交失败:" + displayName + " 的" + attemptStr + "成绩不能为空");
+                    }
+                }
+            } else {
+                if (StringUtils.isBlank(item.getScore())) {
+                    throw new RuntimeException("提交失败:" + displayName + " 的成绩不能为空");
+                }
+            }
+        }
+
         // 1. 批量处理队伍 (缺失则新增)
         Map<String, Long> teamIdMap = new HashMap<>();
         Set<String> missingTeamNames = items.stream()
@@ -515,8 +575,8 @@ public class ToClientServiceImpl implements IToClientService {
             scoreBo.setProjectId(projectId);
             scoreBo.setAthleteId(athleteId);
             scoreBo.setTeamId(teamId);
-            scoreBo.setFaultA(item.getFaultA());
-            scoreBo.setFaultB(item.getFaultB());
+            scoreBo.setFaultA(item.getShiwu());
+            scoreBo.setCompleteTime(item.getYongshi());
 
             // 统一模式:优先处理明细列表
             if (CollectionUtils.isNotEmpty(item.getChengji())) {
@@ -525,8 +585,7 @@ public class ToClientServiceImpl implements IToClientService {
                     dbo.setDetailId(d.getDetailId());
                     dbo.setAttemptIndex(d.getAttemptIndex());
                     dbo.setPerformanceValue(d.getNum());
-                    dbo.setFaultA(d.getShiwuA());
-                    dbo.setFaultB(d.getShiwuB());
+                    dbo.setFaultA(d.getShiwu());
                     return dbo;
                 }).toList();
                 scoreBo.setDetails(details);
@@ -608,13 +667,13 @@ public class ToClientServiceImpl implements IToClientService {
             list.forEach(item -> {
                 if (StringUtils.isNotBlank(item.getScore())) {
                     item.setScore(
-                            convertDecimalToTimeScore(new BigDecimal(item.getScore()), project.getTimingFormat()));
+                            convertDecimalToTimeScore(item.getScore(), project.getTimingFormat()));
                 }
                 if (item.getChengji() != null) {
                     item.getChengji().forEach(detail -> {
                         if (StringUtils.isNotBlank(detail.getNum())) {
                             detail.setNum(convertDecimalToTimeScore(
-                                    new BigDecimal(detail.getNum()),
+                                    detail.getNum(),
                                     project.getTimingFormat()));
                         }
                     });

+ 2 - 2
ruoyi-modules/ruoyi-game-event/src/main/resources/mapper/system/app/ToClientMapper.xml

@@ -57,8 +57,8 @@
                 WHEN p.classification = '0' THEN s.individual_performance
                 ELSE s.team_performance
             END as score,
-            s.fault_a as faultA,
-            s.fault_b as faultB
+            s.fault_a as shiwu,
+            s.complete_time as yongshi
         FROM game_athlete a
         LEFT JOIN game_team t ON a.team_id = t.team_id AND t.del_flag = '0'
         CROSS JOIN game_event_project p ON p.project_id = #{projectId}

+ 8 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysDictTypeService.java

@@ -92,4 +92,12 @@ public interface ISysDictTypeService {
      * @return 结果
      */
     boolean checkDictTypeUnique(SysDictTypeBo dictType);
+
+    /**
+     * 批量根据字典类型查询字典数据
+     *
+     * @param dictTypes 字典类型列表
+     * @return 字典数据集合映射Map
+     */
+    java.util.Map<String, List<SysDictDataVo>> selectDictDataByTypes(List<String> dictTypes);
 }

+ 20 - 0
ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysDictTypeServiceImpl.java

@@ -294,4 +294,24 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
         return BeanUtil.copyToList(list, DictDataDTO.class);
     }
 
+    /**
+     * 批量根据字典类型查询字典数据
+     *
+     * @param dictTypes 字典类型列表
+     * @return 字典数据集合映射Map
+     */
+    @Override
+    public Map<String, List<SysDictDataVo>> selectDictDataByTypes(List<String> dictTypes) {
+        if (CollUtil.isEmpty(dictTypes)) {
+            return Collections.emptyMap();
+        }
+        Map<String, List<SysDictDataVo>> resultMap = new HashMap<>();
+        ISysDictTypeService proxy = SpringUtils.getAopProxy(this);
+        for (String dictType : dictTypes) {
+            List<SysDictDataVo> data = proxy.selectDictDataByType(dictType);
+            resultMap.put(dictType, data != null ? data : Collections.emptyList());
+        }
+        return resultMap;
+    }
+
 }