Преглед изворни кода

feat(enroll): 优化报名服务实现逻辑

- 重构代码格式,改善代码可读性
- 预加载项目分类信息以区分个人/团体项目
- 新增团队项目汇总功能并存储到队伍信息中
- 实现覆盖式校验统计机制,提升数据准确性
- 优化项目报名人数和性别统计逻辑
- 移除重复的号码冲突检查逻辑
- 更新数值转换方法使用 parseInt 替代 valueOf
- 修复多处字符串处理和集合操作的性能问题
zhou пре 1 недеља
родитељ
комит
07dfa6e3e0

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

@@ -531,7 +531,8 @@ public class IEnrollServiceImpl implements IEnrollService {
         }
 
         // 只有明确表示"选择"的值才返回true
-        return value.equals("是") || value.equals("yes") || value.equals("true") || value.equals("1") || value.contains("✔") || value.contains("✅") || value.contains("√");
+        return value.equals("是") || value.equals("yes") || value.equals("true") || value.equals("1")
+                || value.contains("✔") || value.contains("✅") || value.contains("√");
     }
 
     /**
@@ -638,6 +639,14 @@ public class IEnrollServiceImpl implements IEnrollService {
         Map<String, Long> existingAthleteMap = existingAthletes.stream()
                 .collect(Collectors.toMap(GameAthlete::getAthleteCode, GameAthlete::getAthleteId, (v1, v2) -> v1));
 
+        // 1.5 预加载项目分类信息,用于区分个人/团体项目
+        List<GameEventProject> allProjectConfigs = gameEventProjectMapper
+                .selectList(new LambdaQueryWrapper<GameEventProject>()
+                        .select(GameEventProject::getProjectId, GameEventProject::getClassification)
+                        .eq(GameEventProject::getEventId, eventId));
+        Map<Long, String> projectIdToClassification = allProjectConfigs.stream()
+                .collect(Collectors.toMap(GameEventProject::getProjectId, GameEventProject::getClassification));
+
         // 2. 处理每个队伍
         for (Map.Entry<String, List<EnrollProjectVo>> entry : groupedByTeam.entrySet()) {
             String teamName = entry.getKey();
@@ -697,6 +706,20 @@ public class IEnrollServiceImpl implements IEnrollService {
                 }
             }
 
+            // 3.5 汇总该队伍在本次导入中所选的【团体项目】
+            Set<Long> teamProjectIds = new HashSet<>();
+            for (EnrollProjectVo athlete : athletes) {
+                for (Map.Entry<String, Boolean> pEntry : athlete.getProjectSelections().entrySet()) {
+                    if (pEntry.getValue()) {
+                        Long pId = projectList.get(pEntry.getKey());
+                        // 只保存团体项目(Classification = '1')
+                        if (pId != null && "1".equals(projectIdToClassification.get(pId))) {
+                            teamProjectIds.add(pId);
+                        }
+                    }
+                }
+            }
+
             // 4. 处理队伍信息(内存合并,最后批量提交)
             if (isNewTeam) {
                 // 构建新队伍
@@ -707,6 +730,7 @@ public class IEnrollServiceImpl implements IEnrollService {
                 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");
                 team.setCreateTime(DateTime.now());
@@ -715,6 +739,18 @@ public class IEnrollServiceImpl implements IEnrollService {
                 // 合并现有队伍信息
                 existingTeam.setNumberRange(numberRange);
 
+                // 合并项目列表
+                Set<Long> existingProjectIds = new HashSet<>();
+                if (StringUtils.isNotBlank(existingTeam.getProjectValue())) {
+                    try {
+                        existingProjectIds.addAll(JSONUtil.toList(existingTeam.getProjectValue(), Long.class));
+                    } catch (Exception e) {
+                        log.warn("解析队伍{}的原有项目ID失败", teamName);
+                    }
+                }
+                existingProjectIds.addAll(teamProjectIds);
+                existingTeam.setProjectValue(JSONUtil.toJsonStr(existingProjectIds));
+
                 // 合并运动员 ID 列表
                 List<Long> currentAthleteIds = new ArrayList<>();
                 if (StringUtils.isNotBlank(existingTeam.getAthleteValue())) {
@@ -1076,7 +1112,6 @@ public class IEnrollServiceImpl implements IEnrollService {
         return gameAthleteBo;
     }
 
-
     /**
      * 验证号码段格式是否有效
      * 号码段格式为:xxxx-yyyy(如:0001-0300)
@@ -1125,7 +1160,7 @@ public class IEnrollServiceImpl implements IEnrollService {
                 .map(EnrollProjectVo::getAthleteCode)
                 .filter(StringUtils::isNotBlank)
                 .map(String::trim)
-                .filter(code -> StringUtils.isNotBlank(code))
+                .filter(StringUtils::isNotBlank)
                 .collect(Collectors.toList());
 
         if (!providedCodes.isEmpty()) {
@@ -1155,7 +1190,7 @@ public class IEnrollServiceImpl implements IEnrollService {
         String[] rangeParts = numberRange.split("-");
         if (rangeParts.length == 2) {
             try {
-                int startNumber = Integer.valueOf(rangeParts[0]);
+                int startNumber = Integer.parseInt(rangeParts[0]);
                 return new AtomicInteger(startNumber);
             } catch (NumberFormatException e) {
                 log.warn("解析号码段起始号码失败: {}, 使用默认值", numberRange);
@@ -1188,12 +1223,12 @@ public class IEnrollServiceImpl implements IEnrollService {
         try {
             // 查询队伍的最大队员编号
             String maxNumberStr = gameAthleteService.queryMaxNumber(teamId);
-            Integer maxNumber = Integer.valueOf(maxNumberStr);
+            int maxNumber = Integer.parseInt(maxNumberStr);
 
             // 如果最大编号为0,从号码段起始开始
             if (maxNumber == 0) {
                 String[] rangeParts = numberRange.split("-");
-                int startNumber = Integer.valueOf(rangeParts[0]);
+                int startNumber = Integer.parseInt(rangeParts[0]);
                 return new AtomicInteger(startNumber);
             } else {
                 // 续着分配
@@ -1202,29 +1237,11 @@ public class IEnrollServiceImpl implements IEnrollService {
         } catch (Exception e) {
             log.warn("查询队伍{}的号码失败: {},从号码段起始开始", teamName, numberRange, e);
             String[] rangeParts = numberRange.split("-");
-            int startNumber = Integer.valueOf(rangeParts[0]);
+            int startNumber = Integer.parseInt(rangeParts[0]);
             return new AtomicInteger(startNumber);
         }
     }
 
-    /**
-     * 检查号码是否已存在
-     *
-     * @param athleteCode 运动员号码
-     * @param eventId     赛事ID
-     * @return 是否已存在
-     */
-    private boolean isAthleteCodeExists(String athleteCode, Long eventId) {
-        if (StringUtils.isBlank(athleteCode)) {
-            return false;
-        }
-
-        Long count = gameAthleteMapper.selectCount(new LambdaQueryWrapper<GameAthlete>()
-                .eq(GameAthlete::getEventId, eventId)
-                .eq(GameAthlete::getAthleteCode, athleteCode));
-        return count > 0;
-    }
-
     /**
      * 校验基本数据完整性
      */
