7 Revize 07dfa6e3e0 ... b03ffa82bf

Autor SHA1 Zpráva Datum
  zhou b03ffa82bf feat(game-event): 添加云端字体加载功能支持OSS字体文件 před 2 týdny
  zhou 81d1c21b47 refactor(NumberController): 移除参赛证参数边界检查功能 před 2 týdny
  zhou edfa8be24c feat(game-score): 新增成绩导入功能并优化项目成绩数据查询 před 2 týdny
  zhou e7b6b84898 feat(game-team): 优化队伍导入功能 před 2 týdny
  zhou 069aaa86e3 feat(game): 添加纯排名计算功能支持 před 2 týdny
  zhou 1189e36c35 refactor(game): 重构队伍与运动员关联关系的数据存储方式 před 2 týdny
  zhou 90efeec34c feat(gameAthlete): 优化导入功能并完善排序规则 před 2 týdny
23 změnil soubory, kde provedl 1002 přidání a 342 odebrání
  1. 1 1
      ruoyi-common/ruoyi-common-excel/src/main/java/org/dromara/common/excel/utils/ExcelUtil.java
  2. 25 23
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameAthleteController.java
  3. 34 7
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameScoreController.java
  4. 10 18
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameTeamController.java
  5. 31 120
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/NumberController.java
  6. 0 10
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/GameTeam.java
  7. 0 2
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GameTeamBo.java
  8. 2 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GenerateBibBo.java
  9. 68 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameAthleteImportVo.java
  10. 61 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameTeamImportVo.java
  11. 2 7
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameTeamVo.java
  12. 8 6
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/GameTeamMapper.java
  13. 3 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameAthleteService.java
  14. 2 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameEventService.java
  15. 10 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameScoreService.java
  16. 9 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameTeamService.java
  17. 1 1
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/ArticleServiceImpl.java
  18. 70 32
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameAthleteServiceImpl.java
  19. 31 1
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameEventServiceImpl.java
  20. 399 37
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameScoreServiceImpl.java
  21. 233 47
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameTeamServiceImpl.java
  22. 2 24
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/IEnrollServiceImpl.java
  23. 0 6
      ruoyi-modules/ruoyi-game-event/src/main/resources/mapper/system/GameTeamMapper.xml

+ 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");

+ 25 - 23
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameAthleteController.java

@@ -3,15 +3,14 @@ package org.dromara.system.controller;
 import java.io.IOException;
 import java.util.*;
 
+import cn.hutool.core.collection.CollUtil;
 import lombok.RequiredArgsConstructor;
 import jakarta.servlet.http.HttpServletResponse;
 import jakarta.validation.constraints.*;
 import cn.dev33.satoken.annotation.SaCheckPermission;
 import lombok.extern.slf4j.Slf4j;
-import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.excel.core.ExcelResult;
 import org.dromara.common.redis.utils.RedisUtils;
-import org.dromara.system.domain.GameAthlete;
 import org.dromara.system.domain.bo.ProjectSelectionValidationBo;
 import org.dromara.system.domain.constant.GameEventConstant;
 import org.dromara.system.domain.vo.AthleteScoreVo;
@@ -27,6 +26,7 @@ import org.dromara.common.core.validate.AddGroup;
 import org.dromara.common.core.validate.EditGroup;
 import org.dromara.common.log.enums.BusinessType;
 import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.system.domain.vo.GameAthleteImportVo;
 import org.dromara.system.domain.vo.GameAthleteVo;
 import org.dromara.system.domain.bo.GameAthleteBo;
 import org.dromara.system.service.IGameAthleteService;
