Browse Source

feat(game): 实现计时类项目成绩格式转换功能

- 新增计时类项目判断逻辑,根据项目计算规则识别计时项目
- 实现时间格式成绩与小数格式之间的双向转换方法
- 在成绩上传和展示环节集成时间格式转换处理
- 完善分组分配算法,区分个人项目和团体项目的分配规则
- 增加数据权限控制,确保赛事数据访问安全- 优化成绩展示逻辑,计时项目自动格式化显示时间格式
- 添加详细的日志记录,便于追踪分组分配过程和问题排查
zhou 1 month ago
parent
commit
cc2f563015

+ 66 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/GameEventMapper.java

@@ -1,10 +1,18 @@
 package org.dromara.system.mapper;
 
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.dromara.common.mybatis.annotation.DataColumn;
+import org.dromara.common.mybatis.annotation.DataPermission;
 import org.dromara.system.domain.GameEvent;
 import org.dromara.system.domain.vo.GameEventVo;
 import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
 
+import java.util.List;
+
 /**
  * 赛事基本信息Mapper接口
  *
@@ -14,4 +22,62 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
 @Mapper
 public interface GameEventMapper extends BaseMapperPlus<GameEvent, GameEventVo> {
 
+    /**
+     * 分页查询赛事列表,并进行数据权限控制
+     * 注意:如果赛事表没有部门字段,需要根据业务需求调整
+     * 例如:如果赛事通过创建人关联,可以使用 userName 来控制
+     * 如果赛事通过部门关联,可以使用 deptName 来控制
+     *
+     * @param page         分页参数
+     * @param queryWrapper 查询条件
+     * @return 分页的赛事信息
+     */
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default Page<GameEventVo> selectPageEventList(Page<GameEvent> page, Wrapper<GameEvent> queryWrapper) {
+        return this.selectVoPage(page, queryWrapper);
+    }
+
+    /**
+     * 查询赛事列表,并进行数据权限控制
+     *
+     * @param queryWrapper 查询条件
+     * @return 赛事信息集合
+     */
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    default List<GameEventVo> selectEventList(Wrapper<GameEvent> queryWrapper) {
+        return this.selectVoList(queryWrapper);
+    }
+
+    /**
+     * 更新赛事数据,并进行数据权限控制
+     *
+     * @param event         要更新的赛事实体
+     * @param updateWrapper 更新条件封装器
+     * @return 更新操作影响的行数
+     */
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    int update(@Param(Constants.ENTITY) GameEvent event, @Param(Constants.WRAPPER) Wrapper<GameEvent> updateWrapper);
+
+    /**
+     * 根据ID更新赛事数据,并进行数据权限控制
+     *
+     * @param event 要更新的赛事实体
+     * @return 更新操作影响的行数
+     */
+    @Override
+    @DataPermission({
+        @DataColumn(key = "deptName", value = "create_dept"),
+        @DataColumn(key = "userName", value = "create_by")
+    })
+    int updateById(@Param(Constants.ENTITY) GameEvent event);
 }

+ 121 - 15
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameEventGroupServiceImpl.java

@@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.InitializingBean;
 import org.dromara.system.domain.bo.GameEventGroupBo;
 import org.dromara.system.domain.constant.GameEventConstant;
+import org.dromara.system.domain.constant.ProjectClassification;
 import org.dromara.system.domain.GameEventGroup;
 import org.dromara.system.mapper.GameEventGroupMapper;
 import jakarta.annotation.PostConstruct;
@@ -241,6 +242,7 @@ public class GameEventGroupServiceImpl implements IGameEventGroupService {
         if (isValid) {
             //TODO 做一些业务上的校验,判断是否需要校验
         }
+        ids.forEach(id -> gameAthleteCompetitionGroupService.deleteGroupResultByGroupId(id));
         return baseMapper.deleteByIds(ids) > 0;
     }
 