@@ -1295,23 +1312,25 @@ public class IEnrollServiceImpl implements IEnrollService {
             Integer maxProjectsPerPerson = (event != null) ? event.getLimitApplication() : 0;
 
             // 2. 获取项目限制信息:仅查询必要的核心字段
-            List<GameEventProject> projects = gameEventProjectMapper.selectList(new LambdaQueryWrapper<GameEventProject>()
-                    .select(GameEventProject::getProjectId, GameEventProject::getProjectName,
-                            GameEventProject::getLimitMale, GameEventProject::getLimitFemale,
-                            GameEventProject::getLimitTeam, GameEventProject::getClassification)
-                    .eq(GameEventProject::getEventId, eventId));
+            List<GameEventProject> projects = gameEventProjectMapper
+                    .selectList(new LambdaQueryWrapper<GameEventProject>()
+                            .select(GameEventProject::getProjectId, GameEventProject::getProjectName,
+                                    GameEventProject::getLimitMale, GameEventProject::getLimitFemale,
+                                    GameEventProject::getLimitTeam, GameEventProject::getClassification)
+                            .eq(GameEventProject::getEventId, eventId));
 
             // 提前构建项目 ID -> 对象缓存 (用于内部统计方法)
             Map<Long, GameEventProject> projectIdMap = projects.stream()
-                .collect(Collectors.toMap(GameEventProject::getProjectId, Function.identity()));
+                    .collect(Collectors.toMap(GameEventProject::getProjectId, Function.identity()));
             // 构建项目名 -> 对象缓存 (供 validateAthlete 逻辑兼容使用)
             Map<String, GameEventProjectVo> projectLimits = projects.stream()
-                .map(p -> BeanUtil.copyProperties(p, GameEventProjectVo.class))
-                .collect(Collectors.toMap(GameEventProjectVo::getProjectName, Function.identity()));
+                    .map(p -> BeanUtil.copyProperties(p, GameEventProjectVo.class))
+                    .collect(Collectors.toMap(GameEventProjectVo::getProjectName, Function.identity()));
 
             // 3. 预加载所有参与校验的数据:仅查询统计所需的最小字段集
-            List<GameAthlete> allAthletes = gameAthleteMapper.selectList(new LambdaQueryWrapper<GameAthlete>()
-                    .select(GameAthlete::getGender, GameAthlete::getProjectValue, GameAthlete::getTeamId)
+            List<GameAthlete> allAthletesFromDb = gameAthleteMapper.selectList(new LambdaQueryWrapper<GameAthlete>()
+                    .select(GameAthlete::getGender, GameAthlete::getProjectValue, GameAthlete::getTeamId,
+                            GameAthlete::getAthleteCode)
                     .eq(GameAthlete::getEventId, eventId));
 
             List<GameTeam> allTeams = gameTeamMapper.selectList(new LambdaQueryWrapper<GameTeam>()
@@ -1320,13 +1339,31 @@ public class IEnrollServiceImpl implements IEnrollService {
 
             // 构建队伍 ID -> 名称的映射,供运动员统计使用
             Map<Long, String> teamIdToNameMap = allTeams.stream()
-                .filter(t -> t.getTeamId() != null)
-                .collect(Collectors.toMap(GameTeam::getTeamId, GameTeam::getTeamName));
-
-            // 4. 统计状态(使用精简后的数据在内存中计算)
-            Map<String, Map<String, Integer>> projectGenderCounts = calculateProjectGenderCounts(allAthletes, projectIdMap);
-            Map<String, Set<String>> projectTeams = calculateProjectTeams(allTeams, projectIdMap);
-            Map<String, Map<String, Map<String, Integer>>> teamProjectGenderCounts = calculateTeamProjectGenderCounts(allAthletes, projectIdMap, teamIdToNameMap);
+                    .filter(t -> t.getTeamId() != null)
+                    .collect(Collectors.toMap(GameTeam::getTeamId, GameTeam::getTeamName));
+
+            // --- 核心优化:实现覆盖式校验统计 ---
+            // 获取导入列表中所有的运动员编号
+            Set<String> importAthleteCodes = enrollList.stream()
+                    .map(EnrollProjectVo::getAthleteCode)
+                    .filter(StringUtils::isNotBlank)
+                    .collect(Collectors.toSet());
+
+            // 初始统计时,排除掉这些即将在导入中被覆盖的运动员
+            List<GameAthlete> initialStatAthletes = allAthletesFromDb.stream()
+                    .filter(a -> !importAthleteCodes.contains(a.getAthleteCode()))
+                    .collect(Collectors.toList());
+
+            // 初始统计时,暂时保留所有队伍(因为队伍是合并逻辑,后面会处理)
+            // ---------------------------------
+
+            // 4. 统计状态(使用排除后的数据在内存中计算,此时统计的是“除导入列表外”的存量数据)
+            Map<String, Map<String, Integer>> projectGenderCounts = calculateProjectGenderCounts(initialStatAthletes,
+                    projectIdMap);
+            Map<String, Set<String>> projectTeams = calculateProjectTeams(initialStatAthletes, projectIdMap,
+                    teamIdToNameMap);
+            Map<String, Map<String, Map<String, Integer>>> teamProjectGenderCounts = calculateTeamProjectGenderCounts(
+                    initialStatAthletes, projectIdMap, teamIdToNameMap);
 
             // 4. 校验每个运动员
             for (int i = 0; i < enrollList.size(); i++) {
@@ -1389,11 +1426,11 @@ public class IEnrollServiceImpl implements IEnrollService {
         return result;
     }
 
-
     /**
      * 内存统计项目报名人数和性别
      */
-    private Map<String, Map<String, Integer>> calculateProjectGenderCounts(List<GameAthlete> athletes, Map<Long, GameEventProject> projectMap) {
+    private Map<String, Map<String, Integer>> calculateProjectGenderCounts(List<GameAthlete> athletes,
+            Map<Long, GameEventProject> projectMap) {
         Map<String, Map<String, Integer>> counts = new HashMap<>();
         for (GameAthlete athlete : athletes) {
             if (StringUtils.isNotBlank(athlete.getProjectValue())) {
@@ -1401,7 +1438,8 @@ public class IEnrollServiceImpl implements IEnrollService {
                 for (Long projectId : projectIds) {
                     GameEventProject project = projectMap.get(projectId);
                     if (project != null) {
-                        Map<String, Integer> genderMap = counts.computeIfAbsent(project.getProjectName(), k -> new HashMap<>());
+                        Map<String, Integer> genderMap = counts.computeIfAbsent(project.getProjectName(),
+                                k -> new HashMap<>());
                         genderMap.merge(athlete.getGender(), 1, Integer::sum);
                         genderMap.merge("TOTAL", 1, Integer::sum);
                     }
@@ -1412,18 +1450,21 @@ public class IEnrollServiceImpl implements IEnrollService {
     }
 
     /**
-     * 内存统计项目已报名的队伍列表
+     * 内存统计项目已报名的队伍列表(通过运动员数据汇总,确保绝对准确)
      */
-    private Map<String, Set<String>> calculateProjectTeams(List<GameTeam> teams, Map<Long, GameEventProject> projectMap) {
+    private Map<String, Set<String>> calculateProjectTeams(List<GameAthlete> athletes,
+            Map<Long, GameEventProject> projectMap, Map<Long, String> teamIdToNameMap) {
         Map<String, Set<String>> projectTeams = new HashMap<>();
-        for (GameTeam team : teams) {
-            if (StringUtils.isNotBlank(team.getProjectValue())) {
-                List<Long> projectIds = JSONUtil.toList(team.getProjectValue(), Long.class);
+        for (GameAthlete athlete : athletes) {
+            String teamName = teamIdToNameMap.get(athlete.getTeamId());
+            if (StringUtils.isNotBlank(athlete.getProjectValue()) && StringUtils.isNotBlank(teamName)) {
+                List<Long> projectIds = JSONUtil.toList(athlete.getProjectValue(), Long.class);
                 for (Long projectId : projectIds) {
                     GameEventProject project = projectMap.get(projectId);
-                    if (project != null) {
+                    // 只有团体项目(Classification = '1')才将队伍计入项目参与名单
+                    if (project != null && "1".equals(project.getClassification())) {
                         projectTeams.computeIfAbsent(project.getProjectName(), k -> new HashSet<>())
-                                .add(team.getTeamName());
+                                .add(teamName);
                     }
                 }
             }
@@ -1444,7 +1485,8 @@ public class IEnrollServiceImpl implements IEnrollService {
                 for (Long projectId : projectIds) {
                     GameEventProject project = projectMap.get(projectId);
                     if (project != null) {
-                        Map<String, Map<String, Integer>> teamMap = counts.computeIfAbsent(project.getProjectName(), k -> new HashMap<>());
+                        Map<String, Map<String, Integer>> teamMap = counts.computeIfAbsent(project.getProjectName(),
+                                k -> new HashMap<>());
                         Map<String, Integer> genderMap = teamMap.computeIfAbsent(teamName, k -> new HashMap<>());
                         genderMap.merge(athlete.getGender(), 1, Integer::sum);
                     }
@@ -1466,19 +1508,6 @@ public class IEnrollServiceImpl implements IEnrollService {
 
         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();
@@ -1520,7 +1549,8 @@ public class IEnrollServiceImpl implements IEnrollService {
                             int current = genderCounts.getOrDefault("1", 0);
                             if (current >= project.getLimitMale()) {
                                 errors.add(createError(rowIndex, enroll, "GENDER_LIMIT_EXCEEDED", projectName,
-                                        String.format("项目'%s'男生限报人数:%d,当前报名人数已满", projectName, project.getLimitMale())));
+                                        String.format("项目'%s'男生限报人数:%d,当前报名人数已满", projectName,
+                                                project.getLimitMale())));
                             }
                         }
                     } else if ("2".equals(gender)) { // 女
@@ -1542,7 +1572,8 @@ public class IEnrollServiceImpl implements IEnrollService {
                         if (project.getLimitTeam() != null && project.getLimitTeam() > 0) {
                             if (teams.size() >= project.getLimitTeam()) {
                                 errors.add(createError(rowIndex, enroll, "TEAM_LIMIT_EXCEEDED", projectName,
-                                        String.format("项目'%s'队伍限报数:%d,当前报名队伍数已满", projectName, project.getLimitTeam())));
+                                        String.format("项目'%s'队伍限报数:%d,当前报名队伍数已满", projectName,
+                                                project.getLimitTeam())));
                             }
                         }
                     }
@@ -1635,7 +1666,8 @@ public class IEnrollServiceImpl implements IEnrollService {
         if (!result.isValid()) {
             // 如果校验不通过,抛出第一个错误信息
             if (!result.getErrors().isEmpty()) {
-                throw new org.dromara.common.core.exception.ServiceException(result.getErrors().get(0).getErrorMessage());
+                throw new org.dromara.common.core.exception.ServiceException(
+                        result.getErrors().get(0).getErrorMessage());
             }
         }
     }