3
0

7 کامیت‌ها 0395b6a156 ... e95cfed032

نویسنده SHA1 پیام تاریخ
  zhou e95cfed032 feat(game-score): 添加成绩详情导出功能 2 هفته پیش
  zhou 347ec55934 feat(enroll): 实现报名表导入校验功能- 新增导入报名表并验证数据接口,支持Excel文件校验- 添加EnrollImportResult、EnrollValidationResult等校验结果实体类 2 هفته پیش
  zhou a709a957d9 feat(game): 添加参赛项目选择验证功能 2 هفته پیش
  zhou e8065a0753 feat(game-event): 增强Logo上传验证和PDF生成容错性 2 هفته پیش
  zhou 3e14ebeff5 feat(game-event): 更新号码布生成功能 2 هفته پیش
  zhou c8e0039141 refactor(game):重构赛事积分计算逻辑以提升准确性- 调整成绩查询范围为当前赛事 2 هفته پیش
  zhou daef5b2a4d feat(game): 添加运动员和队伍编号唯一性校验- 在保存运动员信息前校验编号唯一性 2 هفته پیش
26فایلهای تغییر یافته به همراه1449 افزوده شده و 174 حذف شده
  1. 30 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/EnrollController.java
  2. 9 4
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameAthleteController.java
  3. 10 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameScoreController.java
  4. 1 1
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/NumberController.java
  5. 13 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/EnrollImportResult.java
  6. 14 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/EnrollValidationError.java
  7. 14 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/EnrollValidationResult.java
  8. 5 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/GameEvent.java
  9. 5 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/GameEventProject.java
  10. 5 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GameEventBo.java
  11. 5 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GameEventProjectBo.java
  12. 2 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/GenerateBibBo.java
  13. 15 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/bo/ProjectSelectionValidationBo.java
  14. 7 1
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/EnrollProjectVo.java
  15. 5 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameEventProjectVo.java
  16. 5 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/domain/vo/GameEventVo.java
  17. 21 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IEnrollService.java
  18. 9 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameAthleteService.java
  19. 7 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameScoreService.java
  20. 84 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameAthleteServiceImpl.java
  21. 28 8
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameEventServiceImpl.java
  22. 374 32
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameScoreServiceImpl.java
  23. 11 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameTeamServiceImpl.java
  24. 647 128
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/IEnrollServiceImpl.java
  25. 123 0
      ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/utils/GenderDictUtils.java
  26. BIN
      ruoyi-modules/ruoyi-game-event/src/main/resources/template/enroll_template.xlsx

+ 30 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/EnrollController.java

@@ -13,6 +13,7 @@ import org.apache.ibatis.javassist.bytecode.AnnotationsAttribute;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.excel.utils.ExcelUtil;
 import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.system.domain.EnrollImportResult;
 import org.dromara.system.domain.vo.EnrollProjectVo;
 import org.dromara.system.domain.vo.GameAthleteVo;
 import org.dromara.system.service.IEnrollService;
@@ -61,4 +62,33 @@ public class EnrollController {
         Boolean result = enrollService.importDataForPoi(file, eventId);
         return R.ok(result ? "导入成功" : "导入失败");
     }
+
+    /**
+     * 导入报名表并验证数据
+     */
+    @PostMapping("/importDataWithValidation/{eventId}")
+    public R<EnrollImportResult> importDataWithValidation(
+        @RequestParam("file") MultipartFile file,
+        @PathVariable Long eventId) {
+        try {
+            // 添加文件校验
+            if (file == null || file.isEmpty()) {
+                return R.fail("请选择要导入的文件");
+            }
+            
+            String fileName = file.getOriginalFilename();
+            if (fileName == null || !fileName.toLowerCase().endsWith(".xlsx")) {
+                return R.fail("请选择Excel文件(.xlsx格式)");
+            }
+            
+            EnrollImportResult result = enrollService.importDataWithValidation(file, eventId);
+            return R.ok(result);
+        } catch (IllegalArgumentException e) {
+            log.warn("参数错误:{}", e.getMessage());
+            return R.fail("参数错误:" + e.getMessage());
+        } catch (Exception e) {
+            log.error("导入报名表失败,赛事ID: {}", eventId, e);
+            return R.fail("导入失败:" + e.getMessage());
+        }
+    }
 }

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

@@ -1,10 +1,7 @@
 package org.dromara.system.controller;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 import java.util.stream.Collectors;
 
 import cn.hutool.json.JSONUtil;
@@ -18,6 +15,7 @@ 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.GameTeam;
+import org.dromara.system.domain.bo.ProjectSelectionValidationBo;
 import org.dromara.system.domain.constant.GameEventConstant;
 import org.dromara.system.domain.vo.AthleteScoreVo;
 import org.dromara.system.domain.vo.GameTeamVo;
@@ -166,4 +164,11 @@ public class GameAthleteController extends BaseController {
                           @PathVariable Long[] athleteIds) {
         return toAjax(gameAthleteService.deleteWithValidByIds(List.of(athleteIds), true));
     }
+
+    @PostMapping("/validateProjectSelection")
+    @SaCheckPermission("system:gameAthlete:add")
+    public R<Map<String, Object>> validateProjectSelection(@RequestBody ProjectSelectionValidationBo bo) {
+        Map<String, Object> result = gameAthleteService.validateProjectSelection(bo);
+        return R.ok(result);
+    }
 }

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

@@ -214,4 +214,14 @@ public class GameScoreController extends BaseController {
             throw new RuntimeException("参数解析失败:" + e.getMessage());
         }
     }
+
+    /**
+     * 导出成绩详情数据(全部)
+     */
+    @SaCheckPermission("system:gameScore:export")
+    @Log(title = "导出成绩详情", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportScoresDetail")
+    public void exportScoresDetail(@RequestParam("eventId") Long eventId, HttpServletResponse response) {
+        gameScoreService.exportScoresDetail(eventId, response);
+    }
 }

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

