From 017264fd0f7afcae7adc5e80f6e4d977806e8118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=85=E6=88=BF?= Date: Sat, 17 Jan 2026 00:03:10 +0800 Subject: [PATCH] refactor: Phase 2 - Extract ParticipantGroupingService from God Class - Create ScheduleGroupData model class in schedule/model/ - Create ParticipantGroupingService in schedule/grouping/ - Modify MartialScheduleArrangeServiceImpl to use ParticipantGroupingService - Remove internal ScheduleGroupData class and grouping methods - All 438 tests passing Related to Issue #11 --- .../buckets/.bloomcycle.bin/xl.meta | Bin 743 -> 743 bytes .../buckets/.usage-cache.bin.bkp/xl.meta | Bin 630 -> 630 bytes .../buckets/.usage-cache.bin/xl.meta | Bin 630 -> 630 bytes .../.minio.sys/buckets/.usage.json/xl.meta | Bin 2360 -> 2360 bytes .../.usage-cache.bin.bkp/xl.meta | Bin 636 -> 636 bytes .../000000-assets/.usage-cache.bin/xl.meta | Bin 636 -> 636 bytes .../assets/.usage-cache.bin.bkp/xl.meta | Bin 575 -> 575 bytes .../buckets/assets/.usage-cache.bin/xl.meta | Bin 575 -> 575 bytes .../MartialScheduleArrangeServiceImpl.java | 397 +----------------- .../grouping/ParticipantGroupingService.java | 252 +++++++++++ .../schedule/model/ScheduleGroupData.java | 42 ++ 11 files changed, 303 insertions(+), 388 deletions(-) create mode 100644 src/main/java/org/springblade/modules/martial/service/schedule/grouping/ParticipantGroupingService.java create mode 100644 src/main/java/org/springblade/modules/martial/service/schedule/model/ScheduleGroupData.java diff --git a/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta b/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta index a660807cce6494bee1d50ea562799597df40566f..5b68c54ab28798c424a8c0f10fa8d445b30eb1f1 100644 GIT binary patch delta 404 zcmaFP`kZxw29M(eDZ|_EkFW%rI5tc)F&5ao=lb6=lDyKKoTH4vd9B7llOw*WPp!Y*kRmnh;)dQu9Rc-ObCjYl$_0bw7j?^v8W_9Me#TfD@aLZR-&rG*2Mkmk}H#QQl0a2 z3vyC{(svjHCa-3)sfVkIn`pS@k1>LK)=1z+B%B-1X{1*E9Km%p{CQ>*g4@Jck)Dj; z?lod7NI-CZ8eaI?4(BGA8nI{vK)I%wSqX(k;tg97(rb;}<{d_GUmDrJUkB$Vsv5a} GDhB{U9g9i; delta 404 zcmaFP`kZxw29Lvwve={7j<5s-w$GYqVl42k`gfSih4E^%j?-7Y9{_M zRbP@?l9+x|A;~<^GRepwDa|Cs!Ynm8(Ja-_z|hjt(9p;-EyctzX|f@sTK&SZbLmN^ z8P1(c(_hZmxFoMMC+8?*@HgEAs{?1xS(#nv+hSjOG~4XU+V6Z{kJ@_d{J{Rc=up4RFs-m!muPSwW8!~-nHe$C5c5PsVT>KSV1Z>v*JPxB2*5nORh}LNp;T8 zEyzg)%HClRfGDU3s))L1=)NZ#t|6M&Q0gWVg6nDM=*NQKwi<@K^F?qEF)Am&M{xfd z&N^2I=f+qXZac9D!7Vjxj|xX{HyWwxPgsQDzcoBwy$sHc)igZ!I1Ir}HJloMVe)rI F8vvo_k6-`* diff --git a/minio_data/.minio.sys/buckets/.usage-cache.bin.bkp/xl.meta b/minio_data/.minio.sys/buckets/.usage-cache.bin.bkp/xl.meta index 5eb67790ca92f512fe5ce9ef8da8d3958f9eaacf..633e6e0f1691752625dfc627d984723fe9cc3e42 100644 GIT binary patch delta 176 zcmeyy@{MJJ29M(eDV}teBP@?DMn9NnVk}U&Px51|p5v5On;m^G{AVtnSYFQtQ8V$6 zje>zmnxT=gnNgytrLl#jp{b#PfmyPpWpb*efnk!VY1-rnMm^nN$F-4+jZ5-Mb8?P6 zuHWQ88xIIuJTDNE(tA2Ni0b{ b&cn(81esZhs)nC~f+v4ttYKLjd+ImUl diff --git a/minio_data/.minio.sys/buckets/.usage-cache.bin/xl.meta b/minio_data/.minio.sys/buckets/.usage-cache.bin/xl.meta index d22f73ae28dde173368190f3680bf3e23b8aa366..a025612fa3526fb368351f44439045917208c4fb 100644 GIT binary patch delta 190 zcmeyy@{MJJ29M(e$p--|j<7hcGq0IwVl1E~ml?AEmeZ0sJ$|dExcu#$SYFQtQ8V$6 z>EsATwU{NTC5h=b6%0($42_Jjc2WhJ%uiqIU$#tlHh$h1GkG(k qhV;W#K8eL8p#>?4C8@`GSQ&sIGb>Tm@N-b`K%uLKpEKDsdjV;a6%*@jgjZF+KjFXI#QzjcSs+lem@%!wCb>23DBumh1L_t}1&8xb}-FfuwcH7+MEba;*!vUl*E$M a<2nq+R0YCJiDQBU{g4B@|wjZ5-Mb8?Qn zoc*zarH%PcgioF?uT|)QDvMhSnXld#akRcQOQe3%UysR~8I7bLuJTDNE(tA2Ni0b{ a&cn(81esZhs)l_EOC~W@urSwcRR#d63qgzk delta 175 zcmeyv@`q)D29Lvw(vzOuM_7XLPamCVVl1G!rBBS-*H3Ua!=%(?bA!ndjC#77t3KE>HZI94&B;0P zQtx5S>iS!Ex~^<=+mO%EuvTR5nTNIoWlD+5KVG@(W!pG;Goz98)m1);#U-HyDTyVi a$9Y&8fFLs~F4WNEM*1YC3KoHkGoJu@%Sjpl diff --git a/minio_data/.minio.sys/buckets/assets/.usage-cache.bin.bkp/xl.meta b/minio_data/.minio.sys/buckets/assets/.usage-cache.bin.bkp/xl.meta index d76c49f8bc2769c40e928131efc40d3cc835ffda..5cebf3169d37183c90a14b2b151d1ed64b739f35 100644 GIT binary patch delta 176 zcmdnbvY%yw29M(eNd?IdM_AlD651!47z^BW@(b8x=M%Ot+N$^H-BT+99eU` z-9biyF_FioP`H9?)x`55nZmNkiN#_!4&R)5{pYh3sMqGQjhbn ZG5|qlR-&rG8}&Pre=<6;ob;&r1OVzTKu!Pv delta 176 zcmdnbvY%yw29Lvw(v*9)M_6KJ9=bQt#8}|c6=y~91ukzFE?wHQ;7X<9#PWJJh?bj8apLl1)v3$kHe+Db2#%#5CE!*xbz2cya`zp04CIxzmh|OY%x{a*nJy z-tHiyz?jJ6Qz%@)wQAz|kW69O?4C8@`G aSQ&sIGb>Tm;Enp7$v+vLSWbFWeF6YZ_C$RE delta 176 zcmdnbvY%yw29LvwQn^QOkFeyK$+Ar}F&6kE@1d+5=i<6F*mv8r1yR-$%j?-7Y9{`% zQLr?!NHsJzO*BYJwoFbnN;Nc0OEfo2HAys2Og2h0vYZ^jsHbc2j^QI??}pziQuwif7YaJoDb=X}s0tgDrbpf6U~~jM~yySNS9smxLCiB$lKe b=V4_4g3PSAP=n4#Mw5RsI getUnlockedCompetitions() { // 查询所有未锁定的赛事(schedule_status != 2) @@ -372,250 +375,18 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang * @param project 项目 * @return 分组类别名称: 男子/女子/混合/团体 */ - private String getProjectCategoryName(MartialProject project) { - if (project == null || project.getCategory() == null) { - return ""; - } - String category = project.getCategory(); - switch (category) { - case "1": return "男子"; - case "2": return "女子"; - case "3": return "混合"; - case "4": return "团体"; - default: return category; // 如果是自定义文字,直接返回 - } - } + /** - * 根据项目的分组类别过滤运动员 - * @param athletes 运动员列表 - * @param project 项目 - * @return 过滤后的运动员列表 + * Auto group participants + * @deprecated Use ParticipantGroupingService.autoGroup() directly */ - private List filterAthletesByProjectCategory(List athletes, MartialProject project) { - if (project == null || project.getCategory() == null) { - return athletes; - } - String category = project.getCategory(); - switch (category) { - case "1": // 男子项目,只保留男性运动员 - return athletes.stream() - .filter(a -> a.getGender() == null || a.getGender() == 1) - .collect(Collectors.toList()); - case "2": // 女子项目,只保留女性运动员 - return athletes.stream() - .filter(a -> a.getGender() != null && a.getGender() == 2) - .collect(Collectors.toList()); - case "3": // 混合项目,所有运动员 - case "4": // 团体项目,所有运动员 - default: - return athletes; - } - } - + @Deprecated private List autoGroupParticipants(List athletes) { - List groups = new ArrayList<>(); - int displayOrder = 1; - - // 先加载所有项目信息(用于获取项目类型和名称) - Set projectIds = athletes.stream() - .map(MartialAthlete::getProjectId) - .filter(id -> id != null) - .collect(Collectors.toSet()); - - Map projectMap = new HashMap<>(); - for (Long projectId : projectIds) { - MartialProject project = projectMapper.selectById(projectId); - if (project != null) { - projectMap.put(projectId, project); - } - } - - // 分离集体和个人项目(根据项目表的type字段: 1=个人, 2=双人, 3=集体) - List teamAthletes = athletes.stream() - .filter(a -> { - MartialProject project = projectMap.get(a.getProjectId()); - return project != null && (project.getType() == 2 || project.getType() == 3); - }) - .collect(Collectors.toList()); - - List individualAthletes = athletes.stream() - .filter(a -> { - MartialProject project = projectMap.get(a.getProjectId()); - return project != null && project.getType() == 1; - }) - .collect(Collectors.toList()); - - // 集体项目分组:按"项目ID_组别"分组 - Map> teamGroupMap = teamAthletes.stream() - .collect(Collectors.groupingBy(a -> - String.valueOf(a.getProjectId()) - )); - - for (Map.Entry> entry : teamGroupMap.entrySet()) { - List members = entry.getValue(); - if (members.isEmpty()) continue; - - MartialAthlete first = members.get(0); - MartialProject project = projectMap.get(first.getProjectId()); - - // 跳过没有项目信息的分组 - if (project == null) { - log.warn("项目不存在, projectId: {}, 跳过该分组", first.getProjectId()); - continue; - } - - // 根据项目的分组类别过滤运动员(男子项目只保留男性,女子项目只保留女性) - members = filterAthletesByProjectCategory(members, project); - if (members.isEmpty()) { - log.info("项目 '{}' 过滤后无符合性别要求的运动员,跳过", project.getProjectName()); - continue; - } - - // 统计队伍数(按单位分组) - long teamCount = members.stream() - .map(MartialAthlete::getTeamName) - .filter(org -> org != null && !org.isEmpty()) - .distinct() - .count(); - - ScheduleGroupData group = new ScheduleGroupData(); - String projectName = project.getProjectName(); - String categoryName = getProjectCategoryName(project); - group.setGroupName(projectName + (categoryName.isEmpty() ? "" : " " + categoryName)); - group.setProjectId(first.getProjectId()); - group.setProjectType((project.getType() == 2 || project.getType() == 3) ? 2 : 1); // type=2(双人)或type=3(集体)映射为projectType=2(集体) - group.setMaxParticipants(project.getMaxParticipants()); // 设置项目单位容纳人数 - group.setCategory(categoryName); // 设置组别 - group.setDisplayOrder(displayOrder++); - group.setTotalParticipants(members.size()); - group.setTotalTeams((int) teamCount); - group.setAthletes(members); - - // 计算预计时长:使用项目的 estimatedDuration - // 如果项目设置了时长,使用 队伍数 × 项目时长 - // 否则使用默认计算: 队伍数 × 5分钟 + 间隔时间 - int duration; - if (project.getEstimatedDuration() != null && project.getEstimatedDuration() > 0) { - duration = (int) teamCount * project.getEstimatedDuration(); - log.debug("集体项目 '{}': 使用项目时长 {}分钟/队, {}队, 总计{}分钟", - projectName, project.getEstimatedDuration(), teamCount, duration); - } else { - duration = (int) teamCount * 5 + Math.max(0, (int) teamCount - 1) * 2; - log.debug("集体项目 '{}': 使用默认时长, {}队, 总计{}分钟", projectName, teamCount, duration); - } - group.setEstimatedDuration(duration); - - groups.add(group); - } - - // 个人项目分组:按"项目ID_组别"分组 - Map> individualGroupMap = individualAthletes.stream() - .collect(Collectors.groupingBy(a -> - String.valueOf(a.getProjectId()) - )); - - for (Map.Entry> entry : individualGroupMap.entrySet()) { - List members = entry.getValue(); - if (members.isEmpty()) continue; - - MartialAthlete first = members.get(0); - MartialProject project = projectMap.get(first.getProjectId()); - - // 跳过没有项目信息的分组 - if (project == null) { - log.warn("项目不存在, projectId: {}, 跳过该分组", first.getProjectId()); - continue; - } - - // 根据项目的分组类别过滤运动员(男子项目只保留男性,女子项目只保留女性) - members = filterAthletesByProjectCategory(members, project); - if (members.isEmpty()) { - log.info("项目 '{}' 过滤后无符合性别要求的运动员,跳过", project.getProjectName()); - continue; - } - - String projectName = project.getProjectName(); - String categoryName = getProjectCategoryName(project); - - // 计算单人时长 - int durationPerPerson = scheduleConfig.getDefaultDurationPerPerson(); - if (project.getEstimatedDuration() != null && project.getEstimatedDuration() > 0) { - durationPerPerson = project.getEstimatedDuration(); - } - - // 自动拆分大组:如果人数过多,拆分成多个小组 - // 优先使用项目配置的单位容纳人数(maxParticipants) - int maxPeoplePerGroup; - Integer projectMax = project.getMaxParticipants(); - if (projectMax != null && projectMax > 0) { - // 使用项目配置的单位容纳人数 - maxPeoplePerGroup = projectMax; - log.debug("项目 '{}' 使用自定义容纳人数: {}", projectName, projectMax); - } else if (members.size() <= scheduleConfig.getMaxPeoplePerGroup()) { - // 35人以内不拆分 - maxPeoplePerGroup = members.size(); - } else { - // 超过35人,按每组30-35人拆分 - maxPeoplePerGroup = scheduleConfig.getTargetPeoplePerGroup(); - } - - if (members.size() <= maxPeoplePerGroup) { - // 人数不多,不需要拆分 - ScheduleGroupData group = new ScheduleGroupData(); - group.setGroupName(projectName + " " + categoryName); - group.setProjectId(first.getProjectId()); - group.setProjectType(1); - group.setMaxParticipants(maxPeoplePerGroup); // 设置项目单位容纳人数 - group.setDisplayOrder(displayOrder++); - group.setCategory(categoryName); // 设置组别 - group.setTotalParticipants(members.size()); - group.setAthletes(members); - - int duration = members.size() * durationPerPerson; - group.setEstimatedDuration(duration); - - log.debug("个人项目 '{}': {}人, {}分钟/人, 总计{}分钟", - projectName + " " + categoryName, members.size(), durationPerPerson, duration); - - groups.add(group); - } else { - // 人数太多,需要拆分成多个小组 - int totalPeople = members.size(); - int numSubGroups = (int) Math.ceil((double) totalPeople / maxPeoplePerGroup); - - log.info("个人项目 '{}' 有{}人,将拆分成{}个小组(每组最多{}人)", - projectName + " " + categoryName, totalPeople, numSubGroups, maxPeoplePerGroup); - - for (int i = 0; i < numSubGroups; i++) { - int startIdx = i * maxPeoplePerGroup; - int endIdx = Math.min(startIdx + maxPeoplePerGroup, totalPeople); - List subGroupMembers = members.subList(startIdx, endIdx); - - ScheduleGroupData group = new ScheduleGroupData(); - group.setGroupName(projectName + " " + categoryName + " 第" + (i + 1) + "组"); - group.setProjectId(first.getProjectId()); - group.setProjectType(1); - group.setMaxParticipants(maxPeoplePerGroup); // 设置项目单位容纳人数 - group.setDisplayOrder(displayOrder++); - group.setCategory(categoryName); // 设置组别 - group.setTotalParticipants(subGroupMembers.size()); - group.setAthletes(new ArrayList<>(subGroupMembers)); - - int duration = subGroupMembers.size() * durationPerPerson; - group.setEstimatedDuration(duration); - - log.debug(" 拆分子组 '第{}组': {}人, 总计{}分钟", - i + 1, subGroupMembers.size(), duration); - - groups.add(group); - } - } - } - - return groups; + return participantGroupingService.autoGroup(athletes); } + /** * 验证时间窗口容量是否足够容纳所有分组 */ @@ -918,154 +689,4 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang } } - private static class ScheduleGroupData { - private String groupName; - private Long projectId; - private Integer projectType; - private Integer displayOrder; - private Integer totalParticipants; - private Integer totalTeams; - private Integer estimatedDuration; - private Integer maxParticipants; // 项目单位容纳人数 - private String category; // 组别(男子/女子/混合) - private List athletes; - - // 分配结果 - private Long assignedVenueId; - private String assignedVenueName; - private LocalDate assignedDate; - private String assignedTimePeriod; - private String assignedTimeSlot; - private Integer assignedTimeSlotIndex; // 时间段索引 - - // Getters and Setters - public String getGroupName() { - return groupName; - } - - public void setGroupName(String groupName) { - this.groupName = groupName; - } - - public Long getProjectId() { - return projectId; - } - - public void setProjectId(Long projectId) { - this.projectId = projectId; - } - - public Integer getProjectType() { - return projectType; - } - - public void setProjectType(Integer projectType) { - this.projectType = projectType; - } - - public Integer getDisplayOrder() { - return displayOrder; - } - - public void setDisplayOrder(Integer displayOrder) { - this.displayOrder = displayOrder; - } - - public Integer getTotalParticipants() { - return totalParticipants; - } - - public void setTotalParticipants(Integer totalParticipants) { - this.totalParticipants = totalParticipants; - } - - public Integer getTotalTeams() { - return totalTeams; - } - - public void setTotalTeams(Integer totalTeams) { - this.totalTeams = totalTeams; - } - - public Integer getEstimatedDuration() { - return estimatedDuration; - } - - public void setEstimatedDuration(Integer estimatedDuration) { - this.estimatedDuration = estimatedDuration; - } - - public Integer getMaxParticipants() { - return maxParticipants; - } - - public void setMaxParticipants(Integer maxParticipants) { - this.maxParticipants = maxParticipants; - } - - public String getCategory() { - return category; - } - - public void setCategory(String category) { - this.category = category; - } - - public List getAthletes() { - return athletes; - } - - public void setAthletes(List athletes) { - this.athletes = athletes; - } - - public Long getAssignedVenueId() { - return assignedVenueId; - } - - public void setAssignedVenueId(Long assignedVenueId) { - this.assignedVenueId = assignedVenueId; - } - - public String getAssignedVenueName() { - return assignedVenueName; - } - - public void setAssignedVenueName(String assignedVenueName) { - this.assignedVenueName = assignedVenueName; - } - - public LocalDate getAssignedDate() { - return assignedDate; - } - - public void setAssignedDate(LocalDate assignedDate) { - this.assignedDate = assignedDate; - } - - public String getAssignedTimePeriod() { - return assignedTimePeriod; - } - - public void setAssignedTimePeriod(String assignedTimePeriod) { - this.assignedTimePeriod = assignedTimePeriod; - } - - public String getAssignedTimeSlot() { - return assignedTimeSlot; - } - - public void setAssignedTimeSlot(String assignedTimeSlot) { - this.assignedTimeSlot = assignedTimeSlot; - } - - public Integer getAssignedTimeSlotIndex() { - return assignedTimeSlotIndex; - } - - public void setAssignedTimeSlotIndex(Integer assignedTimeSlotIndex) { - this.assignedTimeSlotIndex = assignedTimeSlotIndex; - } - } - } diff --git a/src/main/java/org/springblade/modules/martial/service/schedule/grouping/ParticipantGroupingService.java b/src/main/java/org/springblade/modules/martial/service/schedule/grouping/ParticipantGroupingService.java new file mode 100644 index 0000000..cfa8748 --- /dev/null +++ b/src/main/java/org/springblade/modules/martial/service/schedule/grouping/ParticipantGroupingService.java @@ -0,0 +1,252 @@ +package org.springblade.modules.martial.service.schedule.grouping; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.modules.martial.config.ScheduleConfig; +import org.springblade.modules.martial.mapper.MartialProjectMapper; +import org.springblade.modules.martial.pojo.entity.MartialAthlete; +import org.springblade.modules.martial.pojo.entity.MartialProject; +import org.springblade.modules.martial.service.schedule.model.ScheduleGroupData; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Participant grouping service + * Groups athletes by project and category for schedule arrangement + * + * @author Refactored from MartialScheduleArrangeServiceImpl + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ParticipantGroupingService { + + private final MartialProjectMapper projectMapper; + private final ScheduleConfig scheduleConfig; + + /** + * Auto group participants by project and category + */ + public List autoGroup(List athletes) { + List groups = new ArrayList<>(); + int displayOrder = 1; + + Map projectMap = loadProjectMap(athletes); + List teamAthletes = filterTeamAthletes(athletes, projectMap); + List individualAthletes = filterIndividualAthletes(athletes, projectMap); + + displayOrder = groupTeamAthletes(teamAthletes, projectMap, groups, displayOrder); + groupIndividualAthletes(individualAthletes, projectMap, groups, displayOrder); + + return groups; + } + + public String getProjectCategoryName(MartialProject project) { + if (project == null || project.getCategory() == null) { + return ""; + } + switch (project.getCategory()) { + case "1": return "男子"; + case "2": return "女子"; + case "3": return "混合"; + case "4": return "团体"; + default: return project.getCategory(); + } + } + + public List filterByCategory(List athletes, MartialProject project) { + if (project == null || project.getCategory() == null) { + return athletes; + } + switch (project.getCategory()) { + case "1": + return athletes.stream() + .filter(a -> a.getGender() == null || a.getGender() == 1) + .collect(Collectors.toList()); + case "2": + return athletes.stream() + .filter(a -> a.getGender() != null && a.getGender() == 2) + .collect(Collectors.toList()); + default: + return athletes; + } + } + + private Map loadProjectMap(List athletes) { + Set projectIds = athletes.stream() + .map(MartialAthlete::getProjectId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + Map projectMap = new HashMap<>(); + for (Long projectId : projectIds) { + MartialProject project = projectMapper.selectById(projectId); + if (project != null) { + projectMap.put(projectId, project); + } + } + return projectMap; + } + + private List filterTeamAthletes(List athletes, Map projectMap) { + return athletes.stream() + .filter(a -> { + MartialProject project = projectMap.get(a.getProjectId()); + return project != null && (project.getType() == 2 || project.getType() == 3); + }) + .collect(Collectors.toList()); + } + + private List filterIndividualAthletes(List athletes, Map projectMap) { + return athletes.stream() + .filter(a -> { + MartialProject project = projectMap.get(a.getProjectId()); + return project != null && project.getType() == 1; + }) + .collect(Collectors.toList()); + } + + private int groupTeamAthletes(List teamAthletes, Map projectMap, + List groups, int displayOrder) { + Map> teamGroupMap = teamAthletes.stream() + .collect(Collectors.groupingBy(a -> String.valueOf(a.getProjectId()))); + + for (Map.Entry> entry : teamGroupMap.entrySet()) { + List members = entry.getValue(); + if (members.isEmpty()) continue; + + MartialAthlete first = members.get(0); + MartialProject project = projectMap.get(first.getProjectId()); + if (project == null) { + log.warn("Project not found, projectId: {}, skipping", first.getProjectId()); + continue; + } + + members = filterByCategory(members, project); + if (members.isEmpty()) { + log.info("Project '{}' has no athletes after gender filter, skipping", project.getProjectName()); + continue; + } + + long teamCount = members.stream() + .map(MartialAthlete::getTeamName) + .filter(org -> org != null && !org.isEmpty()) + .distinct() + .count(); + + String projectName = project.getProjectName(); + String categoryName = getProjectCategoryName(project); + + ScheduleGroupData group = ScheduleGroupData.builder() + .groupName(projectName + (categoryName.isEmpty() ? "" : " " + categoryName)) + .projectId(first.getProjectId()) + .projectType((project.getType() == 2 || project.getType() == 3) ? 2 : 1) + .maxParticipants(project.getMaxParticipants()) + .category(categoryName) + .displayOrder(displayOrder++) + .totalParticipants(members.size()) + .totalTeams((int) teamCount) + .athletes(members) + .build(); + + int duration; + if (project.getEstimatedDuration() != null && project.getEstimatedDuration() > 0) { + duration = (int) teamCount * project.getEstimatedDuration(); + } else { + duration = (int) teamCount * 5 + Math.max(0, (int) teamCount - 1) * 2; + } + group.setEstimatedDuration(duration); + groups.add(group); + } + return displayOrder; + } + + private void groupIndividualAthletes(List individualAthletes, Map projectMap, + List groups, int displayOrder) { + Map> individualGroupMap = individualAthletes.stream() + .collect(Collectors.groupingBy(a -> String.valueOf(a.getProjectId()))); + + for (Map.Entry> entry : individualGroupMap.entrySet()) { + List members = entry.getValue(); + if (members.isEmpty()) continue; + + MartialAthlete first = members.get(0); + MartialProject project = projectMap.get(first.getProjectId()); + if (project == null) { + log.warn("Project not found, projectId: {}, skipping", first.getProjectId()); + continue; + } + + members = filterByCategory(members, project); + if (members.isEmpty()) continue; + + String projectName = project.getProjectName(); + String categoryName = getProjectCategoryName(project); + int durationPerPerson = (project.getEstimatedDuration() != null && project.getEstimatedDuration() > 0) + ? project.getEstimatedDuration() : scheduleConfig.getDefaultDurationPerPerson(); + + int maxPeoplePerGroup = calculateMaxPeoplePerGroup(project, members.size()); + + if (members.size() <= maxPeoplePerGroup) { + ScheduleGroupData group = ScheduleGroupData.builder() + .groupName(projectName + " " + categoryName) + .projectId(first.getProjectId()) + .projectType(1) + .maxParticipants(maxPeoplePerGroup) + .displayOrder(displayOrder++) + .category(categoryName) + .totalParticipants(members.size()) + .athletes(members) + .estimatedDuration(members.size() * durationPerPerson) + .build(); + groups.add(group); + } else { + displayOrder = splitIntoSubGroups(members, project, projectName, categoryName, + maxPeoplePerGroup, durationPerPerson, groups, displayOrder); + } + } + } + + private int calculateMaxPeoplePerGroup(MartialProject project, int memberCount) { + Integer projectMax = project.getMaxParticipants(); + if (projectMax != null && projectMax > 0) { + return projectMax; + } else if (memberCount <= scheduleConfig.getMaxPeoplePerGroup()) { + return memberCount; + } else { + return scheduleConfig.getTargetPeoplePerGroup(); + } + } + + private int splitIntoSubGroups(List members, MartialProject project, + String projectName, String categoryName, int maxPeoplePerGroup, + int durationPerPerson, List groups, int displayOrder) { + int totalPeople = members.size(); + int numSubGroups = (int) Math.ceil((double) totalPeople / maxPeoplePerGroup); + + log.info("Individual project '{}' has {} people, splitting into {} groups (max {} per group)", + projectName + " " + categoryName, totalPeople, numSubGroups, maxPeoplePerGroup); + + for (int i = 0; i < numSubGroups; i++) { + int startIdx = i * maxPeoplePerGroup; + int endIdx = Math.min(startIdx + maxPeoplePerGroup, totalPeople); + List subGroupMembers = new ArrayList<>(members.subList(startIdx, endIdx)); + + ScheduleGroupData group = ScheduleGroupData.builder() + .groupName(projectName + " " + categoryName + " 第" + (i + 1) + "组") + .projectId(project.getId()) + .projectType(1) + .maxParticipants(maxPeoplePerGroup) + .displayOrder(displayOrder++) + .category(categoryName) + .totalParticipants(subGroupMembers.size()) + .athletes(subGroupMembers) + .estimatedDuration(subGroupMembers.size() * durationPerPerson) + .build(); + groups.add(group); + } + return displayOrder; + } +} diff --git a/src/main/java/org/springblade/modules/martial/service/schedule/model/ScheduleGroupData.java b/src/main/java/org/springblade/modules/martial/service/schedule/model/ScheduleGroupData.java new file mode 100644 index 0000000..3e9c078 --- /dev/null +++ b/src/main/java/org/springblade/modules/martial/service/schedule/model/ScheduleGroupData.java @@ -0,0 +1,42 @@ +package org.springblade.modules.martial.service.schedule.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Builder; +import org.springblade.modules.martial.pojo.entity.MartialAthlete; + +import java.time.LocalDate; +import java.util.List; + +/** + * Schedule group data model + * Represents a group of participants for schedule arrangement + * + * @author Refactored from MartialScheduleArrangeServiceImpl + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ScheduleGroupData { + + private String groupName; + private Long projectId; + private Integer projectType; + private Integer displayOrder; + private Integer totalParticipants; + private Integer totalTeams; + private Integer estimatedDuration; + private Integer maxParticipants; + private String category; + private List athletes; + + // Assignment results + private Long assignedVenueId; + private String assignedVenueName; + private LocalDate assignedDate; + private String assignedTimePeriod; + private String assignedTimeSlot; + private Integer assignedTimeSlotIndex; +}