Selaa lähdekoodia

feat(game-score): 新增成绩导入功能并优化项目成绩数据查询

- 新增成绩导入接口支持Excel批量导入
- 添加导入模板下载功能
- 将ExcelUtil的resetResponse方法改为public以支持外部调用
- 移除运动员服务中的调试日志输出
- 优化成绩服务中的项目分类判断逻辑
- 重构参数校验注解格式提高代码可读性
- 添加FastExcel依赖库支持Excel文件读写操作
- 实现成绩导入的数据验证和错误处理机制
- 优化成绩排名计算算法支持个人和团队项目
- 修改分页查询逻辑使用真正的分页而非全量加载
zhou 2 viikkoa sitten
vanhempi
sitoutus
edfa8be24c

+ 1 - 1
ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java

@@ -366,7 +366,7 @@ public class ExcelUtil {
     /**
      * 重置响应体
      */
-    private static void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException {
+    public static void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException {
         String filename = encodingFilename(sheetName);
         FileUtils.setAttachmentResponseHeader(response, filename);
         response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");

+ 34 - 7
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameScoreController.java

@@ -25,9 +25,9 @@ import org.dromara.system.service.IGameScoreService;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.web.multipart.MultipartFile;
 import java.util.HashMap;
 
-
 /**
  * 成绩
  *
@@ -69,8 +69,7 @@ public class GameScoreController extends BaseController {
      */
     @SaCheckPermission("system:gameScore:query")
     @GetMapping("/{scoreId}")
-    public R<GameScoreVo> getInfo(@NotNull(message = "主键不能为空")
-                                     @PathVariable Long scoreId) {
+    public R<GameScoreVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long scoreId) {
         return R.ok(gameScoreService.queryById(scoreId));
     }
 
@@ -104,8 +103,7 @@ public class GameScoreController extends BaseController {
     @SaCheckPermission("system:gameScore:remove")
     @Log(title = "成绩", businessType = BusinessType.DELETE)
     @DeleteMapping("/{scoreIds}")
-    public R<Void> remove(@NotEmpty(message = "主键不能为空")
-                          @PathVariable Long[] scoreIds) {
+    public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] scoreIds) {
         return toAjax(gameScoreService.deleteWithValidByIds(List.of(scoreIds), true));
     }
 
@@ -114,7 +112,8 @@ public class GameScoreController extends BaseController {
      */
     @SaCheckPermission("system:gameScore:query")
     @GetMapping("/getScoreByAthleteIdAndProjectId")
-    public R<GameScoreVo> getScoreByAthleteIdAndProjectId(@RequestParam("athleteId") Long athleteId, @RequestParam("projectId") Long projectId) {
+    public R<GameScoreVo> getScoreByAthleteIdAndProjectId(@RequestParam("athleteId") Long athleteId,
+            @RequestParam("projectId") Long projectId) {
         return R.ok(gameScoreService.getScoreByAthleteIdAndProjectId(athleteId, projectId));
     }
 
@@ -132,7 +131,8 @@ public class GameScoreController extends BaseController {
             @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
         // 手动构建PageQuery对象
         PageQuery pageQuery = new PageQuery(pageSize, pageNum);
-        Map<String, Object> result = gameScoreService.getProjectScoreData(eventId, projectId, classification, searchValue, pageQuery);
+        Map<String, Object> result = gameScoreService.getProjectScoreData(eventId, projectId, classification,
+                searchValue, pageQuery);
         return R.ok(result);
     }
 
@@ -257,4 +257,31 @@ public class GameScoreController extends BaseController {
             HttpServletResponse response) {
         gameScoreService.exportProjectScore(eventId, projectId, topN, response);
     }
+
+    /**
+     * 导入成绩
+     */
+    @SaCheckPermission("system:gameScore:import")
+    @Log(title = "成绩导入", businessType = BusinessType.IMPORT)
+    @PostMapping("/import")
+    public R<String> importExcel(
+            @RequestPart("file") MultipartFile file,
+            @RequestParam("eventId") Long eventId,
+            @RequestParam("projectId") Long projectId,
+            @RequestParam("classification") String classification,
+            @RequestParam(value = "updateSupport", defaultValue = "0") Boolean updateSupport) {
+        return R.ok(gameScoreService.importScore(eventId, projectId, classification, updateSupport, file));
+    }
+
+    /**
+     * 获取导入模板
+     */
+    @PostMapping("/importTemplate")
+    public void importTemplate(
+            @RequestParam("eventId") Long eventId,
+            @RequestParam("projectId") Long projectId,
+            @RequestParam("classification") String classification,
+            HttpServletResponse response) {
+        gameScoreService.importTemplate(eventId, projectId, classification, response);
+    }
 }

+ 10 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameScoreService.java

@@ -163,4 +163,14 @@ public interface IGameScoreService {
      * @param response HTTP响应对象
      */
     void exportProjectScore(Long eventId, Long projectId, Integer topN, HttpServletResponse response);
+
+    /**
+     * 导入成绩
+     */
+    String importScore(Long eventId, Long projectId, String classification, Boolean updateSupport, org.springframework.web.multipart.MultipartFile file);
+
+    /**
+     * 获取导入模板
+     */
+    void importTemplate(Long eventId, Long projectId, String classification, HttpServletResponse response);
 }

+ 3 - 3
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameAthleteServiceImpl.java

@@ -733,7 +733,7 @@ public class GameAthleteServiceImpl implements IGameAthleteService {
      */
     @Override
     public List<GameAthleteVo> queryListByEventIdAndProjectId(Long eventId, Long projectId, String searchValue) {
-        log.info("查询运动员列表: eventId={}, projectId={}, searchValue={}", eventId, projectId, searchValue);
+//        log.info("查询运动员列表: eventId={}, projectId={}, searchValue={}", eventId, projectId, searchValue);
 
         LambdaQueryWrapper<GameAthlete> lqw = Wrappers.lambdaQuery();
         lqw.eq(GameAthlete::getEventId, eventId);
@@ -744,7 +744,7 @@ public class GameAthleteServiceImpl implements IGameAthleteService {
         }
 
         List<GameAthleteVo> allAthletes = baseMapper.selectVoList(lqw);
-        log.info("查询到总运动员数量: {}", allAthletes.size());
+//        log.info("查询到总运动员数量: {}", allAthletes.size());
 
         // 先转换所有运动员的projectList字段
         allAthletes.forEach(athlete -> {
@@ -777,7 +777,7 @@ public class GameAthleteServiceImpl implements IGameAthleteService {
             })
             .collect(Collectors.toList());
 
-        log.info("过滤后参与项目 {} 的运动员数量: {}", projectId, filteredAthletes.size());
+//        log.info("过滤后参与项目 {} 的运动员数量: {}", projectId, filteredAthletes.size());
         return filteredAthletes;
     }
 

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

@@ -1,6 +1,16 @@
 package org.dromara.system.service.impl;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.idev.excel.FastExcel;
+import cn.idev.excel.metadata.Head;
+import cn.idev.excel.metadata.data.WriteCellData;
+import cn.idev.excel.write.handler.AbstractCellWriteHandler;
+import cn.idev.excel.write.handler.SheetWriteHandler;
+import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
+import cn.idev.excel.write.metadata.holder.WriteTableHolder;
+import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder;
+import cn.idev.excel.write.style.column.SimpleColumnWidthStyleStrategy;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
@@ -44,6 +54,8 @@ import java.net.URLEncoder;
 import org.apache.poi.ss.usermodel.*;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 import org.springframework.transaction.annotation.Transactional;
+import org.dromara.common.excel.utils.ExcelUtil;
+import java.io.IOException;
 
 /**
  * 成绩Service业务层处理
@@ -320,9 +332,9 @@ public class GameScoreServiceImpl implements IGameScoreService {
     @Override
     public Map<String, Object> getProjectScoreData(Long eventId, Long projectId, String classification,
             String searchValue, PageQuery pageQuery) {
-        log.info(
-                "开始获取项目成绩数据 (真分页): eventId={}, projectId={}, classification={}, searchValue={}, pageNum={}, pageSize={}",
-                eventId, projectId, classification, searchValue, pageQuery.getPageNum(), pageQuery.getPageSize());
+//        log.info(
+//                "开始获取项目成绩数据 (真分页): eventId={}, projectId={}, classification={}, searchValue={}, pageNum={}, pageSize={}",
+//                eventId, projectId, classification, searchValue, pageQuery.getPageNum(), pageQuery.getPageSize());
 
         Map<String, Object> result = new HashMap<>();
 
@@ -332,7 +344,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
         long registrationCount = gameAthleteService.selectAthleteCountByProjectId(eventId, projectId);
         stats.put("registrationCount", registrationCount);
 
-        if ("0".equals(classification)) {
+        if (ProjectClassification.SINGLE.getValue().equals(classification)) {
             // 个人项目统计 (按人)
             stats.put("participantCount", registrationCount);
             // 完赛人数 (有成绩记录的运动员数量)
@@ -372,7 +384,7 @@ public class GameScoreServiceImpl implements IGameScoreService {
 
         // 2. 获取分页数据 (真分页)
         TableDataInfo<Map<String, Object>> tableDataInfo;
-        if ("0".equals(classification)) {
+        if (ProjectClassification.SINGLE.getValue().equals(classification)) {
             // 个人项目分页
             tableDataInfo = getIndividualProjectDataPaged(eventId, projectId, searchValue, pageQuery);
         } else {
@@ -560,10 +572,12 @@ public class GameScoreServiceImpl implements IGameScoreService {
 
         // 1. 获取项目配置信息
         GameEventProjectVo project = gameEventProjectService.queryById(bo.getProjectId());
-        if (project == null) return false;
+        if (project == null)
+            return false;
 
         // 判断是否是“纯排名计算”请求(不带具体的录入对象)
-        boolean isOnlyRecalculate = bo.getAthleteId() == null && bo.getTeamId() == null && (bo.getScoreId() == null || bo.getScoreId() == 0);
+        boolean isOnlyRecalculate = bo.getAthleteId() == null && bo.getTeamId() == null
+                && (bo.getScoreId() == null || bo.getScoreId() == 0);
 
         Boolean result = true;
 
@@ -587,7 +601,8 @@ public class GameScoreServiceImpl implements IGameScoreService {
 
             if (result) {
                 // 个人项目:如果传了明细,则保存 (团体项目已在 handleTeamScoreUpdate 中处理过)
-                if ("0".equals(project.getClassification()) && bo.getDetails() != null && !bo.getDetails().isEmpty()) {
+                if (ProjectClassification.SINGLE.getValue().equals(project.getClassification())
+                        && bo.getDetails() != null && !bo.getDetails().isEmpty()) {
                     saveScoreDetails(bo.getScoreId(), bo.getProjectId(), bo.getAthleteId(), null, bo.getDetails());
                 }
             }
@@ -620,7 +635,8 @@ public class GameScoreServiceImpl implements IGameScoreService {
                 .filter(Objects::nonNull)
                 .toList();
 
-        if (values.isEmpty()) return;
+        if (values.isEmpty())
+            return;
 
         BigDecimal aggregate = BigDecimal.ZERO;
 
@@ -696,10 +712,19 @@ public class GameScoreServiceImpl implements IGameScoreService {
                 return false;
             }
 
+            // 提前查询已存在的成绩记录,获取 score_id 以便执行更新而非冲突插入
+            Map<Long, Long> athleteScoreIdMap = baseMapper.selectList(Wrappers.<GameScore>lambdaQuery()
+                    .select(GameScore::getScoreId, GameScore::getAthleteId)
+                    .eq(GameScore::getProjectId, bo.getProjectId())
+                    .in(GameScore::getAthleteId, atheleteIds))
+                    .stream()
+                    .collect(Collectors.toMap(GameScore::getAthleteId, GameScore::getScoreId, (v1, v2) -> v1));
+
             List<GameScore> scoreList = new ArrayList<>();
             // 为每个运动员创建或更新成绩记录
             for (Long athleteId : atheleteIds) {
                 GameScore athleteScore = new GameScore();
+                athleteScore.setScoreId(athleteScoreIdMap.get(athleteId)); // 设置已存在的 ID
                 athleteScore.setEventId(bo.getEventId());
                 athleteScore.setProjectId(bo.getProjectId());
                 athleteScore.setAthleteId(athleteId);
@@ -776,14 +801,15 @@ public class GameScoreServiceImpl implements IGameScoreService {
         List<GameScore> updateList = new ArrayList<>();
 
         // 3. 根据项目类型执行不同的排名逻辑
-        if ("0".equals(project.getClassification())) {
+        if (ProjectClassification.SINGLE.getValue().equals(project.getClassification())) {
             // --- 个人项目排名 ---
-            allScores.sort((a, b) -> compareScores(a, b, orderType, "0"));
+            allScores.sort((a, b) -> compareScores(a, b, orderType, ProjectClassification.SINGLE.getValue()));
 
             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, "0") != 0) {
+                if (i > 0 && compareScores(current, allScores.get(i - 1), orderType,
+                        ProjectClassification.SINGLE.getValue()) != 0) {
                     currentRank = i + 1;
                 }
                 int points = (currentRank <= pointConfig.size()) ? pointConfig.get(currentRank - 1) : 0;
@@ -802,7 +828,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, "1");
+                return compareScores(score1, score2, orderType, ProjectClassification.TEAM.getValue());
             });
 
             // 3.3 分配名次并分发给队员
@@ -812,7 +838,8 @@ public class GameScoreServiceImpl implements IGameScoreService {
                 if (i > 0) {
                     GameScoreVo currentTeamScore = teamGroups.get(teamId).get(0);
                     GameScoreVo prevTeamScore = teamGroups.get(sortedTeamIds.get(i - 1)).get(0);
-                    if (compareScores(currentTeamScore, prevTeamScore, orderType, "1") != 0) {
+                    if (compareScores(currentTeamScore, prevTeamScore, orderType,
+                            ProjectClassification.TEAM.getValue()) != 0) {
                         currentRank = i + 1;
                     }
                 }
@@ -848,8 +875,10 @@ public class GameScoreServiceImpl implements IGameScoreService {
      */
     private int compareScores(GameScoreVo a, GameScoreVo b, String orderType, String classification) {
         // 第一级:主成绩 (0-个人, 1-团队)
-        BigDecimal perfA = "0".equals(classification) ? a.getIndividualPerformance() : a.getTeamPerformance();
-        BigDecimal perfB = "0".equals(classification) ? b.getIndividualPerformance() : b.getTeamPerformance();
+        BigDecimal perfA = ProjectClassification.SINGLE.getValue().equals(classification) ? a.getIndividualPerformance()
+                : a.getTeamPerformance();
+        BigDecimal perfB = ProjectClassification.SINGLE.getValue().equals(classification) ? b.getIndividualPerformance()
+                : b.getTeamPerformance();
         if (perfA == null)
             perfA = BigDecimal.ZERO;
         if (perfB == null)
@@ -2289,4 +2318,335 @@ public class GameScoreServiceImpl implements IGameScoreService {
             throw new RuntimeException("导出失败:" + e.getMessage());
         }
     }
+
+    @Override
+    public void importTemplate(Long eventId, Long projectId, String classification, HttpServletResponse response) {
+        GameEventProjectVo project = gameEventProjectService.queryById(projectId);
+        int scoreCount = project.getScoreCount() != null ? project.getScoreCount() : 1;
+        List<List<String>> head = new ArrayList<>();
+        if (ProjectClassification.SINGLE.getValue().equals(classification)) {
+            head.add(List.of("运动员号码"));
+            head.add(List.of("姓名"));
+        } else {
+            head.add(List.of("队伍编号"));
+            head.add(List.of("队伍名"));
+        }
+        for (int i = 1; i <= scoreCount; i++) {
+            head.add(List.of("第" + i + "轮成绩"));
+        }
+
+        try {
+            ExcelUtil.resetResponse("成绩导入模板", response);
+            // 设置文本格式的 Handler
+            SimpleColumnWidthStyleStrategy columnWidthStyleStrategy = new SimpleColumnWidthStyleStrategy(20);
+
+            FastExcel.write(response.getOutputStream())
+                    .head(head)
+                    .registerWriteHandler(new SheetWriteHandler() {
+                        @Override
+                        public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder,
+                                WriteSheetHolder writeSheetHolder) {
+                            Workbook workbook = writeWorkbookHolder.getWorkbook();
+                            CellStyle textStyle = workbook.createCellStyle();
+                            textStyle.setDataFormat(workbook.createDataFormat().getFormat("@"));
+                            Sheet sheet = writeSheetHolder.getSheet();
+                            // 根据表头的实际长度,动态为所有列设置默认文本格式
+                            for (int i = 0; i < head.size(); i++) {
+                                sheet.setDefaultColumnStyle(i, textStyle);
+                            }
+                        }
+                    })
+                    .registerWriteHandler(new AbstractCellWriteHandler() {
+                        @Override
+                        public void afterCellDispose(WriteSheetHolder writeSheetHolder,
+                                WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell,
+                                Head head, Integer relativeRowIndex, Boolean isHead) {
+                            if (!isHead) {
+                                Workbook workbook = writeSheetHolder.getSheet().getWorkbook();
+                                CellStyle textStyle = workbook.createCellStyle();
+                                textStyle.setDataFormat(workbook.createDataFormat().getFormat("@"));
+                                cell.setCellStyle(textStyle);
+                            }
+                        }
+                    })
+                    .registerWriteHandler(columnWidthStyleStrategy)
+                    .sheet("模板")
+                    .doWrite(new ArrayList<>());
+        } catch (IOException e) {
+            log.error("生成导入模板失败", e);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String importScore(Long eventId, Long projectId, String classification, Boolean updateSupport,
+            org.springframework.web.multipart.MultipartFile file) {
+        GameEventProjectVo project = gameEventProjectService.queryById(projectId);
+        int scoreCount = project.getScoreCount() != null ? project.getScoreCount() : 1;
+
+        List<Map<Integer, String>> list;
+        try {
+            list = FastExcel.read(file.getInputStream()).sheet().doReadSync();
+        } catch (IOException e) {
+            log.error("读取导入文件失败", e);
+            return "读取文件失败";
+        }
+
+        if (CollUtil.isEmpty(list)) {
+            return "文件内容为空";
+        }
+
+        // 1. 预提取所有编号
+        Set<String> codes = list.stream()
+                .skip(1)
+                .map(row -> row.get(0) == null ? "" : String.valueOf(row.get(0)).trim())
+                .filter(code -> StringUtils.isNotBlank(code) && !"null".equals(code))
+                .collect(Collectors.toSet());
+
+        if (codes.isEmpty())
+            return "未检测到有效数据";
+
+        // 2. 批量预加载(仅查询必要字段)
+        Map<String, GameAthlete> athleteMap = new HashMap<>();
+        Map<String, GameTeam> teamMap = new HashMap<>();
+        Map<Long, GameScore> existingScoreMap = new HashMap<>();
+
+        if (ProjectClassification.SINGLE.getValue().equals(classification)) {
+            List<GameAthlete> athletes = gameAthleteMapper.selectList(Wrappers.<GameAthlete>lambdaQuery()
+                    .select(GameAthlete::getAthleteId, GameAthlete::getAthleteCode, GameAthlete::getTeamId,
+                            GameAthlete::getName)
+                    .eq(GameAthlete::getEventId, eventId)
+                    .in(GameAthlete::getAthleteCode, codes)
+                    .eq(GameAthlete::getDelFlag, "0"));
+            athleteMap = athletes.stream().collect(Collectors.toMap(GameAthlete::getAthleteCode, a -> a));
+
+            if (!athleteMap.isEmpty()) {
+                List<GameScore> scores = baseMapper.selectList(Wrappers.<GameScore>lambdaQuery()
+                        .select(GameScore::getScoreId, GameScore::getAthleteId)
+                        .eq(GameScore::getProjectId, projectId)
+                        .in(GameScore::getAthleteId,
+                                athleteMap.values().stream().map(GameAthlete::getAthleteId).toList()));
+                existingScoreMap = scores.stream().collect(Collectors.toMap(GameScore::getAthleteId, s -> s));
+            }
+        } else {
+            List<GameTeam> teams = gameTeamMapper.selectList(Wrappers.<GameTeam>lambdaQuery()
+                    .select(GameTeam::getTeamId, GameTeam::getTeamCode, GameTeam::getTeamName)
+                    .eq(GameTeam::getEventId, eventId)
+                    .in(GameTeam::getTeamCode, codes)
+                    .eq(GameTeam::getDelFlag, "0"));
+            teamMap = teams.stream().collect(Collectors.toMap(GameTeam::getTeamCode, t -> t));
+
+            if (!teamMap.isEmpty()) {
+                List<GameScore> scores = baseMapper.selectList(Wrappers.<GameScore>lambdaQuery()
+                        .select(GameScore::getScoreId, GameScore::getTeamId)
+                        .eq(GameScore::getProjectId, projectId)
+                        .in(GameScore::getTeamId, teamMap.values().stream().map(GameTeam::getTeamId).toList()));
+                // 团体项目成绩按队伍 ID 映射 (取一个代表)
+                existingScoreMap = scores.stream()
+                        .collect(Collectors.toMap(GameScore::getTeamId, s -> s, (s1, s2) -> s1));
+            }
+        }
+
+        // 3. 处理数据
+        List<GameScore> scoreSaveList = new ArrayList<>();
+        List<GameScoreDetail> detailSaveList = new ArrayList<>();
+        Map<Long, GameScoreBo> teamBoMap = new HashMap<>(); // 团体项目成绩暂存
+        int successNum = 0;
+        int failureNum = 0;
+        StringBuilder failureMsg = new StringBuilder();
+
+        for (int i = 0; i < list.size(); i++) {
+            Map<Integer, String> row = list.get(i);
+            String code = row.get(0) == null ? "" : String.valueOf(row.get(0)).trim();
+            String name = row.get(1) == null ? "" : String.valueOf(row.get(1)).trim();
+            if (i == 0 && ("运动员号码".equals(code) || "队伍编号".equals(code)))
+                continue;
+            if (StringUtils.isBlank(code) || "null".equals(code))
+                continue;
+
+            try {
+                GameScoreBo bo = new GameScoreBo();
+                bo.setEventId(eventId);
+                bo.setProjectId(projectId);
+
+                if (ProjectClassification.SINGLE.getValue().equals(classification)) {
+                    GameAthlete athlete = athleteMap.get(code);
+                    if (athlete == null) {
+                        failureNum++;
+                        failureMsg.append("<br/>").append(failureNum).append("、运动员号码 [").append(code).append("] 不存在");
+                        continue;
+                    }
+                    if (!athlete.getName().equals(name)) {
+                        failureNum++;
+                        failureMsg.append("<br/>").append(failureNum).append("、运动员号码 [").append(code).append("] 与姓名 [")
+                                .append(name).append("] 不匹配,系统内姓名为 [").append(athlete.getName()).append("]");
+                        continue;
+                    }
+
+                    GameScore es = existingScoreMap.get(athlete.getAthleteId());
+                    if (es != null) {
+                        if (!updateSupport) {
+                            failureNum++;
+                            failureMsg.append("<br/>").append(failureNum).append("、运动员 [").append(name)
+                                    .append("] 成绩已存在,已跳过");
+                            continue;
+                        }
+                        bo.setScoreId(es.getScoreId());
+                    }
+
+                    bo.setAthleteId(athlete.getAthleteId());
+                    bo.setTeamId(athlete.getTeamId());
+                    bo.setScoreType("individual");
+                } else {
+                    GameTeam team = teamMap.get(code);
+                    if (team == null) {
+                        failureNum++;
+                        failureMsg.append("<br/>").append(failureNum).append("、队伍编号 [").append(code).append("] 不存在");
+                        continue;
+                    }
+                    if (!team.getTeamName().equals(name)) {
+                        failureNum++;
+                        failureMsg.append("<br/>").append(failureNum).append("、队伍编号 [").append(code).append("] 与队伍名 [")
+                                .append(name).append("] 不匹配,系统内队伍名为 [").append(team.getTeamName()).append("]");
+                        continue;
+                    }
+
+                    GameScore es = existingScoreMap.get(team.getTeamId());
+                    if (es != null) {
+                        if (!updateSupport) {
+                            failureNum++;
+                            failureMsg.append("<br/>").append(failureNum).append("、队伍 [").append(name)
+                                    .append("] 成绩已存在,已跳过");
+                            continue;
+                        }
+                        bo.setScoreId(es.getScoreId());
+                    }
+
+                    bo.setTeamId(team.getTeamId());
+                    bo.setScoreType("team");
+                }
+
+                // 填充成绩明细
+                List<GameScoreDetailBo> detailBos = new ArrayList<>();
+                for (int j = 1; j <= scoreCount; j++) {
+                    String scoreStr = row.get(j + 1) == null ? "" : String.valueOf(row.get(j + 1)).trim();
+                    if (StringUtils.isNotBlank(scoreStr) && !"null".equals(scoreStr)) {
+                        BigDecimal performanceValue = parsePerformanceValue(scoreStr, i, j + 1, failureNum,
+                                failureMsg);
+                        if (performanceValue == null) {
+                            continue;
+                        }
+
+                        GameScoreDetailBo dBo = new GameScoreDetailBo();
+                        dBo.setAttemptIndex(j);
+                        dBo.setPerformanceValue(performanceValue);
+                        detailBos.add(dBo);
+                    }
+                }
+
+                // 校验:如果没有任何成绩明细,则视为无效行
+                if (detailBos.isEmpty()) {
+                    failureNum++;
+                    failureMsg.append("<br/>").append(failureNum).append("、第").append(i + 1)
+                            .append("行:未检测到有效成绩,请检查数据。");
+                    continue;
+                }
+
+                bo.setDetails(detailBos);
+
+                // 计算统计值 (aggregatePerformance, faultA, faultB)
+                calculateAndSetAggregatePerformance(bo, project);
+
+                if (ProjectClassification.SINGLE.getValue().equals(classification)) {
+                    GameScore score = MapstructUtils.convert(bo, GameScore.class);
+                    scoreSaveList.add(score);
+
+                    // 个人项目:准备持久化明细
+                    for (GameScoreDetailBo dBo : detailBos) {
+                        GameScoreDetail detail = BeanUtil.copyProperties(dBo, GameScoreDetail.class);
+                        detail.setProjectId(projectId);
+                        detail.setAthleteId(bo.getAthleteId());
+                        detail.setScoreId(bo.getScoreId());
+                        detailSaveList.add(detail);
+                    }
+                } else {
+                    // 团体项目暂存 BO,用于后续 handleTeamScoreUpdate
+                    teamBoMap.put(bo.getTeamId(), bo);
+                }
+                successNum++;
+            } catch (Exception e) {
+                failureNum++;
+                failureMsg.append("<br/>").append(failureNum).append("、第").append(i + 1).append("行处理失败:")
+                        .append(e.getMessage());
+            }
+        }
+
+        // 4. 执行批量入库
+        if (!scoreSaveList.isEmpty()) {
+            baseMapper.insertOrUpdateBatch(scoreSaveList);
+            // 更新明细中的 scoreId (仅个人项目需要)
+            for (int i = 0; i < scoreSaveList.size(); i++) {
+                final Long scoreId = scoreSaveList.get(i).getScoreId();
+                final Long athleteId = scoreSaveList.get(i).getAthleteId();
+                detailSaveList.stream()
+                        .filter(d -> athleteId.equals(d.getAthleteId()))
+                        .forEach(d -> d.setScoreId(scoreId));
+            }
+        }
+
+        // 5. 团体项目同步给队伍所有队员
+        if (ProjectClassification.TEAM.getValue().equals(classification) && !teamBoMap.isEmpty()) {
+            for (GameScoreBo teamBo : teamBoMap.values()) {
+                handleTeamScoreUpdate(teamBo);
+            }
+        }
+
+        if (!detailSaveList.isEmpty()) {
+            scoreDetailMapper.insertOrUpdateBatch(detailSaveList);
+        }
+
+        // 6. 统一重计排名 (仅此一次)
+        recalculateRankingsAndPoints(eventId, projectId);
+
+        if (failureNum > 0) {
+            failureMsg.insert(0, "导入完成。共 " + successNum + " 条成功," + failureNum + " 条失败,错误如下:");
+        } else {
+            failureMsg.setLength(0);
+            failureMsg.append("恭喜您,数据已全部导入成功!共 ").append(successNum).append(" 条。");
+        }
+        return failureMsg.toString();
+    }
+
+    private BigDecimal parsePerformanceValue(String scoreStr, int rowIndex, int colIndex, int failureNum,
+            StringBuilder failureMsg) {
+        if (StringUtils.isBlank(scoreStr))
+            return null;
+        scoreStr = scoreStr.trim();
+        // 如果包含冒号,则认为是时间格式 (HH:mm:ss.SSS 或 mm:ss.SSS)
+        if (scoreStr.contains(":")) {
+            try {
+                String[] parts = scoreStr.split(":");
+                double totalSeconds = 0;
+                if (parts.length == 3) {
+                    // HH:mm:ss.SSS
+                    totalSeconds = Double.parseDouble(parts[0]) * 3600
+                            + Double.parseDouble(parts[1]) * 60
+                            + Double.parseDouble(parts[2]);
+                } else if (parts.length == 2) {
+                    // mm:ss.SSS
+                    totalSeconds = Double.parseDouble(parts[0]) * 60
+                            + Double.parseDouble(parts[1]);
+                } else {
+                    throw new IllegalArgumentException("格式非法");
+                }
+                return BigDecimal.valueOf(totalSeconds).setScale(3, java.math.RoundingMode.HALF_UP);
+            } catch (Exception e) {
+                failureMsg.append("<br/>第").append(rowIndex + 1).append("行第").append(colIndex + 1)
+                        .append("列时间格式不正确:[").append(scoreStr).append("]");
+                return null;
+            }
+        }
+        // 普通数字格式
+        return new BigDecimal(scoreStr);
+    }
 }