@@ -58,7 +58,7 @@ public class NumberController {
     @PostMapping("/generateBib")
     public void generateNumberBib(HttpServletResponse response,
                                   @RequestPart("bgImage") MultipartFile bgImage,
-                                  @RequestPart(name = "logo") MultipartFile logo,
+                                  @RequestPart(name = "logo", required = false) MultipartFile logo,
                                   GenerateBibBo bibParam) {
         gameEventService.generateNumberBib(response, bgImage, logo, bibParam);
     }

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

@@ -0,0 +1,13 @@
+package org.dromara.system.domain;
+
+import lombok.Data;
+
+@Data
+public class EnrollImportResult {
+    private boolean success;                    // 是否成功
+    private EnrollValidationResult validationResult;  // 校验结果
+    private int totalCount;                     // 总数据条数
+    private int validCount;                     // 有效数据条数
+    private int errorCount;                     // 错误数据条数
+    private String message;                     // 结果消息
+}

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

@@ -0,0 +1,14 @@
+package org.dromara.system.domain;
+
+import lombok.Data;
+
+// 校验错误信息
+@Data
+public class EnrollValidationError {
+    private int rowIndex;           // 行号
+    private String athleteName;     // 运动员姓名
+    private String teamName;        // 队伍名称
+    private String errorType;       // 错误类型:PERSON_LIMIT_EXCEEDED, PROJECT_LIMIT_EXCEEDED
+    private String errorMessage;    // 错误信息
+    private String projectName;     // 相关项目名称(如果是项目限制错误)
+}

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

@@ -0,0 +1,14 @@
+package org.dromara.system.domain;
+
+import lombok.Data;
+import org.dromara.system.domain.vo.EnrollProjectVo;
+
+import java.util.List;
+
+// 校验结果VO
+@Data
+public class EnrollValidationResult {
+    private boolean valid;
+    private List<EnrollValidationError> errors;
+    private List<EnrollProjectVo> validData;
+}

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

@@ -66,6 +66,11 @@ public class GameEvent extends TenantEntity {
      */
     private Date endTime;
 
+    /**
+     * 每人限报项目数
+     */
+    private Integer limitApplication;
+
     /**
      * 赛事链接
      */

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

@@ -79,6 +79,11 @@ public class GameEventProject extends TenantEntity {
      */
     private Long participateNum;
 
+    /**
+     * 项目限报人数
+     */
+    private Integer limitPerson;
+
     /**
      * 录取名次
      */

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

@@ -68,6 +68,11 @@ public class GameEventBo extends BaseEntity {
     @NotNull(message = "结束时间不能为空", groups = { AddGroup.class, EditGroup.class })
     private Date endTime;
 
+    /**
+     * 每人限报项目数
+     */
+    private Integer limitApplication;
+
     /**
      * 赛事链接
      */

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

@@ -101,6 +101,11 @@ public class GameEventProjectBo extends BaseEntity {
      */
     private Long participateNum;
 
+    /**
+     * 项目限报人数
+     */
+    private Integer limitPerson;
+
     /**
      * 录取名次
      */

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

@@ -27,6 +27,8 @@ public class GenerateBibBo implements Serializable {
     private Integer fontSize;
     private Integer fontColor;
 
+    private String eventName;
+
 
     @Serial
     private static final long serialVersionUID = 1L;

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

@@ -0,0 +1,15 @@
+package org.dromara.system.domain.bo;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 参赛项目选择表单验证
+ */
+@Data
+public class ProjectSelectionValidationBo {
+//    private Long eventId;
+    private Long athleteId; // 编辑时传入,新增时为null
+    private List<Long> selectedProjectIds;
+}

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

@@ -17,6 +17,12 @@ import java.util.Map;
 @AutoMapper(target = GameAthlete.class)
 public class EnrollProjectVo {
 
+    /**
+     * 号码
+     */
+    @ExcelProperty(value = "号码")
+    private String athleteCode;
+
     /**
      * 姓名
      */
@@ -50,7 +56,7 @@ public class EnrollProjectVo {
     /**
      * 联系方式
      */
-    @ExcelProperty(value = "联系方式")
+    @ExcelProperty(value = "电话")
     private String phone;
 
     private Map<String, Boolean> ProjectSelections;

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

@@ -115,6 +115,11 @@ public class GameEventProjectVo implements Serializable {
     @ExcelProperty(value = "参赛人数")
     private Long participateNum;
 
+    /**
+     * 项目限报人数
+     */
+    private Integer limitPerson;
+
     /**
      * 录取名次
      */

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

@@ -77,6 +77,11 @@ public class GameEventVo implements Serializable {
     @ExcelProperty(value = "结束时间")
     private Date endTime;
 
+    /**
+     * 每人限报项目数
+     */
+    private Integer limitApplication;
+
     /**
      * 赛事链接
      */

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

@@ -2,9 +2,14 @@ package org.dromara.system.service;
 
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
+import org.dromara.system.domain.EnrollImportResult;
+import org.dromara.system.domain.EnrollValidationResult;
 import org.dromara.system.domain.bo.EnrollBo;
+import org.dromara.system.domain.vo.EnrollProjectVo;
 import org.springframework.web.multipart.MultipartFile;
 
+import java.util.List;
+
 public interface IEnrollService {
 
     /**
@@ -23,4 +28,20 @@ public interface IEnrollService {
     Boolean importDataForPoi(MultipartFile file, Long eventId);
 
     Boolean enroll(EnrollBo enrollBo);
+
+    /**
+     * 校验导入数据
+     * @param enrollList 导入的报名数据
+     * @param eventId 赛事ID
+     * @return 校验结果
+     */
+    EnrollValidationResult validateEnrollData(List<EnrollProjectVo> enrollList, Long eventId);
+
+    /**
+     * 导入报名表(带校验)
+     * @param file 文件
+     * @param eventId 赛事ID
+     * @return 导入结果
+     */
+    EnrollImportResult importDataWithValidation(MultipartFile file, Long eventId);
 }

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

@@ -4,11 +4,13 @@ import org.dromara.common.mybatis.core.page.PageQuery;
 import org.dromara.common.mybatis.core.page.TableDataInfo;
 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.GameAthleteVo;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 /**
  * 参赛队员Service接口
@@ -104,4 +106,11 @@ public interface IGameAthleteService {
      * @return 运动员列表
      */
     List<GameAthleteBo> findByProjectIds(Collection<Long> projectIds);
+
+    /**
+     * 验证项目选择
+     * @param bo 验证参数
+     * @return 验证结果
+     */
+    Map<String, Object> validateProjectSelection(ProjectSelectionValidationBo bo);
 }

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

@@ -139,4 +139,11 @@ public interface IGameScoreService {
      * 导出加分Excel
      */
     void exportBonusExcel(Map<String, Object> data, HttpServletResponse response);
+
+    /**
+     * 导出成绩详情数据(全部)
+     * @param eventId 赛事ID
+     * @param response HTTP响应对象
+     */
+    void exportScoresDetail(Long eventId, HttpServletResponse response);
 }

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

@@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.exception.ServiceException;
 import org.dromara.common.core.utils.MapstructUtils;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.mybatis.core.page.PageQuery;
@@ -21,6 +22,7 @@ import org.dromara.system.domain.GameTeam;
 import org.dromara.system.domain.bo.GameAthleteBo;
 import org.dromara.system.domain.bo.GameEventBo;
 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.system.mapper.GameAthleteMapper;
@@ -337,6 +339,15 @@ public class GameAthleteServiceImpl implements IGameAthleteService {
      */
     private void validEntityBeforeSave(GameAthlete entity) {
         //TODO 做一些数据校验,如唯一约束
+        //校验运动员编号是否存在重复值
+        if (entity.getAthleteCode() != null){
+            List<GameAthlete> list = baseMapper.selectList(new LambdaQueryWrapper<GameAthlete>()
+                .eq(GameAthlete::getAthleteCode, entity.getAthleteCode()));
+            //排除自己
+            if (list.size()>1){
+                throw new ServiceException(entity.getName()+"的编号已存在!");
+            }
+        }
     }
 
     /**
@@ -533,4 +544,77 @@ public class GameAthleteServiceImpl implements IGameAthleteService {
     public List<GameAthleteBo> findByProjectIds(Collection<Long> projectIds) {
         return baseMapper.findByProjectIds(projectIds);
     }
+
+    /**
+     * 项目选择验证
+     * @param bo 验证参数
+     * @return
+     */
+    @Override
+    public Map<String, Object> validateProjectSelection(ProjectSelectionValidationBo bo) {
+        Map<String, Object> result = new HashMap<>();
+        List<String> errors = new ArrayList<>();
+        List<String> warnings = new ArrayList<>();
+
+        // 1. 校验每人限报项目数
+        GameEventVo event = gameEventService.getDefaultEvent();
+        if (event.getLimitApplication() != null && event.getLimitApplication() > 0) {
+            if (bo.getSelectedProjectIds().size() > event.getLimitApplication()) {
+                errors.add(String.format("每人限报项目数为 %d 个,您已选择 %d 个项目",
+                    event.getLimitApplication(), bo.getSelectedProjectIds().size()));
+            }
+        }
+
+        // 2. 校验各项目的限报人数
+        for (Long projectId : bo.getSelectedProjectIds()) {
+            GameEventProjectVo project = gameEventProjectService.queryById(projectId);
+            if (project.getLimitPerson() != null && project.getLimitPerson() > 0) {
+                // 统计当前项目已报名人数
+                int currentCount = countAthletesByProjectId(projectId, bo.getAthleteId());
+
+                if (currentCount >= project.getLimitPerson()) {
+                    errors.add(String.format("项目「%s」报名人数已满(%d/%d),无法添加",
+                        project.getProjectName(), currentCount, project.getLimitPerson()));
+                } else if (currentCount >= project.getLimitPerson() * 0.9) {
+                    warnings.add(String.format("项目「%s」报名人数接近上限(%d/%d)",
+                        project.getProjectName(), currentCount, project.getLimitPerson()));
+                }
+            }
+        }
+
+        result.put("valid", errors.isEmpty());
+        result.put("errors", errors);
+        result.put("warnings", warnings);
+
+        return result;
+    }
+
+    // 统计项目报名人数的方法
+    private int countAthletesByProjectId(Long projectId, Long excludeAthleteId) {
+        LambdaQueryWrapper<GameAthlete> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(GameAthlete::getProjectValue, projectId.toString());
+
+        if (excludeAthleteId != null) {
+            wrapper.ne(GameAthlete::getAthleteId, excludeAthleteId);
+        }
+
+        List<GameAthlete> athletes = baseMapper.selectList(wrapper);
+
+        // 需要解析JSON格式的projectValue字段
+        int count = 0;
+        for (GameAthlete athlete : athletes) {
+            if (StringUtils.isNotBlank(athlete.getProjectValue())) {
+                try {
+                    List<Long> projectIds = JSONUtil.toList(athlete.getProjectValue(), Long.class);
+                    if (projectIds.contains(projectId)) {
+                        count++;
+                    }
+                } catch (Exception e) {
+                    log.warn("解析运动员项目列表失败: {}", athlete.getProjectValue());
+                }
+            }
+        }
+
+        return count;
+    }
 }

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

@@ -591,8 +591,9 @@ public class GameEventServiceImpl implements IGameEventService {
         //4.查询赛事组别
         GameEventGroup gameEventGroup = gameEventGroupService.queryByEventId(defaultEventId);
         //5.根据参数生成号码布
-        GameEventVo eventVo = baseMapper.selectVoById(defaultEventId);
-        generateBib(response, bgImage, logo, eventVo.getEventName(), gameEventGroup.getGroupName(), athleteVoList, teamNameMap, projectMap, bibParam);
+//        GameEventVo eventVo = baseMapper.selectVoById(defaultEventId);
+//        generateBib(response, bgImage, logo, eventVo.getEventName(), gameEventGroup.getGroupName(), athleteVoList, teamNameMap, projectMap, bibParam);
+        generateBib(response, bgImage, logo, bibParam.getEventName(), gameEventGroup.getGroupName(), athleteVoList, teamNameMap, projectMap, bibParam);
     }
 
     /**
@@ -665,7 +666,21 @@ public class GameEventServiceImpl implements IGameEventService {
             backgroundImageBytes = backgroundImage.getBytes();
 
             if (logo != null && !logo.isEmpty()) {
+                // 验证logo文件类型
+                String contentType = logo.getContentType();
+                if (contentType == null || !contentType.startsWith("image/")) {
+                    throw new IllegalArgumentException("Logo文件必须是图片格式");
+                }
+
+                // 验证logo文件大小(例如限制为2MB)
+                // if (logo.getSize() > 2 * 1024 * 1024) {
+                //     throw new IllegalArgumentException("Logo文件大小不能超过2MB");
+                // }
+
                 logoImageBytes = logo.getBytes();
+//                log.info("Logo图片上传成功,大小: {} bytes", logoImageBytes.length);
+            } else {
+                log.info("未上传Logo图片,将跳过Logo处理");
             }
         } catch (IOException e) {
             throw new RuntimeException("读取上传文件失败", e);
@@ -716,12 +731,17 @@ public class GameEventServiceImpl implements IGameEventService {
 
                         // 添加 Logo(如果存在)
                         if (finalLogoImageBytes != null && logoX != null && logoY != null) {
-                            Image img = Image.getInstance(finalLogoImageBytes);
-                            img.scaleToFit(80, 80);
-                            float logoPositionX = logoX.floatValue();
-                            float logoPositionY = logoY.floatValue();
-                            img.setAbsolutePosition(logoPositionX, logoPositionY);
-                            cb.addImage(img);
+                            try {
+                                Image img = Image.getInstance(finalLogoImageBytes);
+                                img.scaleToFit(80, 80);
+                                float logoPositionX = logoX.floatValue();
+                                float logoPositionY = logoY.floatValue();
+                                img.setAbsolutePosition(logoPositionX, logoPositionY);
+                                cb.addImage(img);
+//                                log.debug("成功添加Logo到PDF");
+                            } catch (Exception e) {
+                                log.warn("添加Logo到PDF失败,跳过Logo处理: {}", e.getMessage());
+                            }
                         }
 
                         // 添加号码(居中)

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

@@ -357,11 +357,12 @@ public class GameScoreServiceImpl implements IGameScoreService {
             if (athlete.getTeamId() != null) {
                 GameTeamVo team = gameTeamService.queryById(athlete.getTeamId());
                 if (team != null) {
-                    teamName = team.getTeamName();
+                    data.put("teamName", team.getTeamName()); // 添加teamName字段
+                    data.put("teamCode", team.getTeamCode());
                     log.debug("获取到队伍名称: teamId={}, teamName={}", athlete.getTeamId(), teamName);
                 }
             }
-            data.put("teamName", teamName); // 添加teamName字段
+
 
             // 查询成绩信息
             GameScoreVo score = getScoreByAthleteIdAndProjectId(athlete.getAthleteId(), projectId);
@@ -1274,70 +1275,73 @@ public class GameScoreServiceImpl implements IGameScoreService {
             teamQuery.setEventId(eventId);
             List<GameTeamVo> teams = gameTeamService.queryList(teamQuery);
 
-            // 批量查询所有队伍在所有项目中的成绩
+            // 批量查询所有队伍在所有项目中的成绩(当前赛事)
             List<GameScoreVo> allScores = baseMapper.selectVoList(
                 Wrappers.lambdaQuery(GameScore.class)
                     .eq(GameScore::getEventId, eventId)
             );
 
-            // 按队伍ID和项目ID分组成绩数据
-            Map<String, GameScoreVo> scoreMap = new HashMap<>();
+            // 构建 teamId -> (projectId -> 积分累计和)
+            Map<Long, Map<Long, Integer>> teamProjectPointSumMap = new HashMap<>();
             for (GameScoreVo score : allScores) {
-                if (score.getTeamId() != null && score.getProjectId() != null) {
-                    String key = score.getTeamId() + "_" + score.getProjectId();
-                    scoreMap.put(key, score);
+                Long teamId = score.getTeamId();
+                Long projectId = score.getProjectId();
+                if (teamId == null || projectId == null) {
+                    continue;
                 }
+                int points = score.getScorePoint() != null ? score.getScorePoint() : 0;
+
+                Map<Long, Integer> projectPointMap = teamProjectPointSumMap.computeIfAbsent(teamId, k -> new HashMap<>());
+                projectPointMap.merge(projectId, points, Integer::sum);
             }
 
-            // 构建加分数据
+            // 构建导出/前端展示数据
             List<Map<String, Object>> bonusData = new ArrayList<>();
 
-            for (int i = 0; i < teams.size(); i++) {
-                GameTeamVo team = teams.get(i);
+            for (GameTeamVo team : teams) {
                 Map<String, Object> teamData = new HashMap<>();
-
-                // 设置基本信息
                 teamData.put("teamId", team.getTeamId());
                 teamData.put("teamName", team.getTeamName());
-                teamData.put("rank", i + 1);
 
-                // 计算该队伍在各项目中的积分
-                int totalScore = 0;
+                // 项目积分:按项目名称汇总
                 Map<String, Integer> projectScores = new HashMap<>();
-                int leaderPoint = 0;
-                int extraPoint = 0;
+                int totalScore = 0;
 
-                // 设置各项目积分
-                for (GameEventProjectVo project : projects) {
-                    String key = team.getTeamId() + "_" + project.getProjectId();
-                    GameScoreVo score = scoreMap.get(key);
+                Map<Long, Integer> projectPointMap = teamProjectPointSumMap.getOrDefault(team.getTeamId(), Collections.emptyMap());
 
-                    int projectScore = 0;
-                    if (score != null && score.getScorePoint() != null) {
-                        projectScore = score.getScorePoint();
-                        totalScore += projectScore;
+                for (GameEventProjectVo project : projects) {
+                    int projectScore = projectPointMap.getOrDefault(project.getProjectId(), 0);
+                    projectScores.put(project.getProjectName(), projectScore);
+                    totalScore += projectScore;
+                }
 
-                        // 获取加分数据
+                // 读取加分(若有,取队伍该赛事下任一成绩记录中的加分;否则默认为0)
+                int leaderPoint = 0;
+                int extraPoint = 0;
+                for (GameScoreVo score : allScores) {
+                    if (Objects.equals(score.getTeamId(), team.getTeamId())) {
                         if (score.getLeaderPoint() != null) {
                             leaderPoint = score.getLeaderPoint();
                         }
                         if (score.getExtraPoint() != null) {
                             extraPoint = score.getExtraPoint();
                         }
+                        // 找到一条即可(两者若分散在不同记录中,按最后一次非空覆盖)
                     }
-
-                    projectScores.put(project.getProjectName(), projectScore);
                 }
 
+                // 汇总总分(基础项目分 + 加分)
+                int finalTotal = totalScore + leaderPoint + extraPoint;
+
                 teamData.put("projectScores", projectScores);
-                teamData.put("totalScore", totalScore);
+                teamData.put("totalScore", finalTotal);
                 teamData.put("leaderPoint", leaderPoint);
                 teamData.put("extraPoint", extraPoint);
 
                 bonusData.add(teamData);
             }
 
-            // 按总分排序,生成排名
+            // 按总分排序并标注排名(并列名次同分给同名次,下例采用稳定排序+顺序名次,若需并列逻辑可再调整)
             bonusData.sort((a, b) -> Integer.compare((Integer) b.get("totalScore"), (Integer) a.get("totalScore")));
             for (int i = 0; i < bonusData.size(); i++) {
                 bonusData.get(i).put("rank", i + 1);
@@ -1346,7 +1350,6 @@ public class GameScoreServiceImpl implements IGameScoreService {
             Map<String, Object> result = new HashMap<>();
             result.put("rows", bonusData);
             result.put("projects", projects);
-
             return result;
 
         } catch (Exception e) {
@@ -1490,4 +1493,343 @@ public class GameScoreServiceImpl implements IGameScoreService {
             throw new RuntimeException("导出失败:" + e.getMessage());
         }
     }
+
+    /**
+     * 导出积分明细Excel
+     * @param eventId 赛事ID
+     * @param response HTTP响应对象
+     */
+    @Override
+    public void exportScoresDetail(Long eventId, HttpServletResponse response) {
+        try {
+            log.info("开始导出成绩详情,eventId: {}", eventId);
+            
+            // 1. 获取所有项目
+            List<GameEventProjectVo> projects = gameEventProjectService.queryListByEventId(eventId);
+            if (projects.isEmpty()) {
+                throw new RuntimeException("未找到赛事项目");
+            }
+            
+            // 2. 创建Excel工作簿
+            try (Workbook workbook = new XSSFWorkbook()) {
+                
+                // 3. 创建样式
+                CellStyle headerStyle = createHeaderStyle(workbook);
+                CellStyle dataStyleOdd = createDataStyle(workbook, IndexedColors.WHITE);
+                CellStyle dataStyleEven = createDataStyle(workbook, IndexedColors.GREY_25_PERCENT);
+                
+                // 4. 为每个项目创建Sheet页
+                for (GameEventProjectVo project : projects) {
+                    createProjectSheet(workbook, project, eventId, headerStyle, dataStyleOdd, dataStyleEven);
+                }
+                
+                // 5. 设置响应头并输出
+                setResponseHeaders(response, "成绩详情表");
+                workbook.write(response.getOutputStream());
+            }
+            
+            log.info("成绩详情导出完成");
+            
+        } catch (Exception e) {
+            log.error("导出成绩详情失败", e);
+            throw new RuntimeException("导出失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 为单个项目创建Sheet页
+     */
+    private void createProjectSheet(Workbook workbook, GameEventProjectVo project, Long eventId, 
+        CellStyle headerStyle, CellStyle dataStyleOdd, CellStyle dataStyleEven) {
+
+        // 1. 创建Sheet页,名称限制在31个字符内
+        String sheetName = truncateSheetName(project.getProjectName());
+        Sheet sheet = workbook.createSheet(sheetName);
+
+        // 2. 获取项目详细数据
+        List<Map<String, Object>> projectData = getProjectScoreDataAll(
+            eventId, 
+            project.getProjectId(), 
+            project.getClassification()
+        );
+
+        // 3. 创建标题行
+        createProjectHeaderRow(sheet, project, headerStyle);
+
+        // 4. 填充数据行
+        fillProjectDataRows(sheet, projectData, project, dataStyleOdd, dataStyleEven);
+
+        // 5. 自动调整列宽
+        autoSizeColumns(sheet);
+    }
+
+    /**
+     * 获取项目成绩数据(全部,不分页)
+     */
+    private List<Map<String, Object>> getProjectScoreDataAll(Long eventId, Long projectId, String classification) {
+        log.info("获取项目全部数据: eventId={}, projectId={}, classification={}", eventId, projectId, classification);
+        
+        if ("0".equals(classification)) {
+            // 个人项目:获取所有运动员数据
+            return getIndividualProjectDataAll(eventId, projectId);
+        } else {
+            // 团体项目:获取所有队伍数据
+            return getTeamProjectDataAll(eventId, projectId);
+        }
+    }
+
+    /**
+     * 获取个人项目全部数据(不分页)
+     */
+    private List<Map<String, Object>> getIndividualProjectDataAll(Long eventId, Long projectId) {
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        
+        // 查询参与该项目的所有运动员(不分页)
+        List<GameAthleteVo> athletes = gameAthleteService.queryListByEventIdAndProjectId(eventId, projectId, null);
+        
+        for (GameAthleteVo athlete : athletes) {
+            Map<String, Object> data = new HashMap<>();
+            
+            // 基础信息
+            data.put("athleteId", athlete.getAthleteId());
+            data.put("userId", athlete.getUserId());
+            data.put("eventId", eventId);
+            data.put("projectId", projectId);
+            data.put("teamId", athlete.getTeamId());
+            data.put("athleteCode", athlete.getAthleteCode());
+            data.put("name", athlete.getName());
+            data.put("unit", athlete.getUnit());
+            data.put("groupType", athlete.getGroupType());
+            
+            // 队伍信息
+            if (athlete.getTeamId() != null) {
+                GameTeamVo team = gameTeamService.queryById(athlete.getTeamId());
+                if (team != null) {
+                    data.put("teamName", team.getTeamName());
+                    data.put("teamCode", team.getTeamCode());
+                }
+            }
+            
+            // 成绩信息
+            GameScoreVo score = getScoreByAthleteIdAndProjectId(athlete.getAthleteId(), projectId);
+            if (score != null) {
+                data.put("scoreId", score.getScoreId());
+                data.put("scorePoint", score.getScorePoint());
+                data.put("individualPerformance", score.getIndividualPerformance());
+                data.put("teamPerformance", score.getTeamPerformance());
+                data.put("scoreRank", score.getScoreRank());
+                data.put("updateTime", score.getUpdateTime());
+            } else {
+                // 设置默认值
+                data.put("scoreId", 0);
+                data.put("scorePoint", 0);
+                data.put("individualPerformance", 0.0);
+                data.put("teamPerformance", 0.0);
+                data.put("scoreRank", 0);
+                data.put("updateTime", "");
+            }
+            
+            resultList.add(data);
+        }
+        
+        return resultList;
+    }
+
+    /**
+     * 获取团体项目全部数据(不分页)
+     */
+    private List<Map<String, Object>> getTeamProjectDataAll(Long eventId, Long projectId) {
+        List<Map<String, Object>> resultList = new ArrayList<>();
+        Set<Long> processedTeamIds = new HashSet<>();
+        
+        // 查询参与该项目的所有运动员
+        List<GameAthleteVo> athletes = gameAthleteService.queryListByEventIdAndProjectId(eventId, projectId, null);
+        
+        for (GameAthleteVo athlete : athletes) {
+            if (athlete.getTeamId() == null || processedTeamIds.contains(athlete.getTeamId())) {
+                continue;
+            }
+            
+            processedTeamIds.add(athlete.getTeamId());
+            
+            // 查询队伍信息
+            GameTeamVo team = gameTeamService.queryById(athlete.getTeamId());
+            if (team == null) {
+                continue;
+            }
+            
+            Map<String, Object> data = new HashMap<>();
+            data.put("teamId", team.getTeamId());
+            data.put("teamName", team.getTeamName());
+            data.put("teamCode", team.getTeamCode());
+            data.put("eventId", eventId);
+            data.put("projectId", projectId);
+            
+            // 查询成绩信息
+            GameScoreVo score = getScoreByAthleteIdAndProjectId(athlete.getAthleteId(), projectId);
+            if (score != null) {
+                data.put("scoreId", score.getScoreId());
+                data.put("scorePoint", score.getScorePoint());
+                data.put("individualPerformance", score.getIndividualPerformance());
+                data.put("teamPerformance", score.getTeamPerformance());
+                data.put("scoreRank", score.getScoreRank());
+                data.put("updateTime", score.getUpdateTime());
+            } else {
+                data.put("scoreId", 0);
+                data.put("scorePoint", 0);
+                data.put("individualPerformance", 0.0);
+                data.put("teamPerformance", 0.0);
+                data.put("scoreRank", 0);
+                data.put("updateTime", "");
+            }
+            
+            resultList.add(data);
+        }
+        
+        return resultList;
+    }
+
+    /**
+     * 创建项目Sheet页的标题行
+     */
+    private void createProjectHeaderRow(Sheet sheet, GameEventProjectVo project, CellStyle headerStyle) {
+        Row headerRow = sheet.createRow(0);
+        int colIndex = 0;
+        
+        // 根据项目类型确定列标题
+        if ("0".equals(project.getClassification())) {
+            // 个人项目列标题
+            String[] headers = {
+                "序号", "队伍编号", "队伍名称", "姓名", "号码", 
+                "个人成绩", "积分", "排名", "更新时间"
+            };
+            
+            for (String header : headers) {
+                Cell cell = headerRow.createCell(colIndex++);
+                cell.setCellValue(header);
+                cell.setCellStyle(headerStyle);
+            }
+        } else {
+            // 团体项目列标题
+            String[] headers = {
+                "序号", "队伍编号", "队伍名称", "团队成绩", 
+                "积分", "排名", "更新时间"
+            };
+            
+            for (String header : headers) {
+                Cell cell = headerRow.createCell(colIndex++);
+                cell.setCellValue(header);
+                cell.setCellStyle(headerStyle);
+            }
+        }
+    }
+
+    /**
+     * 填充项目数据行
+     */
+    private void fillProjectDataRows(Sheet sheet, List<Map<String, Object>> projectData, 
+    GameEventProjectVo project, CellStyle dataStyleOdd, CellStyle dataStyleEven) {
+
+        int rowIndex = 1;
+        for (int i = 0; i < projectData.size(); i++) {
+            Map<String, Object> data = projectData.get(i);
+            Row dataRow = sheet.createRow(rowIndex++);
+
+            // 选择行样式
+            CellStyle currentRowStyle = (rowIndex % 2 == 0) ? dataStyleEven : dataStyleOdd;
+
+            int colIndex = 0;
+
+            if ("0".equals(project.getClassification())) {
+                // 个人项目数据
+                createCellWithStyle(dataRow, colIndex++, i + 1, currentRowStyle); // 序号
+                createCellWithStyle(dataRow, colIndex++, data.get("teamCode"), currentRowStyle); // 队伍编号
+                createCellWithStyle(dataRow, colIndex++, data.get("teamName"), currentRowStyle); // 队伍名称
+                createCellWithStyle(dataRow, colIndex++, data.get("name"), currentRowStyle); // 姓名
+                createCellWithStyle(dataRow, colIndex++, data.get("athleteCode"), currentRowStyle); // 号码
+                createCellWithStyle(dataRow, colIndex++, data.get("individualPerformance"), currentRowStyle); // 个人成绩
+                createCellWithStyle(dataRow, colIndex++, data.get("scorePoint"), currentRowStyle); // 积分
+                createCellWithStyle(dataRow, colIndex++, data.get("scoreRank"), currentRowStyle); // 排名
+                createCellWithStyle(dataRow, colIndex++, data.get("updateTime"), currentRowStyle); // 更新时间
+            } else {
+                // 团体项目数据
+                createCellWithStyle(dataRow, colIndex++, i + 1, currentRowStyle); // 序号
+                createCellWithStyle(dataRow, colIndex++, data.get("teamCode"), currentRowStyle); // 队伍编号
+                createCellWithStyle(dataRow, colIndex++, data.get("teamName"), currentRowStyle); // 队伍名称
+                createCellWithStyle(dataRow, colIndex++, data.get("teamPerformance"), currentRowStyle); // 团队成绩
+                createCellWithStyle(dataRow, colIndex++, data.get("scorePoint"), currentRowStyle); // 积分
+                createCellWithStyle(dataRow, colIndex++, data.get("scoreRank"), currentRowStyle); // 排名
+                createCellWithStyle(dataRow, colIndex++, data.get("updateTime"), currentRowStyle); // 更新时间
+            }
+        }
+    }
+
+    /**
+     * 处理Sheet名称(Excel限制31个字符,不能包含特殊字符)
+     */
+    private String truncateSheetName(String projectName) {
+        if (projectName == null) {
+            return "项目";
+        }
+        
+        // 替换Excel不允许的特殊字符
+        String cleanName = projectName
+            .replace("*", "×")     // 星号替换为乘号
+            .replace("\\", "_")    // 反斜杠
+            .replace("/", "_")     // 正斜杠
+            .replace("?", "_")     // 问号
+            .replace("[", "(")     // 左方括号替换为左括号
+            .replace("]", ")")     // 右方括号替换为右括号
+            .replace(":", ":")    // 英文冒号替换为中文冒号
+            .trim();               // 去除首尾空格
+        
+        // 处理单引号问题
+        if (cleanName.startsWith("'") || cleanName.endsWith("'")) {
+            cleanName = cleanName.replace("'", "_");
+        }
+        
+        // 如果处理后为空,使用默认名称
+        if (cleanName.isEmpty()) {
+            cleanName = "项目";
+        }
+        
+        // 限制长度在31个字符内
+        if (cleanName.length() > 31) {
+            cleanName = cleanName.substring(0, 31);
+        }
+        
+        return cleanName;
+    }
+
+    /**
+     * 自动调整列宽
+     */
+    private void autoSizeColumns(Sheet sheet) {
+        if (sheet.getLastRowNum() > 0) {
+            Row firstRow = sheet.getRow(0);
+            if (firstRow != null) {
+                for (int i = 0; i < firstRow.getLastCellNum(); i++) {
+                    sheet.autoSizeColumn(i);
+                    // 设置最小列宽
+                    if (sheet.getColumnWidth(i) < 2000) {
+                        sheet.setColumnWidth(i, 2000);
+                    }
+                    // 设置最大列宽
+                    if (sheet.getColumnWidth(i) > 8000) {
+                        sheet.setColumnWidth(i, 8000);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 设置响应头
+     */
+    private void setResponseHeaders(HttpServletResponse response, String fileName) throws Exception {
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setCharacterEncoding("utf-8");
+        String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
+        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + encodedFileName + ".xlsx");
+    }
 }

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

@@ -266,6 +266,17 @@ public class GameTeamServiceImpl implements IGameTeamService {
      */
     private void validEntityBeforeSave(GameTeam entity) {
         //TODO 做一些数据校验,如唯一约束
+        //校验队伍编号的唯一性
+        if (entity.getTeamCode() != null){
+            Long count = baseMapper.selectCount(
+                Wrappers.lambdaQuery(GameTeam.class)
+                    .eq(GameTeam::getTeamCode, entity.getTeamCode())
+            );
+            //排除自己
+            if (count > 1) {
+                throw new ServiceException(entity.getTeamName()+"的编号已存在!");
+            }
+        }
     }
 
     /**

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

@@ -14,16 +14,14 @@ import org.apache.poi.ss.util.CellRangeAddress;
 import org.apache.poi.xssf.usermodel.XSSFSheet;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 import org.dromara.common.core.utils.ObjectUtils;
-import org.dromara.system.domain.GameTeam;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.system.domain.*;
 import org.dromara.system.domain.bo.EnrollBo;
 import org.dromara.system.domain.bo.GameAthleteBo;
 import org.dromara.system.domain.bo.GameTeamBo;
-import org.dromara.system.domain.vo.EnrollProjectVo;
-import org.dromara.system.domain.vo.GameTeamVo;
-import org.dromara.system.service.IEnrollService;
-import org.dromara.system.service.IGameAthleteService;
-import org.dromara.system.service.IGameEventProjectService;
-import org.dromara.system.service.IGameTeamService;
+import org.dromara.system.domain.vo.*;
+import org.dromara.system.service.*;
+import org.dromara.system.utils.GenderDictUtils;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
@@ -44,6 +42,7 @@ public class IEnrollServiceImpl implements IEnrollService {
     private final IGameEventProjectService gameEventProjectService;
     private final IGameTeamService gameTeamService;
     private final IGameAthleteService gameAthleteService;
+    private final IGameEventService gameEventService;
 
     /**
      * 使用poi生成报名表模板
@@ -68,7 +67,7 @@ public class IEnrollServiceImpl implements IEnrollService {
 
             if (!projectMap.isEmpty()) {
                 // 3. 渲染分类
-                int currentColumnIndex = 6;
+                int currentColumnIndex = 7;
                 Row row = sheet.getRow(1);
                 if (row == null) {
                     row = sheet.createRow(1);
@@ -115,7 +114,7 @@ public class IEnrollServiceImpl implements IEnrollService {
                 }
 
                 // 4. 渲染项目(在第4行,索引为3)
-                currentColumnIndex = 6;
+                currentColumnIndex = 7;
                 Row projectRow = sheet.getRow(2);
                 if (projectRow == null) {
                     projectRow = sheet.createRow(2);
@@ -286,8 +285,8 @@ public class IEnrollServiceImpl implements IEnrollService {
                 int lastCellIndex = findValidLastColumnIndex(headerRow);
                 List<String> projectNames = new ArrayList<>();
 
-                // 从第6列(F列,索引5)开始收集项目名称
-                for (int i = 6; i < lastCellIndex; i++) {
+                // 从第7列(G列,索引6)开始收集项目名称(因为新增了号码列)
+                for (int i = 7; i < lastCellIndex; i++) {
                     Cell cell = headerRow.getCell(i);
                     if (cell != null && cell.getStringCellValue() != null && !cell.getStringCellValue().trim().isEmpty()) {
                         projectNames.add(cell.getStringCellValue().trim());
@@ -307,46 +306,54 @@ public class IEnrollServiceImpl implements IEnrollService {
                     EnrollProjectVo enroll = new EnrollProjectVo();
                     Map<String, Boolean> selections = new LinkedHashMap<>(); // 保持顺序
 
-                    // A列:姓名
-                    Cell nameCell = row.getCell(0);
+                    // A列:号码
+                    Cell codeCell = row.getCell(0);
+                    if (codeCell != null) {
+                        enroll.setAthleteCode(getCellValueAsString(codeCell));
+                    }
+
+                    // B列:姓名
+                    Cell nameCell = row.getCell(1);
                     if (nameCell != null) {
                         enroll.setName(getCellValueAsString(nameCell));
                     }
 
-                    // B列:性别
-                    Cell sexCell = row.getCell(1);
+                    // C列:性别
+                    Cell sexCell = row.getCell(2);
                     if (sexCell != null) {
-                        enroll.setSex(getCellValueAsString(sexCell));
+                        String sexText = getCellValueAsString(sexCell);
+                        String sexValue = GenderDictUtils.convertGenderToDictValue(sexText);
+                        enroll.setSex(sexValue);
                     }
 
-                    // C列:年龄
-                    Cell ageCell = row.getCell(2);
+                    // D列:年龄
+                    Cell ageCell = row.getCell(3);
                     if (ageCell != null) {
                         enroll.setAge(getCellValueAsString(ageCell));
                     }
 
-                    // D列:队伍名称
-                    Cell teamCell = row.getCell(3);
+                    // E列:队伍名称
+                    Cell teamCell = row.getCell(4);
                     if (teamCell != null) {
                         enroll.setTeamName(getCellValueAsString(teamCell));
                     }
 
-                    // E列:领队
-                    Cell leaderCell = row.getCell(4);
+                    // F列:领队
+                    Cell leaderCell = row.getCell(5);
                     if (leaderCell != null) {
                         enroll.setLeader(getCellValueAsString(leaderCell));
                     }
 
-                    // F列:联系方式
-                    Cell phoneCell = row.getCell(5);
+                    // G列:联系方式
+                    Cell phoneCell = row.getCell(6);
                     if (phoneCell != null) {
                         enroll.setPhone(getCellValueAsString(phoneCell));
                     }
 
-                    // 从第6列开始读取项目名称
-                    for (int j = 6; j < lastCellIndex; j++) {
+                    // 从第7列开始读取项目名称
+                    for (int j = 7; j < lastCellIndex; j++) {
                         Cell cell = row.getCell(j);
-                        String projectName = projectNames.get(j - 6); // 对应项目名
+                        String projectName = projectNames.get(j - 7); // 对应项目名
                         boolean selected = isCellSelected(cell);
                         selections.put(projectName, selected);
                     }
@@ -357,11 +364,9 @@ public class IEnrollServiceImpl implements IEnrollService {
             }
             return enrolls;
         } catch (Exception e) {
-
-        } finally {
-
+            log.error("解析Excel文件失败", e);
+            return List.of();
         }
-        return List.of();
     }
 
     // 辅助方法:将 Cell 转为字符串
@@ -460,87 +465,6 @@ public class IEnrollServiceImpl implements IEnrollService {
      * @param eventId
      * @return
      */
-    // @Transactional(rollbackFor = Exception.class)
-    // public boolean saveEnrollData(List<EnrollProjectVo> dataList, Long eventId) {
-    //     // 1. 根据队伍分类成Map
-    //     Map<String, List<EnrollProjectVo>> groupedByTeam = dataList.stream()
-    //         .collect(Collectors.groupingBy(EnrollProjectVo::getTeamName));
-    //     // 查询当前赛事下的项目 名称和id映射关系
-    //     Map<String, Long> projectList = gameEventProjectService.mapProjectAndProjectId(eventId);
-    //     // 1.2 根据队伍生成号码段
-    //     Map<String, String> numberRanges = new HashMap<>();
-    //     Map<String, AtomicInteger> currentNumbers = new HashMap<>(); // 记录每个队伍当前分配到的号码
-    //     AtomicInteger teamIndex = new AtomicInteger(1);
-    //     Snowflake snowflake = IdUtil.createSnowflake(1, 1);
-    //
-    //     for (Map.Entry<String, List<EnrollProjectVo>> entry : groupedByTeam.entrySet()) {
-    //         String teamName = entry.getKey();
-    //         String range = generateNumberRange(dataList.size(), teamIndex.getAndIncrement());
-    //         numberRanges.put(teamName, range);
-    //
-    //         // 解析起始号码作为初始值
-    //         int startNum = Integer.parseInt(range.split("-")[0]);
-    //         currentNumbers.put(teamName, new AtomicInteger(startNum));
-    //     }
-    //
-    //     // 2. 保存参赛队伍 & 队员
-    //     for (Map.Entry<String, List<EnrollProjectVo>> entry : groupedByTeam.entrySet()) {
-    //         String teamName = entry.getKey();
-    //         List<EnrollProjectVo> athletes = entry.getValue();
-    //         GameTeamBo gameTeamBo = new GameTeamBo();
-    //         gameTeamBo.setTeamId(snowflake.nextId());
-    //         Long teamId = gameTeamBo.getTeamId();
-    //         // 获取该队伍的当前编号计数器
-    //         AtomicInteger currentNumber = currentNumbers.get(teamName);
-    //         int width = (dataList.size() > 100) ? 5 : 4; // 决定格式化宽度
-    //         List<Long> athletesId = new ArrayList<>();
-    //         // 3. 保存参赛队员
-    //         for (EnrollProjectVo enrollInfo : athletes) {
-    //             GameAthleteBo gameAthleteBo = new GameAthleteBo();
-    //             gameAthleteBo.setEventId(eventId);
-    //             gameAthleteBo.setTeamId(teamId);
-    //             gameAthleteBo.setTeamName(teamName);
-    //             // 分配编号:从当前计数器获取并递增
-    //             int assignedNumber = currentNumber.getAndIncrement();
-    //             String formattedCode = String.format("%0" + width + "d", assignedNumber);
-    //             gameAthleteBo.setAthleteCode(formattedCode);
-    //
-    //             gameAthleteBo.setName(enrollInfo.getName());
-    //             gameAthleteBo.setGender(enrollInfo.getSex());
-    //             gameAthleteBo.setAge(Long.valueOf(enrollInfo.getAge()));
-    //             gameAthleteBo.setPhone(enrollInfo.getPhone());
-    //             gameAthleteBo.setUnit(teamName);
-    //             Map<String, Boolean> selectProjects = enrollInfo.getProjectSelections();
-    //             //查询出对应的赛事id
-    //             List<Long> selectionProjectIds = new ArrayList<>();
-    //             for (Map.Entry<String, Boolean> selectProject : selectProjects.entrySet()) {
-    //                 Long projectId = projectList.get(selectProject.getKey());
-    //                 if (ObjectUtils.isNotEmpty(projectId)) {
-    //                     selectionProjectIds.add(projectId);
-    //                 }
-    //             }
-    //             gameAthleteBo.setProjectValue(JSONUtil.toJsonStr(selectionProjectIds));
-    //
-    //             gameAthleteBo.setStatus("0");
-    //
-    //             gameAthleteService.insertByBo(gameAthleteBo);
-    //             athletesId.add(gameAthleteBo.getAthleteId());
-    //         }
-    //
-    //         gameTeamBo.setEventId(eventId);
-    //         gameTeamBo.setTeamName(teamName);
-    //         // gameTeamBo.setTeamCode("");
-    //         gameTeamBo.setLeader(athletes.get(0).getLeader());
-    //         gameTeamBo.setAthleteValue(JSONUtil.toJsonStr(athletesId));
-    //         gameTeamBo.setAthleteNum(Long.valueOf(athletes.size()));
-    //         gameTeamBo.setNumberRange(numberRanges.get(teamName));
-    //         gameTeamBo.setStatus("0");
-    //         gameTeamService.insertByBo(gameTeamBo);
-    //
-    //
-    //     }
-    //     return true;
-    // }
     @Transactional(rollbackFor = Exception.class)
     public boolean saveEnrollData(List<EnrollProjectVo> dataList, Long eventId) {
         // 1. 根据队伍分类成Map
@@ -603,8 +527,13 @@ public class IEnrollServiceImpl implements IEnrollService {
                         // 成功获取到有效编号,续着分配
                         currentNumber = new AtomicInteger(maxNumber + 1);
                     }
+                } catch (NumberFormatException e) {
+                    log.warn("解析队伍{}的号码段失败: {},从号码段起始开始分配", teamName, numberRange, e);
+                    String[] rangeParts = numberRange.split("-");
+                    int startNumber = Integer.valueOf(rangeParts[0]);
+                    currentNumber = new AtomicInteger(startNumber);
                 } catch (Exception e) {
-                    // 出现异常,从号码段起始开始分配
+                    log.error("查询队伍{}的最大队员编号时发生异常", teamName, e);
                     String[] rangeParts = numberRange.split("-");
                     int startNumber = Integer.valueOf(rangeParts[0]);
                     currentNumber = new AtomicInteger(startNumber);
@@ -614,14 +543,39 @@ public class IEnrollServiceImpl implements IEnrollService {
                 isNewTeam = true;
                 teamId = snowflake.nextId();
 
-                // 为新队伍生成300个号码段
-                int teamIndex = teamIndexCounter.getAndIncrement();
-                numberRange = generateNumberRange(dataList.size(), teamIndex);
-
-                // 解析号码段起始号码
-                String[] rangeParts = numberRange.split("-");
-                int startNumber = Integer.valueOf(rangeParts[0]);
-                currentNumber = new AtomicInteger(startNumber);
+                // 检查是否有填写的号码
+                List<String> providedCodes = athletes.stream()
+                .map(EnrollProjectVo::getAthleteCode)
+                .filter(StringUtils::isNotBlank)
+                .map(code -> code.trim())
+                .filter(code -> StringUtils.isNotBlank(code))
+                .collect(Collectors.toList());
+
+                if (!providedCodes.isEmpty()) {
+                    // 有填写的号码,使用ASCII值比较来确定号码段
+                    numberRange = generateCustomNumberRangeWithChars(providedCodes, eventId, null);
+
+                    if (numberRange != null) {
+                        // 成功生成自定义号码段
+                        currentNumber = null; // 对于字符号码,不使用数字计数器
+                        log.info("队伍 {} 使用自定义字符号码段: {}", teamName, numberRange);
+                    } else {
+                        // 生成失败,使用默认方式
+                        int teamIndex = teamIndexCounter.getAndIncrement();
+                        numberRange = generateNumberRange(dataList.size(), teamIndex);
+                        String[] rangeParts = numberRange.split("-");
+                        int startNumber = Integer.valueOf(rangeParts[0]);
+                        currentNumber = new AtomicInteger(startNumber);
+                        log.warn("队伍 {} 自定义号码段生成失败,使用默认号码段: {}", teamName, numberRange);
+                    }
+                } else {
+                    // 没有填写的号码,使用默认的300号码段
+                    int teamIndex = teamIndexCounter.getAndIncrement();
+                    numberRange = generateNumberRange(dataList.size(), teamIndex);
+                    String[] rangeParts = numberRange.split("-");
+                    int startNumber = Integer.valueOf(rangeParts[0]);
+                    currentNumber = new AtomicInteger(startNumber);
+                }
             }
 
             // 保存队员信息
@@ -654,6 +608,49 @@ public class IEnrollServiceImpl implements IEnrollService {
         return true;
     }
 
+    /**
+     * 检查字符号码段是否与现有队伍冲突
+     *
+     * @param minCode 最小号码
+     * @param maxCode 最大号码
+     * @param eventId 赛事ID
+     * @param excludeTeamId 排除的队伍ID
+     * @return 是否冲突
+     */
+    private boolean isNumberRangeConflictWithChars(String minCode, String maxCode, Long eventId, Long excludeTeamId) {
+        List<GameTeam> existingTeams = gameTeamService.queryTeamByEventId(eventId);
+
+        for (GameTeam team : existingTeams) {
+            // 排除当前队伍
+            if (excludeTeamId != null && team.getTeamId().equals(excludeTeamId)) {
+                continue;
+            }
+
+            if (StringUtils.isNotBlank(team.getNumberRange())) {
+                String[] rangeParts = team.getNumberRange().split("-");
+                if (rangeParts.length == 2) {
+                    String existingMin = rangeParts[0];
+                    String existingMax = rangeParts[1];
+
+                    // 使用ASCII比较检查重叠
+                    if (isRangeOverlapWithChars(minCode, maxCode, existingMin, existingMax)) {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * 检查两个号码段是否有重叠
+     */
+    private boolean isRangeOverlapWithChars(String min1, String max1, String min2, String max2) {
+        // 如果max1 < min2 或 max2 < min1,则没有重叠
+        return !(compareCodesByAscii(max1, min2) < 0 || compareCodesByAscii(max2, min1) < 0);
+    }
+
     /**
      * 生成号码段
      * 规则:
@@ -679,21 +676,258 @@ public class IEnrollServiceImpl implements IEnrollService {
         return startStr + "-" + endStr;
     }
 
+    /**
+     * 生成基于填写号码的号码段(支持字符)
+     *
+     * @param providedCodes 填写的号码列表
+     * @param eventId 赛事ID
+     * @param excludeTeamId 排除的队伍ID
+     * @return 号码段字符串
+     */
+    private String generateCustomNumberRangeWithChars(List<String> providedCodes, Long eventId, Long excludeTeamId) {
+        if (providedCodes.isEmpty()) {
+            return null;
+        }
+
+        // 过滤和验证号码
+        List<String> validCodes = providedCodes.stream()
+            .filter(StringUtils::isNotBlank)
+            .map(String::trim)
+            .filter(this::isValidAthleteCode)
+            .collect(Collectors.toList());
+
+        if (validCodes.isEmpty()) {
+            return null;
+        }
+
+        // 按ASCII值排序
+        List<String> sortedCodes = sortCodesByAscii(validCodes);
+        String minCode = sortedCodes.get(0);
+        String maxCode = sortedCodes.get(sortedCodes.size() - 1);
+
+        // 生成号码段
+        String numberRange = generateRangeFromCodes(minCode, maxCode);
+
+        // 检查是否与现有号码段冲突
+        if (isNumberRangeConflictWithChars(minCode, maxCode, eventId, excludeTeamId)) {
+           // 如果冲突,尝试扩展范围
+            numberRange = expandRangeToAvoidConflict(minCode, maxCode, eventId, excludeTeamId);
+            if (numberRange == null) {
+                log.warn("无法为填写的号码生成合适的号码段,使用默认方式");
+                return null; // 返回null,让调用方使用默认方式
+            }
+        }
+
+        log.info("队伍使用自定义号码段: {} (基于填写的号码: {}-{})",
+            numberRange, minCode, maxCode);
+
+        return numberRange;
+    }
+
+    /**
+     * 根据最小和最大号码生成号码段
+     *
+     * @param minCode 最小号码
+     * @param maxCode 最大号码
+     * @return 号码段字符串
+     */
+    private String generateRangeFromCodes(String minCode, String maxCode) {
+        // 简单的实现:直接使用最小和最大号码
+        return minCode + "-" + maxCode;
+    }
+
+    /**
+     * 通过前缀扩展号码段
+     */
+    private String expandByPrefix(String minCode, String maxCode, String prefix) {
+        return prefix + minCode + "-" + prefix + maxCode;
+    }
+
+    /**
+     * 通过后缀扩展号码段
+     */
+    private String expandBySuffix(String minCode, String maxCode, String suffix) {
+        return minCode + suffix + "-" + maxCode + suffix;
+    }
+
+    /**
+     * 通过填充扩展号码段
+     */
+    private String expandByPadding(String minCode, String maxCode, String padding) {
+        // 确保最小长度一致
+        int maxLength = Math.max(minCode.length(), maxCode.length());
+        String paddedMin = String.format("%-" + maxLength + "s", minCode).replace(' ', padding.charAt(0));
+        String paddedMax = String.format("%-" + maxLength + "s", maxCode).replace(' ', padding.charAt(0));
+
+        return paddedMin + "-" + paddedMax;
+    }
+
+    /**
+     * 扩展号码段以避免冲突
+     *
+     * @param minCode 最小号码
+     * @param maxCode 最大号码
+     * @param eventId 赛事ID
+     * @param excludeTeamId 排除的队伍ID
+     * @return 扩展后的号码段
+     */
+    private String expandRangeToAvoidConflict(String minCode, String maxCode, Long eventId, Long excludeTeamId) {
+        // 尝试不同的扩展策略
+        String[] prefixes = {"A", "B", "C", "D", "E"};
+        String[] suffixes = {"A", "B", "C", "D", "E"};
+
+        // 1. 尝试前缀扩展
+        for (String prefix : prefixes) {
+            String expandedRange = expandByPrefix(minCode, maxCode, prefix);
+            if (!isNumberRangeConflictWithChars(
+                expandedRange.split("-")[0], expandedRange.split("-")[1], eventId, excludeTeamId)) {
+                return expandedRange;
+            }
+        }
+
+        // 2. 尝试后缀扩展
+        for (String suffix : suffixes) {
+            String expandedRange = expandBySuffix(minCode, maxCode, suffix);
+            if (!isNumberRangeConflictWithChars(
+                expandedRange.split("-")[0], expandedRange.split("-")[1], eventId, excludeTeamId)) {
+                return expandedRange;
+            }
+        }
+
+        // 3. 尝试数字扩展
+        for (int i = 1; i <= 9; i++) {
+            String expandedRange = expandByPrefix(minCode, maxCode, String.valueOf(i));
+            if (!isNumberRangeConflictWithChars(
+                expandedRange.split("-")[0], expandedRange.split("-")[1], eventId, excludeTeamId)) {
+                return expandedRange;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * 比较两个号码的ASCII值大小
+     *
+     * @param code1 号码1
+     * @param code2 号码2
+     * @return 比较结果:负数表示code1<code2,0表示相等,正数表示code1>code2
+     */
+    private int compareCodesByAscii(String code1, String code2) {
+        if (code1 == null && code2 == null) {
+            return 0;
+        }
+        if (code1 == null) {
+            return -1;
+        }
+        if (code2 == null) {
+            return 1;
+        }
+
+        // 按字符逐个比较ASCII值
+        int minLength = Math.min(code1.length(), code2.length());
+
+        for (int i = 0; i < minLength; i++) {
+            char char1 = code1.charAt(i);
+            char char2 = code2.charAt(i);
+
+            if (char1 != char2) {
+                return char1 - char2; // 返回ASCII值差
+            }
+        }
+
+        // 如果前面字符都相同,比较长度
+        return code1.length() - code2.length();
+    }
+
+    /**
+     * 对号码列表按ASCII值排序
+     *
+     * @param codes 号码列表
+     * @return 排序后的号码列表
+     */
+    private List<String> sortCodesByAscii(List<String> codes) {
+        return codes.stream()
+            .filter(StringUtils::isNotBlank)
+            .map(String::trim)
+            .sorted(this::compareCodesByAscii)
+            .collect(Collectors.toList());
+    }
+
+    /**
+     * 验证运动员号码格式(支持字符)
+     *
+     * @param athleteCode 运动员号码
+     * @return 是否有效
+     */
+    private boolean isValidAthleteCode(String athleteCode) {
+        if (StringUtils.isBlank(athleteCode)) {
+            return false;
+        }
+
+        String trimmedCode = athleteCode.trim();
+
+        // 检查长度
+        if (trimmedCode.length() < 1 || trimmedCode.length() > 15) {
+            return false;
+        }
+
+        // 允许字母、数字、连字符、下划线、点号
+        if (!trimmedCode.matches("^[A-Za-z0-9\\-_\\.]+$")) {
+            return false;
+        }
+
+        // 检查是否包含连续的特殊字符
+        if (trimmedCode.matches(".*[\\-_\\.]{2,}.*")) {
+            return false;
+        }
+
+        return true;
+    }
+
     /**
      * 创建运动员BO对象
      */
-    private GameAthleteBo createAthleteBo(EnrollProjectVo enrollInfo, Long eventId, Long teamId,
-                                          String teamName, AtomicInteger currentNumber, int width,
-                                          Map<String, Long> projectList) {
+    private GameAthleteBo createAthleteBo(EnrollProjectVo enrollInfo,
+                                            Long eventId, Long teamId,
+                                            String teamName,
+                                            AtomicInteger currentNumber, int width,
+                                            Map<String, Long> projectList) {
         GameAthleteBo gameAthleteBo = new GameAthleteBo();
         gameAthleteBo.setEventId(eventId);
         gameAthleteBo.setTeamId(teamId);
         gameAthleteBo.setTeamName(teamName);
 
-        // 分配编号
-        int assignedNumber = currentNumber.getAndIncrement();
-        String formattedCode = String.format("%0" + width + "d", assignedNumber);
-        gameAthleteBo.setAthleteCode(formattedCode);
+        // 号码处理逻辑
+        String athleteCode;
+        if (StringUtils.isNotBlank(enrollInfo.getAthleteCode())) {
+            // 如果Excel中有号码,使用Excel中的号码
+            athleteCode = enrollInfo.getAthleteCode().trim();
+            
+            // 验证号码格式
+            if (!isValidAthleteCode(athleteCode)) {
+                log.warn("运动员{}的号码格式不正确: {},将使用自动生成的号码", enrollInfo.getName(), athleteCode);
+                // 如果号码格式不正确,使用自动生成的号码
+                if (currentNumber != null) {
+                    int assignedNumber = currentNumber.getAndIncrement();
+                    athleteCode = String.format("%0" + width + "d", assignedNumber);
+                } else {
+                    // 字符号码段情况下,生成一个基于序号的号码
+                    athleteCode = "AUTO_" + (System.currentTimeMillis() % 10000);
+                }
+            }
+        } else {
+            // 如果Excel中没有号码,自动生成
+            if (currentNumber != null) {
+                int assignedNumber = currentNumber.getAndIncrement();
+                athleteCode = String.format("%0" + width + "d", assignedNumber);
+            } else {
+                // 字符号码段情况下,生成一个基于序号的号码
+                athleteCode = "AUTO_" + (System.currentTimeMillis() % 10000);
+            }
+        }
+
+        gameAthleteBo.setAthleteCode(athleteCode);
 
         gameAthleteBo.setName(enrollInfo.getName());
         gameAthleteBo.setGender(enrollInfo.getSex());
@@ -754,4 +988,289 @@ public class IEnrollServiceImpl implements IEnrollService {
     }
 
     // endregion 辅助方法
+
+    /**
+     * 检查号码是否已存在
+     *
+     * @param athleteCode 运动员号码
+     * @param eventId 赛事ID
+     * @return 是否已存在
+     */
+    private boolean isAthleteCodeExists(String athleteCode, Long eventId) {
+        if (StringUtils.isBlank(athleteCode)) {
+            return false;
+        }
+
+        GameAthleteBo bo = new GameAthleteBo();
+        bo.setEventId(eventId);
+        bo.setAthleteCode(athleteCode);
+
+        List<GameAthleteVo> existingAthletes = gameAthleteService.queryList(bo);
+        return existingAthletes.size() > 0;
+    }
+
+    /**
+     * 校验基本数据完整性
+     */
+    private List<EnrollValidationError> validateBasicData(EnrollProjectVo enroll, int rowIndex) {
+        List<EnrollValidationError> errors = new ArrayList<>();
+
+        if (StringUtils.isBlank(enroll.getName())) {
+            EnrollValidationError error = new EnrollValidationError();
+            error.setRowIndex(rowIndex + 1);
+            error.setErrorType("MISSING_NAME");
+            error.setErrorMessage("运动员姓名不能为空");
+            errors.add(error);
+        }
+
+        if (StringUtils.isBlank(enroll.getTeamName())) {
+            EnrollValidationError error = new EnrollValidationError();
+            error.setRowIndex(rowIndex + 1);
+            error.setAthleteName(enroll.getName());
+            error.setErrorType("MISSING_TEAM");
+            error.setErrorMessage("队伍名称不能为空");
+            errors.add(error);
+        }
+
+        // 性别校验 - 使用字典工具验证
+        if (StringUtils.isBlank(enroll.getSex())) {
+            EnrollValidationError error = new EnrollValidationError();
+            error.setRowIndex(rowIndex + 1);
+            error.setAthleteName(enroll.getName());
+            error.setTeamName(enroll.getTeamName());
+            error.setErrorType("MISSING_GENDER");
+            error.setErrorMessage("性别不能为空");
+            errors.add(error);
+        } else if (!GenderDictUtils.isValidGender(enroll.getSex())) {
+            EnrollValidationError error = new EnrollValidationError();
+            error.setRowIndex(rowIndex + 1);
+            error.setAthleteName(enroll.getName());
+            error.setTeamName(enroll.getTeamName());
+            error.setErrorType("INVALID_GENDER");
+            error.setErrorMessage("性别值无效,请填写'男'、'女'、'1'或'2'");
+            errors.add(error);
+        }
+
+        return errors;
+    }
+
+    @Override
+    public EnrollValidationResult validateEnrollData(List<EnrollProjectVo> enrollList, Long eventId) {
+        EnrollValidationResult result = new EnrollValidationResult();
+        result.setValid(true);
+        result.setErrors(new ArrayList<>());
+        result.setValidData(new ArrayList<>());
+
+        if (enrollList == null || enrollList.isEmpty()) {
+            log.warn("导入数据为空,赛事ID: {}", eventId);
+            return result;
+        }
+
+        if (eventId == null) {
+            log.error("赛事ID不能为空");
+            throw new IllegalArgumentException("赛事ID不能为空");
+        }
+
+        try {
+            // 1. 获取赛事配置:每人限报项目数
+            Integer maxProjectsPerPerson = getMaxProjectsPerPerson(eventId);
+
+            // 2. 获取项目限制信息
+            Map<String, Integer> projectLimits = getProjectLimits(eventId);
+
+            // 3. 统计每个项目的当前报名人数
+            Map<String, Integer> currentProjectCounts = getCurrentProjectCounts(eventId);
+
+            // 4. 校验每个运动员
+            for (int i = 0; i < enrollList.size(); i++) {
+                EnrollProjectVo enroll = enrollList.get(i);
+                // 先进行基本数据校验
+                List<EnrollValidationError> basicErrors = validateBasicData(enroll, i);
+                if (!basicErrors.isEmpty()) {
+                    result.setValid(false);
+                    result.getErrors().addAll(basicErrors);
+                    continue; // 基本数据有问题,跳过后续校验
+                }
+
+                List<EnrollValidationError> athleteErrors = validateAthlete(eventId, enroll, i,
+                    maxProjectsPerPerson, projectLimits, currentProjectCounts);
+
+                if (athleteErrors.isEmpty()) {
+                    result.getValidData().add(enroll);
+                    // 更新项目计数(模拟保存后的状态)
+                    updateProjectCounts(enroll, currentProjectCounts);
+                } else {
+                    result.setValid(false);
+                    result.getErrors().addAll(athleteErrors);
+                }
+            }
+        } catch (Exception e) {
+            log.error("校验导入数据时发生异常,赛事ID: {}", eventId, e);
+            result.setValid(false);
+            EnrollValidationError error = new EnrollValidationError();
+            error.setErrorType("SYSTEM_ERROR");
+            error.setErrorMessage("系统校验异常:" + e.getMessage());
+            result.getErrors().add(error);
+        }
+
+        return result;
+    }
+
+    @Override
+    public EnrollImportResult importDataWithValidation(MultipartFile file, Long eventId) {
+        // 1. 解析数据
+        List<EnrollProjectVo> enrollList = parseData(file);
+
+        // 2. 校验数据
+        EnrollValidationResult validationResult = validateEnrollData(enrollList, eventId);
+
+        // 3. 保存有效数据
+        boolean saveSuccess = false;
+        if (!validationResult.getValidData().isEmpty()) {
+            saveSuccess = saveEnrollData(validationResult.getValidData(), eventId);
+        }
+
+        // 4. 返回结果
+        EnrollImportResult result = new EnrollImportResult();
+        result.setSuccess(saveSuccess);
+        result.setValidationResult(validationResult);
+        result.setTotalCount(enrollList.size());
+        result.setValidCount(validationResult.getValidData().size());
+        result.setErrorCount(validationResult.getErrors().size());
+
+        return result;
+    }
+
+    /**
+     * 获取每人限报项目数
+     */
+    private Integer getMaxProjectsPerPerson(Long eventId) {
+        GameEventVo event = gameEventService.queryById(eventId);
+        return event.getLimitApplication() != null ? event.getLimitApplication() : 0; // 默认0个
+    }
+
+    /**
+     * 获取项目限制信息
+     */
+    private Map<String, Integer> getProjectLimits(Long eventId) {
+        List<GameEventProjectVo> projects = gameEventProjectService.queryListByEventId(eventId);
+        return projects.stream()
+            .filter(p -> p.getLimitPerson() != null && p.getLimitPerson() > 0)
+            .collect(Collectors.toMap(
+                GameEventProjectVo::getProjectName,
+                GameEventProjectVo::getLimitPerson
+            ));
+    }
+
+    /**
+     * 获取当前项目报名人数
+     */
+    private Map<String, Integer> getCurrentProjectCounts(Long eventId) {
+        GameAthleteBo bo = new GameAthleteBo();
+        bo.setEventId(eventId);
+        List<GameAthleteVo> athletes = gameAthleteService.queryList(bo);
+        Map<String, Integer> counts = new HashMap<>();
+
+        for (GameAthleteVo athlete : athletes) {
+            if (StringUtils.isNotBlank(athlete.getProjectValue())) {
+                List<Long> projectIds = JSONUtil.toList(athlete.getProjectValue(), Long.class);
+                for (Long projectId : projectIds) {
+                    GameEventProjectVo project = gameEventProjectService.queryById(projectId);
+                    if (project != null) {
+                        counts.merge(project.getProjectName(), 1, Integer::sum);
+                    }
+                }
+            }
+        }
+
+        return counts;
+    }
+
+    /**
+     * 校验单个运动员
+     */
+    private List<EnrollValidationError> validateAthlete(Long eventId, EnrollProjectVo enroll, int rowIndex,
+                                                        Integer maxProjectsPerPerson, Map<String, Integer> projectLimits,
+                                                        Map<String, Integer> currentProjectCounts) {
+
+        List<EnrollValidationError> errors = new ArrayList<>();
+
+        // 号码冲突检查
+        if (StringUtils.isNotBlank(enroll.getAthleteCode())) {
+            if (isAthleteCodeExists(enroll.getAthleteCode(), eventId)) {
+                EnrollValidationError error = new EnrollValidationError();
+                error.setRowIndex(rowIndex + 1);
+                error.setAthleteName(enroll.getName());
+                error.setTeamName(enroll.getTeamName());
+                error.setErrorType("DUPLICATE_ATHLETE_CODE");
+                error.setErrorMessage(String.format("运动员号码'%s'已存在", enroll.getAthleteCode()));
+                errors.add(error);
+            }
+        }
+
+        // 检查项目选择数据是否为空
+        if (enroll.getProjectSelections() == null || enroll.getProjectSelections().isEmpty()) {
+            EnrollValidationError error = new EnrollValidationError();
+            error.setRowIndex(rowIndex + 1);
+            error.setAthleteName(enroll.getName());
+            error.setTeamName(enroll.getTeamName());
+            error.setErrorType("NO_PROJECT_SELECTED");
+            error.setErrorMessage("该运动员未选择任何项目");
+            errors.add(error);
+            return errors; // 如果没有选择项目,直接返回
+        }
+
+        // 1. 校验每人限报项目数
+        long selectedProjectCount = enroll.getProjectSelections().values().stream()
+            .mapToLong(selected -> selected ? 1 : 0)
+            .sum();
+
+        if (maxProjectsPerPerson > 0 && selectedProjectCount > maxProjectsPerPerson) {
+            EnrollValidationError error = new EnrollValidationError();
+            error.setRowIndex(rowIndex + 1); // 从1开始计数
+            error.setAthleteName(enroll.getName());
+            error.setTeamName(enroll.getTeamName());
+            error.setErrorType("PERSON_LIMIT_EXCEEDED");
+            error.setErrorMessage(String.format("该运动员选择了%d个项目,超出每人限报%d个项目",
+                selectedProjectCount, maxProjectsPerPerson));
+            errors.add(error);
+        }
+
+        // 2. 校验项目限报人数
+        for (Map.Entry<String, Boolean> entry : enroll.getProjectSelections().entrySet()) {
+            if (entry.getValue()) { // 如果选择了该项目
+                String projectName = entry.getKey();
+                Integer projectLimit = projectLimits.get(projectName);
+
+                if (projectLimit != null && projectLimit > 0) {
+                    int currentCount = currentProjectCounts.getOrDefault(projectName, 0);
+                    if (currentCount >= projectLimit) {
+                        EnrollValidationError error = new EnrollValidationError();
+                        error.setRowIndex(rowIndex + 1);
+                        error.setAthleteName(enroll.getName());
+                        error.setTeamName(enroll.getTeamName());
+                        error.setErrorType("PROJECT_LIMIT_EXCEEDED");
+                        error.setProjectName(projectName);
+                        error.setErrorMessage(String.format("项目'%s'报名人数已达上限%d人",
+                            projectName, projectLimit));
+                        errors.add(error);
+                    }
+                }
+            }
+        }
+
+        return errors;
+    }
+
+    /**
+     * 更新项目计数(模拟保存后的状态)
+     */
+    private void updateProjectCounts(EnrollProjectVo enroll, Map<String, Integer> currentProjectCounts) {
+        for (Map.Entry<String, Boolean> entry : enroll.getProjectSelections().entrySet()) {
+            if (entry.getValue()) {
+                String projectName = entry.getKey();
+                currentProjectCounts.merge(projectName, 1, Integer::sum);
+            }
+        }
+    }
 }

+ 123 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/utils/GenderDictUtils.java

@@ -0,0 +1,123 @@
+package org.dromara.system.utils;
+
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.common.core.service.DictService;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+
+/**
+ * 性别字典转换工具类
+ */
+@Slf4j
+public class GenderDictUtils {
+    private static final String GENDER_DICT_TYPE = "sys_user_sex";
+
+    /**
+     * 将性别文本转换为字典值
+     * 支持:男/女 -> 1/2,或者直接传入1/2
+     *
+     * @param genderText 性别文本(男、女、1、2等)
+     * @return 字典值(1或2)
+     */
+    public static String convertGenderToDictValue(String genderText) {
+        if (StringUtils.isBlank(genderText)) {
+            return null;
+        }
+
+        String trimmedText = genderText.trim();
+
+        // 如果已经是数字,直接返回
+        if (trimmedText.matches("^[12]$")) {
+            return trimmedText;
+        }
+
+        // 通过字典服务转换
+        try {
+            DictService dictService = SpringUtils.getBean(DictService.class);
+            String dictValue = dictService.getDictValue(GENDER_DICT_TYPE, trimmedText);
+
+            if (StringUtils.isNotBlank(dictValue)) {
+                return dictValue;
+            }
+        } catch (Exception e) {
+            log.warn("通过字典服务转换性别失败,使用默认映射: {}", e.getMessage());
+        }
+
+        // 如果字典服务转换失败,使用默认映射
+        return convertGenderByDefault(trimmedText);
+    }
+
+    /**
+     * 默认性别映射规则
+     *
+     * @param genderText 性别文本
+     * @return 字典值
+     */
+    private static String convertGenderByDefault(String genderText) {
+        switch (genderText) {
+            case "男":
+            case "男性":
+            case "M":
+            case "male":
+            case "Male":
+                return "1";
+            case "女":
+            case "女性":
+            case "F":
+            case "female":
+            case "Female":
+                return "2";
+            default:
+                log.warn("无法识别的性别值: {}", genderText);
+                return null;
+        }
+    }
+
+    /**
+     * 将字典值转换为性别文本
+     *
+     * @param dictValue 字典值(0或1)
+     * @return 性别文本
+     */
+    public static String convertDictValueToGender(String dictValue) {
+        if (StringUtils.isBlank(dictValue)) {
+            return null;
+        }
+
+        try {
+            DictService dictService = SpringUtils.getBean(DictService.class);
+            String dictLabel = dictService.getDictLabel(GENDER_DICT_TYPE, dictValue);
+
+            if (StringUtils.isNotBlank(dictLabel)) {
+                return dictLabel;
+            }
+        } catch (Exception e) {
+            log.warn("通过字典服务转换性别标签失败,使用默认映射: {}", e.getMessage());
+        }
+
+        // 默认映射
+        switch (dictValue) {
+            case "1":
+                return "男";
+            case "2":
+                return "女";
+            default:
+                return dictValue;
+        }
+    }
+
+    /**
+     * 验证性别值是否有效
+     *
+     * @param genderValue 性别值
+     * @return 是否有效
+     */
+    public static boolean isValidGender(String genderValue) {
+        if (StringUtils.isBlank(genderValue)) {
+            return false;
+        }
+
+        String dictValue = convertGenderToDictValue(genderValue);
+        return "1".equals(dictValue) || "2".equals(dictValue);
+    }
+}

BIN
ruoyi-modules/ruoyi-game-event/src/main/resources/template/enroll_template.xlsx