@@ -74,25 +74,26 @@ public class GameAthleteController extends BaseController {
     @SaCheckPermission("system:gameAthlete:import")
     @Log(title = "参赛队员", businessType = BusinessType.IMPORT)
     @PostMapping("/import")
-    public R<Void> importExcel(@RequestPart("file") MultipartFile file) {
-        ExcelResult<GameAthleteVo> excelResult = null;
+    public R<String> importExcel(@RequestPart("file") MultipartFile file) {
         try {
             // 从redis中获取默认参赛赛事
-            Long defaultEventId = Long.valueOf(RedisUtils.getCacheObject(GameEventConstant.DEFAULT_EVENT_ID).toString());
-            excelResult = ExcelUtil.importExcel(file.getInputStream(), GameAthleteVo.class, true);
-            List<GameAthlete> list = MapstructUtils.convert(excelResult.getList(), GameAthlete.class);
-            Long finalDefaultEventId = defaultEventId;
-            list.stream()
-                .map(gameAthlete -> {
-                    gameAthlete.setEventId(finalDefaultEventId);
-                    return gameAthlete;
-                })
-                .toList();
-            gameAthleteService.saveOrEditBatch(list);
+            long defaultEventId = Long
+                    .parseLong(RedisUtils.getCacheObject(GameEventConstant.DEFAULT_EVENT_ID).toString());
+            ExcelResult<GameAthleteImportVo> excelResult = ExcelUtil.importExcel(file.getInputStream(),
+                    GameAthleteImportVo.class, true);
+            if (excelResult == null || CollUtil.isEmpty(excelResult.getList())) {
+                return R.fail("导入数据不能为空");
+            }
+            // 调用新的业务逻辑方法
+            String message = gameAthleteService.importAthletes(excelResult.getList(), defaultEventId);
+            return R.ok(message);
         } catch (IOException e) {
-            e.printStackTrace();
+            log.error("读取导入文件失败", e);
+            return R.fail("导入失败,读取文件异常");
+        } catch (Exception e) {
+            log.error("导入数据失败", e);
+            return R.fail(e.getMessage());
         }
-        return R.ok(excelResult.getAnalysis());
     }
 
     /**
@@ -100,7 +101,7 @@ public class GameAthleteController extends BaseController {
      */
     @PostMapping("/importTemplate")
     public void importTemplate(HttpServletResponse response) {
-        ExcelUtil.exportExcel(new ArrayList<>(), "参数队员数据", GameAthleteVo.class, response);
+        ExcelUtil.exportExcel(new ArrayList<>(), "参赛队员导入模板", GameAthleteImportVo.class, response);
     }
 
     /**
@@ -110,8 +111,7 @@ public class GameAthleteController extends BaseController {
      */
     @SaCheckPermission("system:gameAthlete:query")
     @GetMapping("/{athleteId}")
-    public R<GameAthleteVo> getInfo(@NotNull(message = "主键不能为空")
-                                    @PathVariable Long athleteId) {
+    public R<GameAthleteVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long athleteId) {
         return R.ok(gameAthleteService.queryById(athleteId));
     }
 
@@ -157,8 +157,7 @@ public class GameAthleteController extends BaseController {
     @SaCheckPermission("system:gameAthlete:remove")
     @Log(title = "参赛队员", businessType = BusinessType.DELETE)
     @DeleteMapping("/{athleteIds}")
-    public R<Void> remove(@NotEmpty(message = "主键不能为空")
-                          @PathVariable Long[] athleteIds) {
+    public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] athleteIds) {
         return toAjax(gameAthleteService.deleteWithValidByIds(List.of(athleteIds), true));
     }
 
@@ -171,15 +170,18 @@ public class GameAthleteController extends BaseController {
 
     /**
      * 根据项目ID获取参赛人数
+     *
      * @param projectId 项目ID
      */
     @GetMapping("/count/{projectId}")
-    public R<Long> getAthleteCountByProjectId(@PathVariable Long projectId, @RequestParam(required = false) Long eventId) {
+    public R<Long> getAthleteCountByProjectId(@PathVariable Long projectId,
+            @RequestParam(required = false) Long eventId) {
         return R.ok(gameAthleteService.selectAthleteCountByProjectId(eventId, projectId));
     }
 
     /**
      * 根据项目ID分页查询队伍详细信息
+     *
      * @param projectId 项目ID
      */
     @GetMapping("/teamListByProject/{projectId}")

+ 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 - 18
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameTeamController.java

@@ -7,7 +7,6 @@ import jakarta.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.common.core.domain.R;
-import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.validate.AddGroup;
 import org.dromara.common.core.validate.EditGroup;
 import org.dromara.common.excel.core.ExcelResult;
@@ -18,10 +17,10 @@ import org.dromara.common.log.enums.BusinessType;
 import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.web.core.BaseController;
-import org.dromara.system.domain.GameTeam;
 import org.dromara.system.domain.bo.GameTeamBo;
 import org.dromara.system.domain.request.MoveGroupRequest;
 import org.dromara.system.domain.request.UpdateTeamAthletesRequest;
+import org.dromara.system.domain.vo.GameTeamImportVo;
 import org.dromara.system.domain.vo.GameTeamVo;
 import org.dromara.system.service.IGameEventService;
 import org.dromara.system.service.IGameTeamService;
@@ -32,7 +31,6 @@ import org.springframework.web.multipart.MultipartFile;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 
 /**
  * 参赛队伍
@@ -65,23 +63,17 @@ public class GameTeamController extends BaseController {
     @SaCheckPermission("system:gameTeam:import")
     @Log(title = "参赛队伍", businessType = BusinessType.IMPORT)
     @PostMapping("/import")
-    public R<Void> importExcel(@RequestPart("file") MultipartFile file) {
-        ExcelResult<GameTeamVo> excelResult = null;
+    public R<String> importExcel(@RequestPart("file") MultipartFile file, boolean updateSupport) {
+        ExcelResult<GameTeamImportVo> excelResult = null;
         try {
-            excelResult = ExcelUtil.importExcel(file.getInputStream(), GameTeamVo.class, true);
-            Map<String, Long> eventIdNameMap = gameEventService.getEventIdNameMap();
-            excelResult.getList().stream().forEach(item -> {
-                //根据导入的赛事名称查询对应的赛事ID
-                if (eventIdNameMap.containsKey(item.getEventName())){
-                    item.setEventId(eventIdNameMap.get(item.getEventName()));
-                }
-            });
-            List<GameTeam> list = MapstructUtils.convert(excelResult.getList(), GameTeam.class);
-            gameTeamService.saveBatch(list);
+            excelResult = ExcelUtil.importExcel(file.getInputStream(), GameTeamImportVo.class, true);
+            List<GameTeamImportVo> list = excelResult.getList();
+            String message = gameTeamService.importTeam(list, updateSupport);
+            return R.ok("导入成功", message);
         } catch (IOException e) {
-            e.printStackTrace();
+            log.error("导入队伍失败", e);
+            return R.fail("文件读取失败");
         }
-        return R.ok(excelResult.getAnalysis());
     }
 
     /**
@@ -89,7 +81,7 @@ public class GameTeamController extends BaseController {
      */
     @PostMapping("/importTemplate")
     public void importTemplate(HttpServletResponse response) {
-        ExcelUtil.exportExcel(new ArrayList<>(), "参数队伍数据", GameTeamVo.class, response);
+        ExcelUtil.exportExcel(new ArrayList<>(), "参数队伍数据", GameTeamImportVo.class, response);
     }
 
     /**

+ 31 - 120
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/NumberController.java

@@ -29,7 +29,6 @@ import java.awt.*;
 import java.awt.image.BufferedImage;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.InputStream;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 
@@ -44,9 +43,6 @@ public class NumberController {
     private final IGameBibTaskService gameBibTaskService;
     private final ISysOssService sysOssService;
 
-    // 预览框尺寸(固定)
-    private static final int previewWidth = 600;
-
     /**
      *
      * @return key:队伍名称 value:队伍运动员
@@ -82,16 +78,13 @@ public class NumberController {
         Object cacheObject = RedisUtils.getCacheObject(GameEventConstant.DEFAULT_EVENT_ID);
         Long eventId = Long.valueOf(cacheObject.toString());
 
-        // 边界检查和修正
-        // validateAndCorrectBibParams(bibParam);
-
         // 调试日志
-        log.info("创建参赛证任务,taskName: {}, bibParam: {}", taskName, bibParam);
-        log.info("bibParam详情 - fontName: {}, fontSize: {}, fontColor: {}, logoX: {}, logoY: {}",
-                bibParam.getFontName(), bibParam.getFontSize(), bibParam.getFontColor(),
-                bibParam.getLogoX(), bibParam.getLogoY());
-        log.info("二维码坐标详情 - qRCodeX: {}, qRCodeY: {}",
-                bibParam.getQRCodeX(), bibParam.getQRCodeY());
+//        log.info("创建参赛证任务,taskName: {}, bibParam: {}", taskName, bibParam);
+//        log.info("bibParam详情 - fontName: {}, fontSize: {}, fontColor: {}, logoX: {}, logoY: {}",
+//                bibParam.getFontName(), bibParam.getFontSize(), bibParam.getFontColor(),
+//                bibParam.getLogoX(), bibParam.getLogoY());
+//        log.info("二维码坐标详情 - qRCodeX: {}, qRCodeY: {}",
+//                bibParam.getQRCodeX(), bibParam.getQRCodeY());
         // log.info("Logo缩放: {}, 二维码缩放: {}", bibParam.getLogoScale(), bibParam.getBarcodeScale());
 
         // 先保存图片文件,再创建任务
@@ -182,9 +175,9 @@ public class NumberController {
             }
 
             // 生成1小时有效的预签名URL
-            log.info("开始生成预签名URL,taskId: {}, ossId: {}", taskId, task.getOssId());
+//            log.info("开始生成预签名URL,taskId: {}, ossId: {}", taskId, task.getOssId());
             String presignedUrl = sysOssService.generatePresignedUrl(task.getOssId(), 3600);
-            log.info("预签名URL生成成功: {}", presignedUrl);
+//            log.info("预签名URL生成成功: {}", presignedUrl);
             return R.ok(presignedUrl);
         } catch (Exception e) {
             log.error("获取下载URL失败,taskId: {}", taskId, e);
@@ -192,93 +185,6 @@ public class NumberController {
         }
     }
 
-    /**
-     * 验证和修正参赛证参数边界
-     */
-    private void validateAndCorrectBibParams(GenerateBibBo bibParam) {
-        // 修正二维码尺寸(范围16-512)
-        if (bibParam.getQRCodeWidth() != null) {
-            int qrWidth = Math.max(16, Math.min(512, bibParam.getQRCodeWidth()));
-            if (Integer.compare(qrWidth, bibParam.getQRCodeWidth()) != 0) {
-                log.warn("二维码宽度超出边界,从 {} 修正为 {}", bibParam.getQRCodeWidth(), qrWidth);
-                bibParam.setQRCodeWidth(qrWidth);
-            }
-        }
-
-        if (bibParam.getQRCodeHeight() != null) {
-            int qrHeight = Math.max(16, Math.min(512, bibParam.getQRCodeHeight()));
-            if (Integer.compare(qrHeight, bibParam.getQRCodeHeight()) != 0) {
-                log.warn("二维码高度超出边界,从 {} 修正为 {}", bibParam.getQRCodeHeight(), qrHeight);
-                bibParam.setQRCodeHeight(qrHeight);
-            }
-        }
-
-        // 修正号码字体大小(范围8-200)
-        if (bibParam.getNumberFontSize() != null) {
-            int numberFontSize = Math.max(8, Math.min(200, bibParam.getNumberFontSize()));
-            if (Integer.compare(numberFontSize, bibParam.getNumberFontSize()) != 0) {
-                log.warn("号码字体大小超出边界,从 {} 修正为 {}", bibParam.getNumberFontSize(), numberFontSize);
-                bibParam.setNumberFontSize(numberFontSize);
-            }
-        }
-
-        // 修正赛事名称字体大小(范围8-120)
-        if (bibParam.getEventFontSize() != null) {
-            int eventFontSize = Math.max(8, Math.min(120, bibParam.getEventFontSize()));
-            if (Integer.compare(eventFontSize, bibParam.getEventFontSize()) != 0) {
-                log.warn("赛事名称字体大小超出边界,从 {} 修正为 {}", bibParam.getEventFontSize(), eventFontSize);
-                bibParam.setEventFontSize(eventFontSize);
-            }
-        }
-
-
-
-        // 修正字体大小(范围8-200)
-        if (bibParam.getFontSize() != null) {
-            int fontSize = Math.max(8, Math.min(200, bibParam.getFontSize()));
-            if (Integer.compare(fontSize, bibParam.getFontSize()) != 0) {
-                log.warn("字体大小超出边界,从 {} 修正为 {}", bibParam.getFontSize(), fontSize);
-                bibParam.setFontSize(fontSize);
-            }
-        }
-
-        // 修正画布尺寸(范围100-4000)
-        if (bibParam.getCanvasWidth() != null) {
-            int canvasWidth = Math.max(100, Math.min(4000, bibParam.getCanvasWidth()));
-            if (Integer.compare(canvasWidth, bibParam.getCanvasWidth()) != 0) {
-                log.warn("画布宽度超出边界,从 {} 修正为 {}", bibParam.getCanvasWidth(), canvasWidth);
-                bibParam.setCanvasWidth(canvasWidth);
-            }
-        }
-
-        if (bibParam.getCanvasHeight() != null) {
-            int canvasHeight = Math.max(100, Math.min(4000, bibParam.getCanvasHeight()));
-            if (Integer.compare(canvasHeight, bibParam.getCanvasHeight()) != 0) {
-                log.warn("画布高度超出边界,从 {} 修正为 {}", bibParam.getCanvasHeight(), canvasHeight);
-                bibParam.setCanvasHeight(canvasHeight);
-            }
-        }
-
-        // 修正Logo尺寸(范围10-500)
-        if (bibParam.getLogoWidth() != null) {
-            int logoWidth = Math.max(10, Math.min(500, bibParam.getLogoWidth()));
-            if (Integer.compare(logoWidth, bibParam.getLogoWidth()) != 0) {
-                log.warn("Logo宽度超出边界,从 {} 修正为 {}", bibParam.getLogoWidth(), logoWidth);
-                bibParam.setLogoWidth(logoWidth);
-            }
-        }
-
-        if (bibParam.getLogoHeight() != null) {
-            int logoHeight = Math.max(10, Math.min(500, bibParam.getLogoHeight()));
-            if (Integer.compare(logoHeight, bibParam.getLogoHeight()) != 0) {
-                log.warn("Logo高度超出边界,从 {} 修正为 {}", bibParam.getLogoHeight(), logoHeight);
-                bibParam.setLogoHeight(logoHeight);
-            }
-        }
-
-        log.info("参赛证参数边界检查完成");
-    }
-
     /**
      * 生成画布模版
      */
@@ -286,7 +192,7 @@ public class NumberController {
         try {
             // 读取背景图片
             BufferedImage originalBgImage = ImageIO.read(new File(bgImagePath));
-            log.info("原始背景图片尺寸: {}x{}", originalBgImage.getWidth(), originalBgImage.getHeight());
+//            log.info("原始背景图片尺寸: {}x{}", originalBgImage.getWidth(), originalBgImage.getHeight());
 
             // 获取目标画布尺寸
             int targetWidth = bibParam.getCanvasWidth() != null ? bibParam.getCanvasWidth() : originalBgImage.getWidth();
@@ -303,11 +209,11 @@ public class NumberController {
 
             // 将原始背景图片拉伸/缩放到目标画布尺寸
             g2d.drawImage(originalBgImage, 0, 0, targetWidth, targetHeight, null);
-            log.info("背景图片已调整到目标尺寸: {}x{}", targetWidth, targetHeight);
+//            log.info("背景图片已调整到目标尺寸: {}x{}", targetWidth, targetHeight);
 
             // 绘制Logo(如果存在)
             if (logoImagePath != null && new File(logoImagePath).exists()) {
-                log.info("开始绘制Logo - 路径: {}", logoImagePath);
+//                log.info("开始绘制Logo - 路径: {}", logoImagePath);
                 drawLogoOnTemplate(g2d, logoImagePath, bibParam, bgImage.getWidth(), bgImage.getHeight());
             } else {
                 log.warn("Logo图片不存在或路径为空: {}", logoImagePath);
@@ -315,7 +221,7 @@ public class NumberController {
 
             // 绘制赛事名称(如果存在)
             if (StringUtils.isNotBlank(bibParam.getEventName())) {
-                log.info("开始绘制赛事名称: {}", bibParam.getEventName());
+//                log.info("开始绘制赛事名称: {}", bibParam.getEventName());
                 drawEventNameOnTemplate(g2d, bibParam.getEventName(), bibParam, bgImage.getWidth(), bgImage.getHeight());
             } else {
                 log.warn("赛事名称为空");
@@ -326,7 +232,7 @@ public class NumberController {
             // 转换为字节数组
             ByteArrayOutputStream baos = new ByteArrayOutputStream();
             ImageIO.write(bgImage, "PNG", baos);
-            log.info("画布模版生成完成,大小: {} bytes", baos.size());
+//            log.info("画布模版生成完成,大小: {} bytes", baos.size());
             return baos.toByteArray();
 
         } catch (Exception e) {
@@ -342,7 +248,7 @@ public class NumberController {
         try {
             // 检查Logo图片路径是否为空
             if (logoImagePath == null || logoImagePath.isEmpty()) {
-                log.info("Logo图片路径为空,跳过Logo绘制");
+//                log.info("Logo图片路径为空,跳过Logo绘制");
                 return;
             }
 
@@ -360,7 +266,7 @@ public class NumberController {
                 return;
             }
 
-            log.info("Logo图片尺寸: {}x{}", logoImage.getWidth(), logoImage.getHeight());
+//            log.info("Logo图片尺寸: {}x{}", logoImage.getWidth(), logoImage.getHeight());
 
             // 计算Logo位置(直接使用前端传入的百分比坐标)
             Double logoX = bibParam.getLogoX();
@@ -371,13 +277,13 @@ public class NumberController {
                 // 直接使用前端传入的像素坐标
                 x = logoX.intValue();
                 y = logoY.intValue();
-                log.info("Logo坐标转换 - 前端传入像素坐标: ({}, {}), 实际图片坐标: ({}, {})",
-                    logoX, logoY, x, y);
+//                log.info("Logo坐标转换 - 前端传入像素坐标: ({}, {}), 实际图片坐标: ({}, {})",
+//                    logoX, logoY, x, y);
             } else {
                 // 使用默认位置(左上角)
                 x = (int) (4.17 * canvasWidth / 100.0);  // 4.17%
                 y = (int) (6.25 * canvasHeight / 100.0); // 6.25%
-                log.info("使用默认Logo位置: ({}, {})", x, y);
+//                log.info("使用默认Logo位置: ({}, {})", x, y);
             }
 
             // 使用传入的Logo尺寸(前端已计算好最终尺寸)
@@ -386,7 +292,7 @@ public class NumberController {
                 // 使用前端传入的Logo尺寸(已包含所有缩放计算)
                 scaledWidth = bibParam.getLogoWidth();
                 scaledHeight = bibParam.getLogoHeight();
-                log.info("使用前端计算的Logo尺寸: {}x{}", scaledWidth, scaledHeight);
+//                log.info("使用前端计算的Logo尺寸: {}x{}", scaledWidth, scaledHeight);
             }
 
             // 实现前端的translateX(-50%)水平居中效果
@@ -402,8 +308,8 @@ public class NumberController {
 
             // 绘制Logo
             g2d.drawImage(logoImage, x, y, scaledWidth, scaledHeight, null);
-            log.info("Logo绘制完成 - 位置: ({}, {}), 尺寸: {}x{}, 画布尺寸: {}x{}",
-                x, y, scaledWidth, scaledHeight, canvasWidth, canvasHeight);
+//            log.info("Logo绘制完成 - 位置: ({}, {}), 尺寸: {}x{}, 画布尺寸: {}x{}",
+//                x, y, scaledWidth, scaledHeight, canvasWidth, canvasHeight);
 
         } catch (Exception e) {
             log.error("绘制Logo失败", e);
@@ -421,14 +327,19 @@ public class NumberController {
 
             // 使用前端传递的字体名称,否则使用默认字体
             String fontName = bibParam.getFontName();
+            Long fontOssId = bibParam.getFontOssId();
             Font font;
-            if (fontName == null || "simhei".equalsIgnoreCase(fontName) || "SimHei".equals(fontName)) {
+            if (fontOssId != null) {
+                // 如果指定了云端字体,优先从 OSS 加载
+                font = gameEventService.loadCustomFont(fontOssId, fontSize);
+//                log.info("赛事名称使用云端字体: {}, ossId: {}", font, fontOssId);
+            } else if (fontName == null || "simhei".equalsIgnoreCase(fontName) || "SimHei".equals(fontName)) {
                 // 如果指定了黑体或未指定字体,则使用自定义加载的黑体字体,确保线上环境字体一致
                 font = gameEventService.loadCustomFont("simhei", fontSize);
-                log.warn("使用自定义黑体字体: {}", font);
+//                log.info("赛事名称使用自定义黑体字体: {}", font);
             } else {
                 font = gameEventService.loadCustomFont(fontName, fontSize);
-                log.warn("使用自定义字体: {}", font);
+//                log.info("赛事名称使用自定义字体: {}", font);
             }
             g2d.setFont(font);
 
@@ -443,8 +354,8 @@ public class NumberController {
 
             // 居中绘制 - 与前端 transform: translateX(-50%) 和 transformOrigin: 'top center' 保持一致
             g2d.drawString(eventName, x - textWidth / 2, y + textAscent);
-            log.info("赛事名称绘制完成 - 位置: ({}, {}), 字体大小: {}, 画布尺寸: {}x{}",
-                x, y, fontSize, canvasWidth, canvasHeight);
+//            log.info("赛事名称绘制完成 - 位置: ({}, {}), 字体大小: {}, 画布尺寸: {}x{}",
+//                x, y, fontSize, canvasWidth, canvasHeight);
 
         } catch (Exception e) {
             log.error("绘制赛事名称失败", e);

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

@@ -52,21 +52,11 @@ public class GameTeam extends TenantEntity {
      */
     private String leader;
 
-    /**
-     * 队员列表
-     */
-    private String athleteValue;
-
     /**
      * 参与项目列表
      */
     private String projectValue;
 
-    /**
-     * 人数
-     */
-    private Integer athleteNum;
-
     /**
      * 号码段
      */

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

@@ -64,8 +64,6 @@ public class GameTeamBo extends BaseEntity {
     /**
      * 队员列表
      */
-//    @NotBlank(message = "队员列表不能为空", groups = { AddGroup.class, EditGroup.class })
-    private String athleteValue;
     private List<Long> athleteList;
 
     private String projectValue;

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

@@ -35,6 +35,8 @@ public class GenerateBibBo implements Serializable {
     private String fontName;
     private Integer fontSize;
     private Integer fontColor;
+    @JsonProperty("fontOssId")
+    private Long fontOssId;
     @JsonProperty("numberFontSize")
     private Integer numberFontSize;
 

+ 68 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameAthleteImportVo.java

@@ -0,0 +1,68 @@
+package org.dromara.system.domain.vo;
+
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import org.dromara.common.excel.annotation.ExcelDictFormat;
+import org.dromara.common.excel.convert.ExcelDictConvert;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 参赛队员导入对象
+ *
+ * @author zlt
+ * @date 2025-07-30
+ */
+@Data
+@ExcelIgnoreUnannotated
+public class GameAthleteImportVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 运动员编号
+     */
+    @ExcelProperty(value = "号码")
+    private String athleteCode;
+
+    /**
+     * 姓名
+     */
+    @ExcelProperty(value = "姓名")
+    private String name;
+
+    /**
+     * 性别
+     */
+    @ExcelProperty(value = "性别", converter = ExcelDictConvert.class)
+    @ExcelDictFormat(dictType = "sys_user_sex")
+    private String gender;
+
+    /**
+     * 年龄
+     */
+    @ExcelProperty(value = "年龄")
+    private Long age;
+
+    /**
+     * 手机号
+     */
+    @ExcelProperty(value = "手机号")
+    private String phone;
+
+    /**
+     * 队伍名称
+     */
+    @ExcelProperty(value = "队伍")
+    private String teamName;
+
+    /**
+     * 备注
+     */
+    @ExcelProperty(value = "备注")
+    private String remark;
+
+}

+ 61 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameTeamImportVo.java

@@ -0,0 +1,61 @@
+package org.dromara.system.domain.vo;
+
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.system.domain.GameTeam;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 参赛队伍导入对象
+ *
+ * @author zlt
+ * @date 2025-07-30
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = GameTeam.class)
+public class GameTeamImportVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 队伍编号
+     */
+    @ExcelProperty(value = "队伍编号")
+    private String teamCode;
+
+    /**
+     * 队伍名称
+     */
+    @ExcelProperty(value = "队伍名称")
+    private String teamName;
+
+    /**
+     * 排名分组名
+     */
+    @ExcelProperty(value = "分组名")
+    private String rgName;
+
+    /**
+     * 团队描述
+     */
+    @ExcelProperty(value = "团队描述")
+    private String teamDescribe;
+
+    /**
+     * 领队
+     */
+    @ExcelProperty(value = "领队")
+    private String leader;
+
+    /**
+     * 排名分组ID (内部使用)
+     */
+    private Long rgId;
+
+}

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

@@ -27,13 +27,11 @@ public class GameTeamVo implements Serializable {
     @Serial
     private static final long serialVersionUID = 1L;
 
-    @ExcelProperty(value = "队伍Id")
     private Long teamId;
 
     /**
      * 赛事ID
      */
-    // @ExcelProperty(value = "赛事ID")
     private Long eventId;
 
     /**
@@ -44,7 +42,6 @@ public class GameTeamVo implements Serializable {
     /**
      * 赛事名称
      */
-    // @ExcelProperty(value = "赛事名称")
     private String eventName;
 
     /**
@@ -62,8 +59,6 @@ public class GameTeamVo implements Serializable {
     /**
      * 队员列表
      */
-    // @ExcelProperty(value = "队员列表")
-    private String athleteValue;
     private List<Long> athleteList;
 
     private String projectValue;
@@ -108,14 +103,14 @@ public class GameTeamVo implements Serializable {
     /**
      * 状态(0正常 1停用)
      */
-//    @ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
+    // @ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
     @ExcelDictFormat(readConverterExp = "0=正常,1=停用")
     private String status;
 
     /**
      * 备注
      */
-//    @ExcelProperty(value = "备注")
+    // @ExcelProperty(value = "备注")
     private String remark;
 
 }

+ 8 - 6
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/GameTeamMapper.java

@@ -85,13 +85,15 @@ public interface GameTeamMapper extends BaseMapperPlus<GameTeam, GameTeamVo> {
      * @return 队伍列表
      */
     @Select("<script>" +
-        "SELECT * FROM game_team " +
-        "WHERE del_flag = '0' and(" +
-        "<foreach collection='athleteIds' item='athleteId' separator=' OR '>" +
-        "JSON_CONTAINS(athlete_value, CAST(#{athleteId} AS JSON))" +
-        "</foreach>" +")"+
+        "SELECT DISTINCT gt.* FROM game_team gt " +
+        "INNER JOIN game_athlete ga ON ga.team_id = gt.team_id " +
+        "WHERE gt.del_flag = '0' AND ga.del_flag = '0' " +
+        "AND ga.athlete_id IN " +
+        "<foreach collection='athleteIds' item='athleteId' open='(' separator=',' close=')'>" +
+        "#{athleteId}" +
+        "</foreach>" +
         "</script>")
-    List<GameTeamBo> findByAthleteIds(Collection<Long> athleteIds);
+    List<GameTeamBo> findByAthleteIds(@Param("athleteIds") Collection<Long> athleteIds);
 
     /**
      * 查询队伍列表并包含分组名

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

@@ -6,6 +6,7 @@ import org.dromara.system.domain.GameAthlete;
 import org.dromara.system.domain.bo.GameAthleteBo;
 import org.dromara.system.domain.bo.ProjectSelectionValidationBo;
 import org.dromara.system.domain.vo.AthleteScoreVo;
+import org.dromara.system.domain.vo.GameAthleteImportVo;
 import org.dromara.system.domain.vo.GameAthleteVo;
 import org.dromara.system.domain.vo.GameTeamVo;
 
@@ -63,6 +64,8 @@ public interface IGameAthleteService {
 
     List<GameAthleteVo> exportData(GameAthleteBo bo);
 
+    String importAthletes(List<GameAthleteImportVo> list, Long eventId);
+
     /**
      * 新增参赛队员
      *

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

@@ -164,4 +164,6 @@ public interface IGameEventService {
 
     Font loadCustomFont(String fontPath, int fontSize);
 
+    Font loadCustomFont(Long ossId, int fontSize);
+
 }

+ 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);
 }

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

@@ -1,6 +1,7 @@
 package org.dromara.system.service;
 
 import org.dromara.system.domain.GameTeam;
+import org.dromara.system.domain.vo.GameTeamImportVo;
 import org.dromara.system.domain.vo.GameTeamVo;
 import org.dromara.system.domain.bo.GameTeamBo;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
@@ -144,4 +145,12 @@ public interface IGameTeamService {
      * @return 队伍信息
      */
     List<GameTeamBo> findByAthleteIds(Collection<Long> athleteIds);
+    /**
+     * 导入队伍数据
+     *
+     * @param teamList        队伍数据列表
+     * @param isUpdateSupport 是否支持更新,如果已存在,则进行更新数据
+     * @return 结果
+     */
+    String importTeam(List<GameTeamImportVo> teamList, Boolean isUpdateSupport);
 }

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

@@ -102,7 +102,7 @@ public class ArticleServiceImpl implements IArticleService {
     private LambdaQueryWrapper<Article> buildQueryWrapper(ArticleBo bo) {
         Map<String, Object> params = bo.getParams();
         LambdaQueryWrapper<Article> lqw = Wrappers.lambdaQuery();
-        lqw.orderByAsc(Article::getId);
+        lqw.orderByAsc(Article::getCreateTime);
         lqw.eq(bo.getEventId() != null, Article::getEventId, bo.getEventId());
 
         // 通过名称模糊查询

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

@@ -25,6 +25,8 @@ import org.dromara.system.domain.bo.GameTeamBo;
 import org.dromara.system.domain.bo.ProjectSelectionValidationBo;
 import org.dromara.system.domain.constant.GameEventConstant;
 import org.dromara.system.domain.vo.*;
+import org.dromara.common.core.exception.ServiceException;
+import cn.hutool.core.bean.BeanUtil;
 import org.dromara.system.mapper.GameAthleteMapper;
 import org.dromara.system.mapper.GameEventProjectMapper;
 import org.dromara.system.mapper.GameScoreMapper;
@@ -458,7 +460,7 @@ public class GameAthleteServiceImpl implements IGameAthleteService {
     private LambdaQueryWrapper<GameAthlete> buildQueryWrapper(GameAthleteBo bo) {
         Map<String, Object> params = bo.getParams();
         LambdaQueryWrapper<GameAthlete> lqw = Wrappers.lambdaQuery();
-        lqw.orderByAsc(GameAthlete::getAthleteId);
+        lqw.orderByAsc(GameAthlete::getCreateTime);
         lqw.eq(bo.getEventId() != null, GameAthlete::getEventId, bo.getEventId());
         // 通过赛事名称模糊查询
         if (StringUtils.isNotBlank(bo.getEventName())) {
@@ -582,45 +584,81 @@ public class GameAthleteServiceImpl implements IGameAthleteService {
         if (isValid) {
             //TODO 做一些业务上的校验,判断是否需要校验
         }
-        //批量删除前,删除队伍中的关联数据
-        removeAthleteFromTeams(ids);
-        //批量删除前,删除运动员的相关成绩
+        // 重构:由于废弃了队伍表的 athlete_value 冗余字段,删除运动员时不再需要去队伍表清理关联。
+        // 批量删除前,删除运动员的相关成绩
         gameScoreMapper.deleteByAthleteIds(ids);
         return baseMapper.deleteByIds(ids) > 0;
     }
 
-    /**
-     * 从所有队伍的项目列表中移除指定运动员
-     *
-     * @param athleteIds 要移除的项目ID列表
-     */
+
+    @Override
     @Transactional(rollbackFor = Exception.class)
-    public void removeAthleteFromTeams(Collection<Long> athleteIds) {
-        try {
-            if (CollectionUtils.isEmpty(athleteIds)) {
-                return;
-            }
+    public String importAthletes(List<GameAthleteImportVo> list, Long eventId) {
+        if (CollUtil.isEmpty(list)) {
+            return "导入运动员数据不能为空!";
+        }
+
+        // 1. 获取导入数据中涉及的所有队伍名称,减少内存占用并提升查询效率
+        Set<String> importTeamNames = list.stream()
+            .map(GameAthleteImportVo::getTeamName)
+            .filter(StringUtils::isNotBlank)
+            .collect(Collectors.toSet());
 
-            // 查询所有包含这些项目的运动员
-            List<GameTeamBo> teams = gameTeamService.findByAthleteIds(athleteIds);
+        Map<String, Long> teamMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(importTeamNames)) {
+            // 只查询当前赛事中且在 Excel 范围内的队伍,且仅查询 ID 和 名称 两个字段
+            teamMap = gameTeamMapper.selectList(new LambdaQueryWrapper<GameTeam>()
+                    .eq(GameTeam::getEventId, eventId)
+                    .in(GameTeam::getTeamName, importTeamNames)
+                    .select(GameTeam::getTeamId, GameTeam::getTeamName))
+                .stream().collect(Collectors.toMap(GameTeam::getTeamName, GameTeam::getTeamId, (v1, v2) -> v1));
+        }
 
-            log.info("找到 {} 个队伍需要更新运动员关联,运动员ID列表: {}", teams.size(), athleteIds);
+        int successNum = 0;
+        int failureNum = 0;
+        StringBuilder successMsg = new StringBuilder();
+        StringBuilder failureMsg = new StringBuilder();
 
-            for (GameTeamBo team : teams) {
-                for (Long athleteId : athleteIds){
-                    if (team.getAthleteList() != null) {
-                        team.getAthleteList().remove(athleteId);
-                        log.info("已从队伍 {} 的运动员列表中移除项目 {}", team.getTeamName(), athleteId);
-                    }
-                }
-                gameTeamService.updateByBo(team);
+        List<GameAthlete> saveList = new ArrayList<>();
+
+        for (GameAthleteImportVo vo : list) {
+            // 校验姓名和号码是否为空
+            if (StringUtils.isBlank(vo.getName()) || StringUtils.isBlank(vo.getAthleteCode())) {
+                failureNum++;
+                failureMsg.append("<br/>" + failureNum + "、导入数据必填项(姓名或号码)不能为空");
+                continue;
+            }
+
+            // 校验队伍是否存在
+            if (StringUtils.isBlank(vo.getTeamName()) || !teamMap.containsKey(vo.getTeamName())) {
+                failureNum++;
+                failureMsg.append("<br/>" + failureNum + "、运动员 " + vo.getName() + " 关联失败:队伍「" + vo.getTeamName() + "」在当前赛事中不存在");
+                continue;
             }
 
-            log.info("队伍与运动员的关联清理完成,共处理 {} 个队伍", teams.size());
-        } catch (Exception e) {
-            log.error("清理队伍与运动员的关联失败,运动员ID列表: {}", athleteIds, e);
-            throw e;
+            // 转换实体并设置必要字段
+            GameAthlete athlete = new GameAthlete();
+            BeanUtil.copyProperties(vo, athlete);
+            athlete.setEventId(eventId);
+            athlete.setTeamId(teamMap.get(vo.getTeamName()));
+            // 明确不导入项目信息,避免覆盖原有数据(如果是更新的话)
+            athlete.setProjectValue(null);
+
+            saveList.add(athlete);
+            successNum++;
+        }
+
+        if (CollUtil.isNotEmpty(saveList)) {
+            // 重构:只需保存运动员即可,运动员表中的 team_id 已是唯一事实来源。
+            // 队伍表中的 athlete_value 将被废弃,不再需要同步更新。
+            this.saveOrEditBatch(saveList);
+        }
+
+        if (failureNum > 0) {
+            failureMsg.insert(0, "导入完毕,共 " + successNum + " 条成功," + failureNum + " 条失败,错误如下:");
+            throw new ServiceException(failureMsg.toString());
         }
+        return "恭喜您,数据已全部导入成功!共 " + successNum + " 条。";
     }
 
     /**
@@ -695,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);
@@ -706,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 -> {
@@ -739,7 +777,7 @@ public class GameAthleteServiceImpl implements IGameAthleteService {
             })
             .collect(Collectors.toList());
 
-        log.info("过滤后参与项目 {} 的运动员数量: {}", projectId, filteredAthletes.size());
+//        log.info("过滤后参与项目 {} 的运动员数量: {}", projectId, filteredAthletes.size());
         return filteredAthletes;
     }
 

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

@@ -34,6 +34,8 @@ import org.dromara.system.mapper.GameEventMapper;
 import org.dromara.system.config.FileUploadConfig;
 import org.dromara.system.service.*;
 import org.dromara.system.domain.vo.SysOssVo;
+import org.dromara.common.oss.factory.OssFactory;
+import org.dromara.common.oss.core.OssClient;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
@@ -140,6 +142,29 @@ public class GameEventServiceImpl implements IGameEventService {
         }
     }
 
+    @Override
+    public Font loadCustomFont(Long ossId, int fontSize) {
+        if (ossId == null) {
+            return new Font("SimHei", Font.BOLD, fontSize);
+        }
+        try {
+            SysOssVo sysOss = sysOssService.getById(ossId);
+            if (sysOss == null) {
+                log.warn("未找到OSS字体文件, ossId: {}", ossId);
+                return new Font("SimHei", Font.BOLD, fontSize);
+            }
+            OssClient storage = OssFactory.instance(sysOss.getService());
+            try (InputStream is = storage.getObjectContent(sysOss.getFileName())) {
+                Font font = Font.createFont(Font.TRUETYPE_FONT, is);
+                log.info("成功从OSS加载字体文件: {}", sysOss.getOriginalName());
+                return font.deriveFont(Font.BOLD, fontSize);
+            }
+        } catch (Exception e) {
+            log.error("从OSS加载字体失败,ossId: {}", ossId, e);
+            return new Font("SimHei", Font.BOLD, fontSize);
+        }
+    }
+
     // 预览框尺寸(固定)
     private static final int previewWidth = 600;
     private static final int previewHeight = 400;
@@ -1216,8 +1241,13 @@ public class GameEventServiceImpl implements IGameEventService {
 
             // 确保字体名称匹配系统可用字体
             String fontName = bibParam.getFontName();
+            Long fontOssId = bibParam.getFontOssId();
             Font font;
-            if (fontName == null || "simhei".equalsIgnoreCase(fontName) || "SimHei".equals(fontName)) {
+            if (fontOssId != null) {
+                // 如果指定了云端字体,优先从 OSS 加载
+                font = loadCustomFont(fontOssId, fontSize);
+                log.info("号码使用云端字体: {}, ossId: {}", font, fontOssId);
+            } else if (fontName == null || "simhei".equalsIgnoreCase(fontName) || "SimHei".equals(fontName)) {
                 // 如果指定了黑体或未指定字体,则使用自定义加载的黑体字体,确保线上环境字体一致
                 font = loadCustomFont("simhei", fontSize);
                 log.info("号码使用自定义黑体字体: {}", font);

+ 399 - 37
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,42 +572,47 @@ public class GameScoreServiceImpl implements IGameScoreService {
 
         // 1. 获取项目配置信息
         GameEventProjectVo project = gameEventProjectService.queryById(bo.getProjectId());
-        if (project == null) return false;
+        if (project == null)
+            return false;
 
-        // 自动分析汇总成绩和失误
-        if (bo.getDetails() != null && !bo.getDetails().isEmpty()) {
-            calculateAndSetAggregatePerformance(bo, project);
-        }
+        // 判断是否是“纯排名计算”请求(不带具体的录入对象)
+        boolean isOnlyRecalculate = bo.getAthleteId() == null && bo.getTeamId() == null
+                && (bo.getScoreId() == null || bo.getScoreId() == 0);
 
-        Boolean result = false;
+        Boolean result = true;
 
-        if (ProjectClassification.TEAM.getValue().equals(project.getClassification())) {
-            // 团体项目:为队伍中的所有运动员创建或更新成绩记录
-            // log.info("处理团体项目成绩更新");
-            result = handleTeamScoreUpdate(bo);
-        } else {
-            // 个人项目:直接更新或插入
-            // log.info("处理个人项目成绩更新,scoreId: {}", bo.getScoreId());
-            if (bo.getScoreId() != null && bo.getScoreId() > 0) {
-                result = updateByBo(bo);
-                // log.info("更新现有成绩记录,结果: {}", result);
+        if (!isOnlyRecalculate) {
+            // 自动分析汇总成绩和失误
+            if (bo.getDetails() != null && !bo.getDetails().isEmpty()) {
+                calculateAndSetAggregatePerformance(bo, project);
+            }
+
+            if (ProjectClassification.TEAM.getValue().equals(project.getClassification())) {
+                // 团体项目:为队伍中的所有运动员创建或更新成绩记录
+                result = handleTeamScoreUpdate(bo);
             } else {
-                result = insertByBo(bo);
-                // log.info("插入新成绩记录,结果: {}", result);
+                // 个人项目:直接更新或插入
+                if (bo.getScoreId() != null && bo.getScoreId() > 0) {
+                    result = updateByBo(bo);
+                } else {
+                    result = insertByBo(bo);
+                }
             }
-        }
 
-        if (result) {
-            // 个人项目:如果传了明细,则保存 (团体项目已在 handleTeamScoreUpdate 中处理过)
-            if ("0".equals(project.getClassification()) && bo.getDetails() != null && !bo.getDetails().isEmpty()) {
-                saveScoreDetails(bo.getScoreId(), bo.getProjectId(), bo.getAthleteId(), null, bo.getDetails());
+            if (result) {
+                // 个人项目:如果传了明细,则保存 (团体项目已在 handleTeamScoreUpdate 中处理过)
+                if (ProjectClassification.SINGLE.getValue().equals(project.getClassification())
+                        && bo.getDetails() != null && !bo.getDetails().isEmpty()) {
+                    saveScoreDetails(bo.getScoreId(), bo.getProjectId(), bo.getAthleteId(), null, bo.getDetails());
+                }
             }
+        }
 
+        if (result) {
             // 重新计算排名和积分
             recalculateRankingsAndPoints(bo.getEventId(), bo.getProjectId());
         }
 
-        // log.info("成绩更新处理完成,结果: {}", result);
         return result;
     }
 
@@ -618,7 +635,8 @@ public class GameScoreServiceImpl implements IGameScoreService {
                 .filter(Objects::nonNull)
                 .toList();
 
-        if (values.isEmpty()) return;
+        if (values.isEmpty())
+            return;
 
         BigDecimal aggregate = BigDecimal.ZERO;
 
@@ -694,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);
@@ -774,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;
@@ -800,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 分配名次并分发给队员
@@ -810,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;
                     }
                 }
@@ -846,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)
@@ -2287,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);
+    }
 }

+ 233 - 47
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameTeamServiceImpl.java

@@ -1,8 +1,6 @@
 package org.dromara.system.service.impl;
 
 import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.ObjectUtil;
-import cn.hutool.json.JSONUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -17,18 +15,22 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
 import org.dromara.common.redis.utils.RedisUtils;
 import org.dromara.system.domain.GameAthlete;
 import org.dromara.system.domain.GameEvent;
+import org.dromara.system.domain.GameRankGroup;
 import org.dromara.system.domain.GameTeam;
+import org.dromara.system.domain.bo.GameRankGroupBo;
 import org.dromara.system.domain.bo.GameTeamBo;
 import org.dromara.system.domain.constant.GameEventConstant;
 import org.dromara.system.domain.vo.GameRankGroupVo;
+import org.dromara.system.domain.vo.GameTeamImportVo;
 import org.dromara.system.domain.vo.GameTeamVo;
 import org.dromara.system.mapper.GameAthleteMapper;
 import org.dromara.system.mapper.GameEventMapper;
+import org.dromara.system.mapper.GameRankGroupMapper;
 import org.dromara.system.mapper.GameTeamMapper;
-import org.dromara.system.service.IGameEventProjectService;
 import org.dromara.system.service.IGameRankGroupService;
 import org.dromara.system.service.IGameTeamService;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.*;
 import java.util.stream.Collectors;
@@ -51,7 +53,7 @@ public class GameTeamServiceImpl implements IGameTeamService {
     private final GameEventMapper gameEventMapper;
 
     private final IGameRankGroupService gameRankGroupService;
-    private final IGameEventProjectService gameEventProjectService;
+    private final GameRankGroupMapper gameRankGroupMapper;
 
     /**
      * 查询参赛队伍
@@ -65,10 +67,13 @@ public class GameTeamServiceImpl implements IGameTeamService {
         if (vo == null) {
             return null;
         }
-        if (vo.getAthleteValue() != null) {
-            log.info("teamId:{}, athleteValue:{}", teamId, vo.getAthleteValue());
-            vo.setAthleteList(JSONUtil.toList(vo.getAthleteValue(), Long.class));
-        }
+        // 重构:实时查询属于该队伍的运动员ID列表
+        List<Long> athleteIds = gameAthleteMapper.selectList(Wrappers.<GameAthlete>lambdaQuery()
+                .eq(GameAthlete::getTeamId, teamId)
+                .select(GameAthlete::getAthleteId))
+                .stream().map(GameAthlete::getAthleteId).collect(Collectors.toList());
+        vo.setAthleteList(athleteIds);
+        vo.setAthleteNum(athleteIds.size());
         return vo;
     }
 
@@ -92,13 +97,27 @@ public class GameTeamServiceImpl implements IGameTeamService {
         LambdaQueryWrapper<GameTeam> lqw = buildQueryWrapper(bo);
         // Page<GameTeamVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
         Page<GameTeamVo> result = baseMapper.selectPageTeamListWithProjects(pageQuery.build(), lqw);
-        // 处理队员列表 JSON 转换
-        result.getRecords().forEach(vo -> {
-            if (StringUtils.isNotBlank(vo.getAthleteValue())) {
-                List<Long> athleteIds = JSONUtil.toList(vo.getAthleteValue(), Long.class);
+
+        // 重构:为每一页的队伍动态加载人数和成员列表
+        if (CollUtil.isNotEmpty(result.getRecords())) {
+            List<Long> teamIds = result.getRecords().stream().map(GameTeamVo::getTeamId).collect(Collectors.toList());
+
+            // 批量查询所有相关运动员
+            List<GameAthlete> allAthletes = gameAthleteMapper.selectList(Wrappers.<GameAthlete>lambdaQuery()
+                    .in(GameAthlete::getTeamId, teamIds)
+                    .select(GameAthlete::getAthleteId, GameAthlete::getTeamId));
+
+            // 按队伍ID分组
+            Map<Long, List<Long>> teamAthleteMap = allAthletes.stream()
+                    .collect(Collectors.groupingBy(GameAthlete::getTeamId,
+                            Collectors.mapping(GameAthlete::getAthleteId, Collectors.toList())));
+
+            result.getRecords().forEach(vo -> {
+                List<Long> athleteIds = teamAthleteMap.getOrDefault(vo.getTeamId(), new ArrayList<>());
                 vo.setAthleteList(athleteIds);
-            }
-        });
+                vo.setAthleteNum(athleteIds.size());
+            });
+        }
 
         return TableDataInfo.build(result);
     }
@@ -129,15 +148,14 @@ public class GameTeamServiceImpl implements IGameTeamService {
         lqw.and(bo.getRgId() != null,
                 wrapper -> wrapper.eq(GameTeam::getRgId, bo.getRgId())
                         .or()
-                        .apply("rg_id IN (SELECT rg_id FROM game_rank_group WHERE FIND_IN_SET({0}, ancestors))", bo.getRgId())
-        );
+                        .apply("rg_id IN (SELECT rg_id FROM game_rank_group WHERE FIND_IN_SET({0}, ancestors))",
+                                bo.getRgId()));
         lqw.eq(StringUtils.isNotBlank(bo.getLeader()), GameTeam::getLeader, bo.getLeader());
-        lqw.eq(StringUtils.isNotBlank(bo.getAthleteValue()), GameTeam::getAthleteValue, bo.getAthleteValue());
+        // 重构:废弃 athleteValue 字段查询,此处不再支持根据原始 JSON 字符串查询
         lqw.eq(StringUtils.isNotBlank(bo.getProjectValue()), GameTeam::getProjectValue, bo.getProjectValue());
         lqw.apply(bo.getProjectId() != null,
                 "EXISTS (SELECT 1 FROM game_athlete ga WHERE ga.team_id = gt.team_id AND ga.del_flag = '0' AND (JSON_CONTAINS(ga.project_value, CAST({0} AS CHAR)) OR JSON_CONTAINS(ga.project_value, CONCAT('\"', {0}, '\"'))))",
                 bo.getProjectId());
-        lqw.eq(bo.getAthleteNum() != null, GameTeam::getAthleteNum, bo.getAthleteNum());
         lqw.eq(StringUtils.isNotBlank(bo.getNumberRange()), GameTeam::getNumberRange, bo.getNumberRange());
         lqw.eq(StringUtils.isNotBlank(bo.getTeamDescribe()), GameTeam::getTeamDescribe, bo.getTeamDescribe());
         lqw.eq(StringUtils.isNotBlank(bo.getStatus()), GameTeam::getStatus, bo.getStatus());
@@ -163,13 +181,24 @@ public class GameTeamServiceImpl implements IGameTeamService {
         }
         LambdaQueryWrapper<GameTeam> lqw = buildQueryWrapper(bo);
         List<GameTeamVo> list = baseMapper.selectTeamListWithProjects(lqw);
-        // 处理队员列表 JSON 转换
-        list.forEach(vo -> {
-            if (StringUtils.isNotBlank(vo.getAthleteValue())) {
-                List<Long> athleteIds = JSONUtil.toList(vo.getAthleteValue(), Long.class);
+
+        // 重构:动态加载成员列表和人数
+        if (CollUtil.isNotEmpty(list)) {
+            List<Long> teamIds = list.stream().map(GameTeamVo::getTeamId).collect(Collectors.toList());
+            List<GameAthlete> allAthletes = gameAthleteMapper.selectList(Wrappers.<GameAthlete>lambdaQuery()
+                    .in(GameAthlete::getTeamId, teamIds)
+                    .select(GameAthlete::getAthleteId, GameAthlete::getTeamId));
+
+            Map<Long, List<Long>> teamAthleteMap = allAthletes.stream()
+                    .collect(Collectors.groupingBy(GameAthlete::getTeamId,
+                            Collectors.mapping(GameAthlete::getAthleteId, Collectors.toList())));
+
+            list.forEach(vo -> {
+                List<Long> athleteIds = teamAthleteMap.getOrDefault(vo.getTeamId(), new ArrayList<>());
                 vo.setAthleteList(athleteIds);
-            }
-        });
+                vo.setAthleteNum(athleteIds.size());
+            });
+        }
         return list;
     }
 
@@ -189,9 +218,9 @@ public class GameTeamServiceImpl implements IGameTeamService {
                 bo.setEventId((Long) cacheObject);
             }
         }
-        if (bo.getAthleteList() != null) {
-            bo.setAthleteValue(JSONUtil.toJsonStr(bo.getAthleteList()));
-        }
+//        if (bo.getAthleteList() != null) {
+//            bo.setAthleteValue(JSONUtil.toJsonStr(bo.getAthleteList()));
+//        }
         GameTeam add = MapstructUtils.convert(bo, GameTeam.class);
         validEntityBeforeSave(add);
         // 查询是否重复
@@ -225,9 +254,9 @@ public class GameTeamServiceImpl implements IGameTeamService {
                 bo.setEventId((Long) cacheObject);
             }
         }
-        if (bo.getAthleteList() != null) {
-            bo.setAthleteValue(JSONUtil.toJsonStr(bo.getAthleteList()));
-        }
+//        if (bo.getAthleteList() != null) {
+//            bo.setAthleteValue(JSONUtil.toJsonStr(bo.getAthleteList()));
+//        }
         GameTeam update = MapstructUtils.convert(bo, GameTeam.class);
         validEntityBeforeSave(update);
         return baseMapper.updateById(update) > 0;
@@ -301,10 +330,6 @@ public class GameTeamServiceImpl implements IGameTeamService {
                 Wrappers.lambdaQuery(GameTeam.class)
                         .eq(GameTeam::getEventId, eventId)
                         .in(GameTeam::getTeamName, names));
-        // 批量添加队伍时,运动员列表为空列表而不是null
-        list.forEach(team -> {
-            team.setAthleteValue(new ArrayList<Long>().toString());
-        });
         if (count > 0) {
             throw new ServiceException("导入失败,有队伍名称重复");
         }
@@ -332,9 +357,21 @@ public class GameTeamServiceImpl implements IGameTeamService {
         List<GameTeamVo> list = baseMapper.selectVoList(
                 Wrappers.lambdaQuery(GameTeam.class)
                         .in(GameTeam::getTeamId, teamIds));
-        list.forEach(vo -> {
-            vo.setAthleteList(JSONUtil.toList(vo.getAthleteValue(), Long.class));
-        });
+        if (CollUtil.isNotEmpty(list)) {
+            List<GameAthlete> allAthletes = gameAthleteMapper.selectList(Wrappers.<GameAthlete>lambdaQuery()
+                    .in(GameAthlete::getTeamId, teamIds)
+                    .select(GameAthlete::getAthleteId, GameAthlete::getTeamId));
+
+            Map<Long, List<Long>> teamAthleteMap = allAthletes.stream()
+                    .collect(Collectors.groupingBy(GameAthlete::getTeamId,
+                            Collectors.mapping(GameAthlete::getAthleteId, Collectors.toList())));
+
+            list.forEach(vo -> {
+                List<Long> athleteIds = teamAthleteMap.getOrDefault(vo.getTeamId(), new ArrayList<>());
+                vo.setAthleteList(athleteIds);
+                vo.setAthleteNum(athleteIds.size());
+            });
+        }
         return list;
     }
 
@@ -346,19 +383,23 @@ public class GameTeamServiceImpl implements IGameTeamService {
      * @return 是否更新成功
      */
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public Boolean updateTeamAthletes(Long teamId, List<Long> athleteIds) {
-        GameTeam team = baseMapper.selectById(teamId);
-        if (team == null) {
-            throw new RuntimeException("队伍不存在");
+        // 重构:更新队伍运动员不再修改队伍表字段,而是批量修改运动员表中的 team_id
+
+        // 1. 先将原属于该队伍的所有运动员 team_id 置空
+        // gameAthleteMapper.update(null, Wrappers.<GameAthlete>lambdaUpdate()
+        // .eq(GameAthlete::getTeamId, teamId)
+        // .set(GameAthlete::getTeamId, null));
+
+        // 2. 将传入的运动员列表 team_id 设置为该队伍
+        if (CollUtil.isNotEmpty(athleteIds)) {
+            gameAthleteMapper.update(null, Wrappers.<GameAthlete>lambdaUpdate()
+                    .in(GameAthlete::getAthleteId, athleteIds)
+                    .set(GameAthlete::getTeamId, teamId));
         }
 
-        // 将运动员ID列表转换为JSON字符串
-        String athleteValue = JSONUtil.toJsonStr(athleteIds);
-        team.setAthleteValue(athleteValue);
-        team.setAthleteNum(athleteIds.size());
-
-        // 更新队伍信息
-        return baseMapper.updateById(team) > 0;
+        return true;
     }
 
     /**
@@ -460,4 +501,149 @@ public class GameTeamServiceImpl implements IGameTeamService {
     public List<GameTeamBo> findByAthleteIds(Collection<Long> athleteIds) {
         return baseMapper.findByAthleteIds(athleteIds);
     }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String importTeam(List<GameTeamImportVo> teamList, Boolean isUpdateSupport) {
+        if (CollUtil.isEmpty(teamList)) {
+            throw new ServiceException("导入队伍数据不能为空!");
+        }
+
+        // 1. 获取当前默认赛事ID
+        Long eventId = null;
+        Object cacheObject = RedisUtils.getCacheObject(GameEventConstant.DEFAULT_EVENT_ID);
+        if (cacheObject instanceof Integer) {
+            eventId = ((Integer) cacheObject).longValue();
+        } else if (cacheObject instanceof Long) {
+            eventId = (Long) cacheObject;
+        }
+        if (eventId == null) {
+            throw new ServiceException("未设置当前默认赛事,请先选择赛事!");
+        }
+
+        // 2. 加载所有排名分组,建立 名称 -> ID 映射 (仅查询必要字段节省内存)
+        List<GameRankGroupVo> rankGroups = gameRankGroupMapper.selectVoList(Wrappers.<GameRankGroup>lambdaQuery()
+            .eq(GameRankGroup::getEventId, eventId)
+            .eq(GameRankGroup::getDelFlag, "0")
+            .select(GameRankGroup::getRgId, GameRankGroup::getRgName)
+        );
+        Map<String, Long> rgNameMap = rankGroups.stream()
+            .filter(g -> StringUtils.isNotBlank(g.getRgName()))
+            .collect(Collectors.toMap(GameRankGroupVo::getRgName, GameRankGroupVo::getRgId, (v1, v2) -> v1));
+
+        // 3. 性能优化:批量预查现有队伍 (Name -> Team) 避免循环查询
+        Set<String> importNames = teamList.stream()
+            .map(GameTeamImportVo::getTeamName)
+            .filter(StringUtils::isNotBlank)
+            .collect(Collectors.toSet());
+
+        Map<String, GameTeam> existingTeamMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(importNames)) {
+            List<GameTeam> existingTeams = baseMapper.selectList(Wrappers.<GameTeam>lambdaQuery()
+                .eq(GameTeam::getEventId, eventId)
+                .in(GameTeam::getTeamName, importNames)
+                .eq(GameTeam::getDelFlag, "0"));
+            existingTeamMap = existingTeams.stream()
+                .collect(Collectors.toMap(GameTeam::getTeamName, t -> t, (v1, v2) -> v1));
+        }
+
+        // 4. 性能优化:批量预查现有队伍编号 (Code -> TeamId) 用于快速唯一性校验
+        Set<String> importCodes = teamList.stream()
+            .map(GameTeamImportVo::getTeamCode)
+            .filter(StringUtils::isNotBlank)
+            .collect(Collectors.toSet());
+
+        Map<String, Long> existingCodeMap = new HashMap<>();
+        if (CollUtil.isNotEmpty(importCodes)) {
+            List<GameTeam> codes = baseMapper.selectList(Wrappers.<GameTeam>lambdaQuery()
+                .eq(GameTeam::getEventId, eventId)
+                .in(GameTeam::getTeamCode, importCodes)
+                .eq(GameTeam::getDelFlag, "0")
+                .select(GameTeam::getTeamCode, GameTeam::getTeamId));
+            existingCodeMap = codes.stream()
+                .collect(Collectors.toMap(GameTeam::getTeamCode, GameTeam::getTeamId, (v1, v2) -> v1));
+        }
+
+        int successNum = 0;
+        int failureNum = 0;
+        StringBuilder successMsg = new StringBuilder();
+        StringBuilder failureMsg = new StringBuilder();
+
+        List<GameTeam> insertList = new ArrayList<>();
+        List<GameTeam> updateList = new ArrayList<>();
+
+        for (GameTeamImportVo teamVo : teamList) {
+            try {
+                String teamName = teamVo.getTeamName();
+                if (StringUtils.isBlank(teamName)) {
+                    failureNum++;
+                    failureMsg.append("<br/>").append(failureNum).append("、队伍名称不能为空");
+                    continue;
+                }
+
+                // 关联排名分组 (内存查找)
+                if (StringUtils.isNotBlank(teamVo.getRgName())) {
+                    Long rgId = rgNameMap.get(teamVo.getRgName());
+                    if (rgId == null) {
+                        failureNum++;
+                        failureMsg.append("<br/>").append(failureNum).append("、队伍 [").append(teamName)
+                            .append("] 关联的分组 [").append(teamVo.getRgName()).append("] 不存在,导入失败");
+                        continue;
+                    }
+                    teamVo.setRgId(rgId);
+                }
+
+                // 编号唯一性校验 (内存查找)
+                if (StringUtils.isNotBlank(teamVo.getTeamCode())) {
+                    Long existIdWithCode = existingCodeMap.get(teamVo.getTeamCode());
+                    GameTeam existTeamWithName = existingTeamMap.get(teamName);
+                    // 逻辑:如果编号已存在,但对应的 ID 不是当前名称匹配到的队伍 ID,说明该编号被别的队伍占用了
+                    if (existIdWithCode != null && (existTeamWithName == null || !existIdWithCode.equals(existTeamWithName.getTeamId()))) {
+                        failureNum++;
+                        failureMsg.append("<br/>").append(failureNum).append("、队伍 [").append(teamName).append("] 的编号 [")
+                            .append(teamVo.getTeamCode()).append("] 已被其他队伍使用");
+                        continue;
+                    }
+                }
+
+                GameTeam existingTeam = existingTeamMap.get(teamName);
+                if (existingTeam == null) {
+                    GameTeam team = MapstructUtils.convert(teamVo, GameTeam.class);
+                    team.setEventId(eventId);
+                    insertList.add(team);
+                    successNum++;
+                } else if (isUpdateSupport) {
+                    GameTeam team = MapstructUtils.convert(teamVo, GameTeam.class);
+                    team.setTeamId(existingTeam.getTeamId());
+                    team.setEventId(eventId);
+                    updateList.add(team);
+                    successNum++;
+                } else {
+                    failureNum++;
+                    failureMsg.append("<br/>").append(failureNum).append("、队伍 [").append(teamName).append("] 已存在");
+                }
+            } catch (Exception e) {
+                failureNum++;
+                String msg = "<br/>" + failureNum + "、队伍 [" + teamVo.getTeamName() + "] 导入异常:";
+                failureMsg.append(msg).append(e.getMessage());
+                log.error(msg, e);
+            }
+        }
+
+        // 5. 批量执行数据库写入,大幅提升性能
+        if (CollUtil.isNotEmpty(insertList)) {
+            baseMapper.insertBatch(insertList);
+        }
+        if (CollUtil.isNotEmpty(updateList)) {
+            baseMapper.updateBatchById(updateList);
+        }
+
+        if (failureNum > 0) {
+            failureMsg.insert(0, "很抱歉,导入过程中出现错误!共 " + failureNum + " 条失败,错误如下:");
+            throw new ServiceException(failureMsg.toString());
+        } else {
+            successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条。");
+        }
+        return successMsg.toString();
+    }
 }

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

@@ -225,8 +225,6 @@ public class IEnrollServiceImpl implements IEnrollService {
             gameTeamBo.setEventId(eventId);
             gameTeamBo.setTeamName(teamName);
             gameTeamBo.setLeader(name);
-            gameTeamBo.setAthleteValue(JSONUtil.toJsonStr(List.of(athleteId)));
-            gameTeamBo.setAthleteNum(1);
             gameTeamBo.setNumberRange(startNumber + "-" + endNumber);
             gameTeamBo.setCreateDept(-1L);
             gameTeamBo.setCreateBy(-1L);
@@ -237,13 +235,10 @@ public class IEnrollServiceImpl implements IEnrollService {
             gameTeamService.insertByBo(gameTeamBo);
         } else {
             // 1.2 查询当前队伍中号码段最大的 继续号码
+            // 1.3 报名逻辑简化:不再需要更新队伍表的 athleteValue
             teamId = team.getTeamId();
             String maxNumber = gameAthleteService.queryMaxNumber(teamId);
             athleteCode = String.valueOf(Integer.parseInt(maxNumber) + 1);
-            // 1.3 更新运动员成员
-            List<Long> ids = JSONUtil.toList(team.getAthleteValue(), Long.class);
-            ids.add(athleteId);
-            team.setAthleteValue(JSONUtil.toJsonStr(ids));
 
             GameTeamBo bo = new GameTeamBo();
             BeanUtil.copyProperties(team, bo);
@@ -600,7 +595,7 @@ public class IEnrollServiceImpl implements IEnrollService {
         // 优化:仅查询必要字段进行内存合并
         List<GameTeam> existingTeams = gameTeamMapper.selectList(new LambdaQueryWrapper<GameTeam>()
                 .select(GameTeam::getTeamId, GameTeam::getTeamName, GameTeam::getNumberRange,
-                        GameTeam::getAthleteValue, GameTeam::getProjectValue)
+                         GameTeam::getProjectValue)
                 .eq(GameTeam::getEventId, eventId));
         Map<String, GameTeam> existingTeamMap = existingTeams.stream()
                 .collect(Collectors.toMap(GameTeam::getTeamName, Function.identity()));
@@ -728,8 +723,6 @@ public class IEnrollServiceImpl implements IEnrollService {
                 team.setEventId(eventId);
                 team.setTeamName(teamName);
                 team.setLeader(athletes.get(0).getLeader());
-                team.setAthleteValue(JSONUtil.toJsonStr(athletesId));
-                team.setAthleteNum(athletes.size());
                 team.setProjectValue(JSONUtil.toJsonStr(teamProjectIds));
                 team.setNumberRange(numberRange);
                 team.setStatus("0");
@@ -751,21 +744,6 @@ public class IEnrollServiceImpl implements IEnrollService {
                 existingProjectIds.addAll(teamProjectIds);
                 existingTeam.setProjectValue(JSONUtil.toJsonStr(existingProjectIds));
 
-                // 合并运动员 ID 列表
-                List<Long> currentAthleteIds = new ArrayList<>();
-                if (StringUtils.isNotBlank(existingTeam.getAthleteValue())) {
-                    try {
-                        currentAthleteIds = JSONUtil.toList(existingTeam.getAthleteValue(), Long.class);
-                    } catch (Exception e) {
-                        log.warn("解析队伍{}的原有运动员ID失败", teamName);
-                    }
-                }
-                // 排除重复后合并
-                Set<Long> mergedIds = new LinkedHashSet<>(currentAthleteIds);
-                mergedIds.addAll(athletesId);
-                existingTeam.setAthleteValue(JSONUtil.toJsonStr(mergedIds));
-                existingTeam.setAthleteNum(mergedIds.size());
-
                 updateTeams.add(existingTeam);
             }
         }

+ 0 - 6
ruoyi-modules/ruoyi-game-event/src/main/resources/mapper/system/GameTeamMapper.xml

@@ -25,9 +25,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             gt.team_name,
             gt.team_code,
             gt.leader,
-            gt.athlete_value as athleteValue,
             gt.project_value as projectValue,
-            gt.athlete_num as athleteNum,
             gt.number_range as numberRange,
             gt.team_describe as teamDescribe,
             gt.status,
@@ -59,9 +57,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             gt.team_code,
             (SELECT event_name FROM game_event WHERE event_id = gt.event_id) as eventName,
             (SELECT name FROM game_athlete WHERE athlete_id = gt.leader) as leader,
-            gt.athlete_value as athleteValue,
             gt.project_value as projectValue,
-            gt.athlete_num as athleteNum,
             gt.number_range as numberRange,
             gt.team_describe as teamDescribe,
             gt.status,
@@ -87,9 +83,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             gt.team_code,
             (SELECT event_name FROM game_event WHERE event_id = gt.event_id) as eventName,
             (SELECT name FROM game_athlete WHERE athlete_id = gt.leader) as leader,
-            gt.athlete_value as athleteValue,
             gt.project_value as projectValue,
-            gt.athlete_num as athleteNum,
             gt.number_range as numberRange,
             gt.team_describe as teamDescribe,
             gt.status,