@@ -342,8 +344,7 @@ public class GameEventGroupServiceImpl implements IGameEventGroupService {
                         .filter(g -> g.getDictLabel().equals("混合"))
                         .map(SysDictDataVo::getDictValue)
                         .findFirst();
-                    if (StringUtils.isNotBlank(groupInfo.getMemberGender()) &&
-                        !mixGender.equals(groupInfo.getMemberGender())) {
+                    if (StringUtils.isNotBlank(groupInfo.getMemberGender())) {
                         if (!groupInfo.getMemberGender().equals(
                             athlete.getGender() != null ? athlete.getGender().toString() : null)) {
                             return false;
@@ -394,19 +395,31 @@ public class GameEventGroupServiceImpl implements IGameEventGroupService {
                             continue;
                         }
 
-                        // 检查同一组中是否已有同一队伍的运动员
-                        boolean hasSameTeamInGroup = groupResult.entrySet().stream()
-                            .anyMatch(entry -> {
-                                String[] keyParts = entry.getKey().split("-");
-                                if (keyParts.length >= 1) {
-                                    String existingGroup = keyParts[0];
-                                    return existingGroup.equals(String.valueOf(currentGroupIndex)) &&
-                                           entry.getValue().getTeamId().equals(candidateAthlete.getTeamId());
-                                }
-                                return false;
-                            });
-
-                        if (!hasSameTeamInGroup) {
+                        // 只有项目属于团体项目才检查同一组中是否已有同一队伍的运动员
+                        // 个人项目(classification="0")不限制同一队伍,团体项目(classification="1")才限制
+                        boolean isValidCandidate = true;
+                        if (project != null && ProjectClassification.TEAM.getValue().equals(project.getClassification())) {
+                            // 团体项目:检查同一组中是否已有同一队伍的运动员,避免同一队伍在同一组中重复分配
+                            boolean hasSameTeamInGroup = groupResult.entrySet().stream()
+                                .anyMatch(entry -> {
+                                    String[] keyParts = entry.getKey().split("-");
+                                    if (keyParts.length >= 1) {
+                                        String existingGroup = keyParts[0];
+                                        return existingGroup.equals(String.valueOf(currentGroupIndex)) &&
+                                               entry.getValue().getTeamId() != null &&
+                                               entry.getValue().getTeamId().equals(candidateAthlete.getTeamId());
+                                    }
+                                    return false;
+                                });
+                            if (hasSameTeamInGroup) {
+                                isValidCandidate = false;
+                                log.debug("第{}组拒绝运动员{}:团体项目中同一组不能有同一队伍({})的运动员",
+                                    currentGroupIndex, candidateAthlete.getAthleteCode(), candidateAthlete.getTeamId());
+                            }
+                        }
+                        // 个人项目(classification="0"或null):允许同一组中有同一队伍的运动员,不进行队伍限制检查
+
+                        if (isValidCandidate) {
                             selectedAthlete = candidateAthlete;
                             // 标记运动员为已分配
                             assignedAthleteIds.add(candidateAthlete.getAthleteId());
@@ -419,10 +432,103 @@ public class GameEventGroupServiceImpl implements IGameEventGroupService {
                     if (selectedAthlete != null) {
                         String key = currentGroupIndex + "-" + track;
                         groupResult.put(key, selectedAthlete);
+                        log.debug("分配成功 - 第{}组第{}道: 运动员编号={}, 姓名={}, 队伍={}",
+                            currentGroupIndex, track, selectedAthlete.getAthleteCode(),
+                            selectedAthlete.getName(), selectedAthlete.getTeamName());
+                    } else {
+                        // 检查是否有可用但未分配的运动员
+                        long availableCount = eligibleAthletes.stream()
+                            .filter(a -> !assignedAthleteIds.contains(a.getAthleteId()))
+                            .count();
+                        if (availableCount > 0) {
+                            log.warn("第{}组第{}道未找到合适的运动员,但仍有{}个符合条件的运动员未分配",
+                                currentGroupIndex, track, availableCount);
+                        } else {
+                            log.warn("第{}组第{}道未找到合适的运动员,所有符合条件的运动员都已被分配",
+                                currentGroupIndex, track);
+                        }
                     }
                 }
             }
 
+            // 记录所有运动员的分配情况
+            log.info("========== 分组分配详情 - 分组ID: {} ==========", groupId);
+            log.info("符合条件的运动员总数: {}", eligibleAthletes.size());
+
+            // 记录已分配的运动员
+            log.info("【已分配的运动员】");
+            groupResult.forEach((key, athlete) -> {
+                log.info("  已分配 - 位置: {}, 运动员编号: {}, 姓名: {}, 队伍ID: {}, 队伍名称: {}",
+                    key, athlete.getAthleteCode(), athlete.getName(),
+                    athlete.getTeamId(), athlete.getTeamName());
+            });
+            log.info("已分配运动员数量: {}", groupResult.size());
+
+            // 找出并记录未分配的运动员及原因
+            log.info("【未分配的运动员】");
+            List<GameAthleteVo> unassignedAthletes = eligibleAthletes.stream()
+                .filter(athlete -> !assignedAthleteIds.contains(athlete.getAthleteId()))
+                .toList();
+
+            if (unassignedAthletes.isEmpty()) {
+                log.info("  所有符合条件的运动员都已成功分配");
+            } else {
+                // 计算需要的道次总数(每组人数 × 组数,但不超过道数)
+                int personsPerGroup = groupInfo.getPersonNum() != null &&
+                    groupInfo.getPersonNum() <= groupInfo.getTrackNum()
+                        ? groupInfo.getPersonNum().intValue()
+                        : groupInfo.getTrackNum().intValue();
+                Long totalTracksNeeded = groupInfo.getIncludeGroupNum() * personsPerGroup;
+
+                for (GameAthleteVo athlete : unassignedAthletes) {
+                    String reason;
+
+                    // 分析未分配原因
+                    if (groupResult.size() >= totalTracksNeeded) {
+                        reason = String.format("道次已满(需要%d个道次,已分配%d个运动员)", totalTracksNeeded, groupResult.size());
+                    } else if (project != null && ProjectClassification.TEAM.getValue().equals(project.getClassification())) {
+                        // 团体项目:分析每个组中是否有该队伍
+                        Map<Integer, Set<Long>> groupTeamMap = new HashMap<>();
+                        groupResult.forEach((key, assignedAthlete) -> {
+                            String[] keyParts = key.split("-");
+                            if (keyParts.length >= 1) {
+                                int group = Integer.parseInt(keyParts[0]);
+                                groupTeamMap.computeIfAbsent(group, k -> new HashSet<>()).add(assignedAthlete.getTeamId());
+                            }
+                        });
+
+                        // 检查该运动员的队伍是否在每个组中都已存在
+                        boolean blockedByTeamRule = false;
+                        StringBuilder groupDetails = new StringBuilder();
+                        for (int g = 1; g <= groupInfo.getIncludeGroupNum(); g++) {
+                            Set<Long> teamsInGroup = groupTeamMap.getOrDefault(g, new HashSet<>());
+                            if (teamsInGroup.contains(athlete.getTeamId())) {
+                                blockedByTeamRule = true;
+                                if (!groupDetails.isEmpty()) groupDetails.append("、");
+                                groupDetails.append("第").append(g).append("组");
+                            }
+                        }
+
+                        if (blockedByTeamRule) {
+                            reason = String.format("团体项目限制:队伍(%s)在第%s组已有运动员,同组不能重复分配",
+                                athlete.getTeamName(), groupDetails.toString());
+                        } else {
+                            reason = "未知原因:可能是分配算法逻辑问题";
+                        }
+                    } else {
+                        // 个人项目:道次未满但没有分配,可能是算法问题
+                        reason = String.format("道次未满(已分配%d/%d个道次),可能是分配算法限制",
+                            groupResult.size(), totalTracksNeeded);
+                    }
+
+                    log.warn("  未分配 - 运动员编号: {}, 姓名: {}, 队伍ID: {}, 队伍名称: {}, 原因: {}",
+                        athlete.getAthleteCode(), athlete.getName(),
+                        athlete.getTeamId(), athlete.getTeamName(), reason);
+                }
+                log.info("未分配运动员数量: {}", unassignedAthletes.size());
+            }
+            log.info("==========================================");
+
             // 保存分组结果到数据库
             try {
                 boolean saveSuccess = getGameAthleteCompetitionGroupService().saveGroupResult(groupId, groupResult, groupInfo);

+ 4 - 2
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameEventServiceImpl.java

@@ -133,7 +133,8 @@ public class GameEventServiceImpl implements IGameEventService {
     @Override
     public TableDataInfo<GameEventVo> queryPageList(GameEventBo bo, PageQuery pageQuery) {
         LambdaQueryWrapper<GameEvent> lqw = buildQueryWrapper(bo);
-        Page<GameEventVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        // 使用带数据权限注解的方法
+        Page<GameEventVo> result = baseMapper.selectPageEventList(pageQuery.build(), lqw);
         return TableDataInfo.build(result);
     }
 
@@ -146,7 +147,8 @@ public class GameEventServiceImpl implements IGameEventService {
     @Override
     public List<GameEventVo> queryList(GameEventBo bo) {
         LambdaQueryWrapper<GameEvent> lqw = buildQueryWrapper(bo);
-        return baseMapper.selectVoList(lqw);
+        // 使用带数据权限注解的方法
+        return baseMapper.selectEventList(lqw);
     }
 
     private LambdaQueryWrapper<GameEvent> buildQueryWrapper(GameEventBo bo) {

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

@@ -33,6 +33,8 @@ import java.util.stream.Collectors;
 import jakarta.servlet.http.HttpServletResponse;
 import org.dromara.system.domain.bo.GameEventProjectBo;
 import org.dromara.system.domain.bo.GameTeamBo;
+import org.dromara.system.service.ISysDictTypeService;
+import org.dromara.system.domain.vo.SysDictDataVo;
 
 import java.net.URLEncoder;
 
@@ -57,6 +59,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
     private final GameAthleteMapper gameAthleteMapper;
     private final GameEventProjectMapper projectMapper;
     private final IGameEventProjectService gameEventProjectService;
+    private final ISysDictTypeService dictTypeService;
 
 
     /**
@@ -331,6 +334,9 @@ public class GameScoreServiceImpl implements IGameScoreService {
         log.info("开始获取个人项目数据: eventId={}, projectId={}, searchValue={}", eventId, projectId, searchValue);
 
         List<Map<String, Object>> resultList = new ArrayList<>();
+        // 获取项目信息,判断是否为计时类项目
+        GameEventProjectVo project = gameEventProjectService.queryById(projectId);
+        boolean isTiming = isTimingProject(project);
 
         // 查询参与该项目的运动员
         List<GameAthleteVo> athletes = gameAthleteService.queryListByEventIdAndProjectId(eventId, projectId, searchValue);
@@ -369,7 +375,13 @@ public class GameScoreServiceImpl implements IGameScoreService {
             if (score != null) {
                 data.put("scoreId", score.getScoreId());
                 data.put("scorePoint", score.getScorePoint());
-                data.put("individualPerformance", score.getIndividualPerformance());
+                // 如果是计时类项目,转换为时间格式显示
+                if (isTiming && score.getIndividualPerformance() != null) {
+                    String timeFormat = convertDecimalToTimeScore(score.getIndividualPerformance());
+                    data.put("individualPerformance", timeFormat != null ? timeFormat : score.getIndividualPerformance());
+                } else {
+                    data.put("individualPerformance", score.getIndividualPerformance());
+                }
                 data.put("teamPerformance", score.getTeamPerformance());
                 data.put("scoreRank", score.getScoreRank());
                 data.put("updateTime", score.getUpdateTime());
@@ -377,7 +389,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
             } else {
                 data.put("scoreId", 0);
                 data.put("scorePoint", 0);
-                data.put("individualPerformance", 0.0);
+                data.put("individualPerformance", isTiming ? "00:00:00.000" : 0.0);
                 data.put("teamPerformance", 0.0);
                 data.put("scoreRank", 0);
                 data.put("updateTime", "");
@@ -424,18 +436,28 @@ public class GameScoreServiceImpl implements IGameScoreService {
 
             // 查询成绩信息(使用第一个运动员的成绩作为队伍成绩)
             GameScoreVo score = getScoreByAthleteIdAndProjectId(athlete.getAthleteId(), projectId);
+            // 获取项目信息,判断是否为计时类项目
+            GameEventProjectVo project = gameEventProjectService.queryById(projectId);
+            boolean isTiming = isTimingProject(project);
+            
             if (score != null) {
                 data.put("scoreId", score.getScoreId());
                 data.put("scorePoint", score.getScorePoint());
                 data.put("individualPerformance", score.getIndividualPerformance());
-                data.put("teamPerformance", score.getTeamPerformance());
+                // 如果是计时类项目,转换为时间格式显示
+                if (isTiming && score.getTeamPerformance() != null) {
+                    String timeFormat = convertDecimalToTimeScore(score.getTeamPerformance());
+                    data.put("teamPerformance", timeFormat != null ? timeFormat : score.getTeamPerformance());
+                } else {
+                    data.put("teamPerformance", score.getTeamPerformance());
+                }
                 data.put("scoreRank", score.getScoreRank());
                 data.put("updateTime", score.getUpdateTime());
             } else {
                 data.put("scoreId", 0);
                 data.put("scorePoint", 0);
                 data.put("individualPerformance", 0.0);
-                data.put("teamPerformance", 0.0);
+                data.put("teamPerformance", isTiming ? "00:00:00.000" : 0.0);
                 data.put("scoreRank", 0);
                 data.put("updateTime", "");
             }
@@ -987,9 +1009,14 @@ public class GameScoreServiceImpl implements IGameScoreService {
                     vo.setClassification("");
                 }
 
-                // 设置成绩信息
-                vo.setIndividualPerformance(convertToBigDecimal(score.getIndividualPerformance()));
-                vo.setTeamPerformance(convertToBigDecimal(score.getTeamPerformance()));
+                // 设置成绩信息 - 如果是计时类项目,需要特殊处理
+                BigDecimal individualPerf = convertToBigDecimal(score.getIndividualPerformance());
+                BigDecimal teamPerf = convertToBigDecimal(score.getTeamPerformance());
+                
+                // 注意:AppScoreVo 的字段类型是 BigDecimal,但如果是计时类项目,我们仍然存储 BigDecimal
+                // 前端可以根据项目类型决定如何显示
+                vo.setIndividualPerformance(individualPerf);
+                vo.setTeamPerformance(teamPerf);
                 vo.setScorePoint(score.getScorePoint() != null ? score.getScorePoint() : 0);
                 vo.setScoreRank(score.getScoreRank() != null ? score.getScoreRank() : 0);
 
@@ -1583,6 +1610,9 @@ public class GameScoreServiceImpl implements IGameScoreService {
      */
     private List<Map<String, Object>> getIndividualProjectDataAll(Long eventId, Long projectId) {
         List<Map<String, Object>> resultList = new ArrayList<>();
+        // 获取项目信息,判断是否为计时类项目
+        GameEventProjectVo project = gameEventProjectService.queryById(projectId);
+        boolean isTiming = isTimingProject(project);
 
         // 查询参与该项目的所有运动员(不分页)
         List<GameAthleteVo> athletes = gameAthleteService.queryListByEventIdAndProjectId(eventId, projectId, null);
@@ -1615,7 +1645,13 @@ public class GameScoreServiceImpl implements IGameScoreService {
             if (score != null) {
                 data.put("scoreId", score.getScoreId());
                 data.put("scorePoint", score.getScorePoint());
-                data.put("individualPerformance", score.getIndividualPerformance());
+                // 如果是计时类项目,转换为时间格式显示
+                if (isTiming && score.getIndividualPerformance() != null) {
+                    String timeFormat = convertDecimalToTimeScore(score.getIndividualPerformance());
+                    data.put("individualPerformance", timeFormat != null ? timeFormat : score.getIndividualPerformance());
+                } else {
+                    data.put("individualPerformance", score.getIndividualPerformance());
+                }
                 data.put("teamPerformance", score.getTeamPerformance());
                 data.put("scoreRank", score.getScoreRank());
                 data.put("updateTime", score.getUpdateTime());
@@ -1623,7 +1659,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
                 // 设置默认值
                 data.put("scoreId", 0);
                 data.put("scorePoint", 0);
-                data.put("individualPerformance", 0.0);
+                data.put("individualPerformance", isTiming ? "00:00:00.000" : 0.0);
                 data.put("teamPerformance", 0.0);
                 data.put("scoreRank", 0);
                 data.put("updateTime", "");
@@ -1642,6 +1678,10 @@ public class GameScoreServiceImpl implements IGameScoreService {
         List<Map<String, Object>> resultList = new ArrayList<>();
         Set<Long> processedTeamIds = new HashSet<>();
 
+        // 获取项目信息,判断是否为计时类项目
+        GameEventProjectVo project = gameEventProjectService.queryById(projectId);
+        boolean isTiming = isTimingProject(project);
+
         // 查询参与该项目的所有运动员
         List<GameAthleteVo> athletes = gameAthleteService.queryListByEventIdAndProjectId(eventId, projectId, null);
 
@@ -1671,14 +1711,20 @@ public class GameScoreServiceImpl implements IGameScoreService {
                 data.put("scoreId", score.getScoreId());
                 data.put("scorePoint", score.getScorePoint());
                 data.put("individualPerformance", score.getIndividualPerformance());
-                data.put("teamPerformance", score.getTeamPerformance());
+                // 如果是计时类项目,转换为时间格式显示
+                if (isTiming && score.getTeamPerformance() != null) {
+                    String timeFormat = convertDecimalToTimeScore(score.getTeamPerformance());
+                    data.put("teamPerformance", timeFormat != null ? timeFormat : score.getTeamPerformance());
+                } else {
+                    data.put("teamPerformance", score.getTeamPerformance());
+                }
                 data.put("scoreRank", score.getScoreRank());
                 data.put("updateTime", score.getUpdateTime());
             } else {
                 data.put("scoreId", 0);
                 data.put("scorePoint", 0);
                 data.put("individualPerformance", 0.0);
-                data.put("teamPerformance", 0.0);
+                data.put("teamPerformance", isTiming ? "00:00:00.000" : 0.0);
                 data.put("scoreRank", 0);
                 data.put("updateTime", "");
             }
@@ -1832,4 +1878,87 @@ public class GameScoreServiceImpl implements IGameScoreService {
         String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
         response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx");
     }
+
+    /**
+     * 判断项目是否为计时类项目
+     * 根据项目的计算规则(scoreRule)判断
+     *
+     * @param project 项目信息
+     * @return true表示是计时类项目,false表示不是
+     */
+    private boolean isTimingProject(GameEventProjectVo project) {
+        if (project == null || project.getScoreRule() == null) {
+            return false;
+        }
+        try {
+            List<SysDictDataVo> gameScoreType = dictTypeService.selectDictDataByType("game_score_type");
+            String scoreRule = project.getScoreRule().toLowerCase();
+            Optional<String> rule = gameScoreType.stream()
+                .filter(data -> data.getDictLabel().contains("计时"))
+                .map(SysDictDataVo::getDictValue)
+                .findFirst();
+            // 判断计算规则是否为计时类
+            return rule.isPresent() && scoreRule.equals(rule.get().toLowerCase());
+        } catch (Exception e) {
+            log.warn("判断项目是否为计时类失败: {}", project.getProjectId(), e);
+            return false;
+        }
+    }
+
+    /**
+     * 将小数格式的成绩转换成时间格式显示(时:分:秒.毫秒)
+     * 格式:xx:xx:xx.nnn (时:分:秒.毫秒)
+     *
+     * @param decimalScore 以秒为单位的小数值
+     * @return 时间格式字符串,如 "01:23:45.678"
+     */
+    private String convertDecimalToTimeScore(BigDecimal decimalScore) {
+        if (decimalScore == null || decimalScore.compareTo(BigDecimal.ZERO) < 0) {
+            return null;
+        }
+
+        try {
+            // 计算小时
+            BigDecimal hoursBigDecimal = decimalScore.divide(BigDecimal.valueOf(3600), 0, java.math.RoundingMode.DOWN);
+            int hours = hoursBigDecimal.intValue();
+            BigDecimal remaining = decimalScore.remainder(BigDecimal.valueOf(3600));
+
+            // 计算分钟
+            BigDecimal minutesBigDecimal = remaining.divide(BigDecimal.valueOf(60), 0, java.math.RoundingMode.DOWN);
+            int minutes = minutesBigDecimal.intValue();
+            remaining = remaining.remainder(BigDecimal.valueOf(60));
+
+            // 秒和毫秒
+            int seconds = remaining.intValue();
+            // 计算毫秒:取小数部分乘以1000并四舍五入
+            BigDecimal millisecondsBigDecimal = remaining.subtract(BigDecimal.valueOf(seconds))
+                    .multiply(BigDecimal.valueOf(1000))
+                    .setScale(0, java.math.RoundingMode.HALF_UP);
+            int milliseconds = millisecondsBigDecimal.intValue();
+
+            // 处理毫秒溢出(理论上不应该发生,但保险起见)
+            if (milliseconds >= 1000) {
+                seconds += milliseconds / 1000;
+                milliseconds = milliseconds % 1000;
+            }
+
+            // 处理秒溢出(理论上不应该发生,但保险起见)
+            if (seconds >= 60) {
+                minutes += seconds / 60;
+                seconds = seconds % 60;
+            }
+
+            // 处理分钟溢出(理论上不应该发生,但保险起见)
+            if (minutes >= 60) {
+                hours += minutes / 60;
+                minutes = minutes % 60;
+            }
+
+            // 格式化输出:时:分:秒.毫秒
+            return String.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds);
+        } catch (Exception e) {
+            log.warn("转换小数为时间格式失败: {}", decimalScore, e);
+            return null;
+        }
+    }
 }

+ 183 - 25
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/app/ElectrometerServiceImpl.java

@@ -61,7 +61,7 @@ public class ElectrometerServiceImpl implements IElectrometerService {
         // 2. 查询项目信息
         LambdaQueryWrapper<GameEventProject> projectWrapper = Wrappers.lambdaQuery(GameEventProject.class)
                 .eq(GameEventProject::getEventId, eventId)
-                .eq(GameEventProject::getScoreRule,tjsdm);
+                .eq(GameEventProject::getProjectType,tjsdm);
 
         List<GameEventProject> projects = gameEventProjectMapper.selectList(projectWrapper);
         Map<Long, GameEventProject> projectMap = projects.stream()
@@ -70,7 +70,8 @@ public class ElectrometerServiceImpl implements IElectrometerService {
         // 3. 查询运动员比赛分组信息
         List<Long> athleteIds = athletes.stream().map(GameAthlete::getAthleteId).collect(Collectors.toList());
         LambdaQueryWrapper<GameAthleteCompetitionGroup> groupWrapper = Wrappers.lambdaQuery(GameAthleteCompetitionGroup.class)
-                .in(GameAthleteCompetitionGroup::getAthleteId, athleteIds);
+                .in(GameAthleteCompetitionGroup::getAthleteId, athleteIds)
+            .in(GameAthleteCompetitionGroup::getProjectId, projects.stream().map(GameEventProject::getProjectId).collect(Collectors.toList()));
         List<GameAthleteCompetitionGroup> competitionGroups = gameAthleteCompetitionGroupMapper.selectList(groupWrapper);
         // 支持一个运动员对应多个分组
         Map<Long, List<GameAthleteCompetitionGroup>> groupMap = competitionGroups.stream()
@@ -90,10 +91,10 @@ public class ElectrometerServiceImpl implements IElectrometerService {
         }
 
         // 5. 组装数据
-        List<SysDictDataVo> data = dictTypeService.selectDictDataByType("game_stage");
 //        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
+        List<SysDictDataVo> gameStage = dictTypeService.selectDictDataByType("game_stage");
         List<ElectrometerDataItemVo> result = new ArrayList<>();
-        
+
         for (GameAthlete athlete : athletes) {
             // 获取该运动员的所有分组
             List<GameAthleteCompetitionGroup> athleteGroups = groupMap.get(athlete.getAthleteId());
@@ -130,7 +131,7 @@ public class ElectrometerServiceImpl implements IElectrometerService {
                         item.setXmMc(project.getProjectName());
                         item.setTjsDm(project.getProjectType());
                         item.setSx(project.getScoreRule()); // 计算规则-项目属性
-                        Optional<String> label = data.stream()
+                        Optional<String> label = gameStage.stream()
                             .filter(d -> d.getDictValue().equals(project.getGameStage()))
                             .map(SysDictDataVo::getDictLabel)
                             .findFirst();
@@ -144,7 +145,7 @@ public class ElectrometerServiceImpl implements IElectrometerService {
                 }
             }
         }
-        
+
         return result;
     }
 
@@ -191,8 +192,30 @@ public class ElectrometerServiceImpl implements IElectrometerService {
                 if (!"OK".equals(validationResult)) {
                     return validationResult;
                 }
-                if (saveScoreRecord(eventId, project, scoreItem)) {
-                    successCount++;
+                // 判断是否为计时类项目,如果是则应用时间格式转换
+                if (isTimingProject(project)) {
+                    // 将成绩从字符串转换为时间格式再转换为小数
+                    String scoreStr = scoreItem.getScore() != null ? scoreItem.getScore().toString() : null;
+                    if (scoreStr != null && !scoreStr.isEmpty()) {
+                        BigDecimal convertedScore = convertTimeScoreToDecimal(scoreStr);
+                        if (convertedScore != null) {
+                            ElectrometerScoreUploadVo convertedItem = new ElectrometerScoreUploadVo();
+                            convertedItem.setAthleteCode(scoreItem.getAthleteCode());
+                            convertedItem.setTrackIndex(scoreItem.getTrackIndex());
+                            convertedItem.setGroupIndex(scoreItem.getGroupIndex());
+                            convertedItem.setRank(scoreItem.getRank());
+                            convertedItem.setScore(convertedScore);
+                            if (saveScoreRecord(eventId, project, convertedItem)) {
+                                successCount++;
+                            }
+                        } else {
+                            Log.warn("成绩格式转换失败,跳过记录: {}", scoreStr);
+                        }
+                    }
+                } else {
+                    if (saveScoreRecord(eventId, project, scoreItem)) {
+                        successCount++;
+                    }
                 }
             }
 //            Log.info("成绩上传完成,成功保存 {} 条记录", successCount);
@@ -349,8 +372,13 @@ public class ElectrometerServiceImpl implements IElectrometerService {
 
             if (existingScore != null) {
                 // 更新现有记录
-                existingScore.setIndividualPerformance(scoreItem.getScore());
-                existingScore.setScoreRank(scoreItem.getRank());
+                // scoreItem.getScore() 已经是 BigDecimal 类型,可以直接使用
+                if (scoreItem.getScore() != null) {
+                    existingScore.setIndividualPerformance(scoreItem.getScore());
+                }
+                if (scoreItem.getRank() != null) {
+                    existingScore.setScoreRank(scoreItem.getRank());
+                }
                 existingScore.setRemark("电计设备上传");
 
                 return gameScoreMapper.updateById(existingScore) > 0;
@@ -361,8 +389,12 @@ public class ElectrometerServiceImpl implements IElectrometerService {
                 newScore.setProjectId(project.getProjectId());
                 newScore.setAthleteId(athlete.getAthleteId());
                 newScore.setTeamId(athlete.getTeamId());
-                newScore.setIndividualPerformance(scoreItem.getScore());
-                newScore.setScoreRank(scoreItem.getRank());
+                if (scoreItem.getScore() != null) {
+                    newScore.setIndividualPerformance(scoreItem.getScore());
+                }
+                if (scoreItem.getRank() != null) {
+                    newScore.setScoreRank(scoreItem.getRank());
+                }
                 newScore.setScoreType("individual");
                 newScore.setStatus("0"); // 正常
                 newScore.setRemark("电计设备上传");
@@ -562,23 +594,20 @@ public class ElectrometerServiceImpl implements IElectrometerService {
 
             GameScore existingScore = gameScoreMapper.selectOne(scoreWrapper);
 
-            // 处理成绩数据
+            // 处理成绩数据 - 如果是计时类项目,应用时间格式转换
             BigDecimal scoreValue = null;
             if (scoreItem.getScore() != null && !scoreItem.getScore().isEmpty()) {
-                try {
-                    // 处理时间格式的成绩(如1:23.213)
-                    if (scoreItem.getScore().contains(":")) {
-                        String[] timeParts = scoreItem.getScore().split(":");
-                        if (timeParts.length == 2) {
-                            double minutes = Double.parseDouble(timeParts[0]);
-                            double seconds = Double.parseDouble(timeParts[1]);
-                            scoreValue = BigDecimal.valueOf(minutes * 60 + seconds);
-                        }
-                    } else {
+                // 判断是否为计时类项目
+                if (isTimingProject(project)) {
+                    // 使用统一的时间格式转换方法
+                    scoreValue = convertTimeScoreToDecimal(scoreItem.getScore());
+                } else {
+                    // 非计时类项目直接转换
+                    try {
                         scoreValue = BigDecimal.valueOf(Double.parseDouble(scoreItem.getScore()));
+                    } catch (NumberFormatException e) {
+                        Log.warn("解析成绩失败: {}", scoreItem.getScore(), e);
                     }
-                } catch (NumberFormatException e) {
-                    Log.warn("解析成绩失败: {}", scoreItem.getScore(), e);
                 }
             }
 
@@ -618,4 +647,133 @@ public class ElectrometerServiceImpl implements IElectrometerService {
             return false;
         }
     }
+
+    /**
+     * 判断项目是否为计时类项目
+     * 根据项目的计算规则(scoreRule)判断,常见值如:"timing"、"计时"、"time"等
+     *
+     * @param project 项目信息
+     * @return true表示是计时类项目,false表示不是
+     */
+    private boolean isTimingProject(GameEventProject project) {
+        if (project == null || project.getScoreRule() == null) {
+            return false;
+        }
+        List<SysDictDataVo> gameScoreType = dictTypeService.selectDictDataByType("game_score_type");
+        String scoreRule = project.getScoreRule().toLowerCase();
+        Optional<String> rule = gameScoreType.stream()
+            .filter(data -> data.getDictLabel().contains("计时"))
+            .map(SysDictDataVo::getDictValue)
+            .findFirst();
+        // 判断计算规则是否为计时类(可以根据实际字典值调整)
+        return rule.isPresent() && scoreRule.equals(rule.get().toLowerCase());
+    }
+
+    /**
+     * 将各种时间格式成绩转换成小数格式存储(以秒为单位)
+     * 支持格式:
+     * - 0.111 (已经是小数格式)
+     * - 11.000 (已经是小数格式)
+     * - 11:00.000 (分:秒.毫秒格式)
+     * - 11:00:00.000 (时:分:秒.毫秒格式)
+     *
+     * @param timeScore 时间格式的成绩字符串
+     * @return 转换为秒为单位的小数值,如果解析失败返回null
+     */
+    public BigDecimal convertTimeScoreToDecimal(String timeScore) {
+        if (timeScore == null || timeScore.trim().isEmpty()) {
+            return null;
+        }
+
+        try {
+            String trimmed = timeScore.trim();
+
+            // 如果包含冒号,说明是时间格式(时:分:秒.毫秒 或 分:秒.毫秒)
+            if (trimmed.contains(":")) {
+                String[] parts = trimmed.split(":");
+                double totalSeconds = 0.0;
+
+                if (parts.length == 2) {
+                    // 格式:分:秒.毫秒 (如 11:00.000)
+                    double minutes = Double.parseDouble(parts[0]);
+                    double seconds = Double.parseDouble(parts[1]);
+                    totalSeconds = minutes * 60 + seconds;
+                } else if (parts.length == 3) {
+                    // 格式:时:分:秒.毫秒 (如 11:00:00.000)
+                    double hours = Double.parseDouble(parts[0]);
+                    double minutes = Double.parseDouble(parts[1]);
+                    double seconds = Double.parseDouble(parts[2]);
+                    totalSeconds = hours * 3600 + minutes * 60 + seconds;
+                } else {
+                    Log.warn("不支持的时间格式: {}", timeScore);
+                    return null;
+                }
+
+                return BigDecimal.valueOf(totalSeconds);
+            } else {
+                // 已经是小数格式,直接转换
+                return BigDecimal.valueOf(Double.parseDouble(trimmed));
+            }
+        } catch (NumberFormatException e) {
+            Log.warn("解析时间成绩失败: {}", timeScore, e);
+            return null;
+        }
+    }
+
+    /**
+     * 将小数格式的成绩转换成时间格式显示(时:分:秒.毫秒)
+     * 格式:xx:xx:xx.nn (时:分:秒.毫秒)
+     *
+     * @param decimalScore 以秒为单位的小数值
+     * @return 时间格式字符串,如 "01:23:45.678"
+     */
+    public String convertDecimalToTimeScore(BigDecimal decimalScore) {
+        if (decimalScore == null) {
+            return null;
+        }
+
+        try {
+            // 计算小时
+            BigDecimal hoursBigDecimal = decimalScore.divide(BigDecimal.valueOf(3600), 0, java.math.RoundingMode.DOWN);
+            int hours = hoursBigDecimal.intValue();
+            BigDecimal remaining = decimalScore.remainder(BigDecimal.valueOf(3600));
+
+            // 计算分钟
+            BigDecimal minutesBigDecimal = remaining.divide(BigDecimal.valueOf(60), 0, java.math.RoundingMode.DOWN);
+            int minutes = minutesBigDecimal.intValue();
+            remaining = remaining.remainder(BigDecimal.valueOf(60));
+
+            // 秒和毫秒
+            int seconds = remaining.intValue();
+            // 计算毫秒:取小数部分乘以1000并四舍五入
+            BigDecimal millisecondsBigDecimal = remaining.subtract(BigDecimal.valueOf(seconds))
+                    .multiply(BigDecimal.valueOf(1000))
+                    .setScale(0, java.math.RoundingMode.HALF_UP);
+            int milliseconds = millisecondsBigDecimal.intValue();
+
+            // 处理毫秒溢出(理论上不应该发生,但保险起见)
+            if (milliseconds >= 1000) {
+                seconds += milliseconds / 1000;
+                milliseconds = milliseconds % 1000;
+            }
+
+            // 处理秒溢出(理论上不应该发生,但保险起见)
+            if (seconds >= 60) {
+                minutes += seconds / 60;
+                seconds = seconds % 60;
+            }
+
+            // 处理分钟溢出(理论上不应该发生,但保险起见)
+            if (minutes >= 60) {
+                hours += minutes / 60;
+                minutes = minutes % 60;
+            }
+
+            // 格式化输出:时:分:秒.毫秒
+            return String.format("%02d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds);
+        } catch (Exception e) {
+            Log.warn("转换小数为时间格式失败: {}", String.valueOf(decimalScore), e);
+            return null;
+        }
+    }
 }