From 82ae96503dd143a759fe0845c050f1117dcdb882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AE=85=E6=88=BF?= Date: Fri, 9 Jan 2026 17:42:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E7=BC=96=E6=8E=92?= =?UTF-8?q?=E5=88=86=E7=BB=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 根据项目分组类别(男子/女子/混合)过滤运动员 2. 使用项目的category字段显示分组名称 3. 按team_name统计队伍数量(集体项目) 4. 添加场地容量约束和负载均衡分配 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../MartialScheduleArrangeServiceImpl.java | 263 ++++++++++++------ 1 file changed, 184 insertions(+), 79 deletions(-) diff --git a/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleArrangeServiceImpl.java b/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleArrangeServiceImpl.java index 672dc13..a2daa3c 100644 --- a/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleArrangeServiceImpl.java +++ b/src/main/java/org/springblade/modules/martial/service/impl/MartialScheduleArrangeServiceImpl.java @@ -30,6 +30,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.Period; import java.util.*; import java.util.stream.Collectors; @@ -355,6 +356,53 @@ 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 过滤后的运动员列表 + */ + 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; + } + } + private List autoGroupParticipants(List athletes) { List groups = new ArrayList<>(); int displayOrder = 1; @@ -391,7 +439,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang // 集体项目分组:按"项目ID_组别"分组 Map> teamGroupMap = teamAthletes.stream() .collect(Collectors.groupingBy(a -> - a.getProjectId() + "_" + Func.toStr(a.getCategory(), "未分组") + String.valueOf(a.getProjectId()) )); for (Map.Entry> entry : teamGroupMap.entrySet()) { @@ -401,24 +449,33 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang MartialAthlete first = members.get(0); MartialProject project = projectMap.get(first.getProjectId()); - // 统计队伍数(按单位分组) - long teamCount = members.stream() - .map(MartialAthlete::getOrganization) - .filter(org -> org != null && !org.isEmpty()) - .distinct() - .count(); - // 跳过没有项目信息的分组 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(); - group.setGroupName(projectName + " " + (first.getCategory() != null ? first.getCategory() : "未分组")); + 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.setDisplayOrder(displayOrder++); group.setTotalParticipants(members.size()); group.setTotalTeams((int) teamCount); @@ -444,7 +501,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang // 个人项目分组:按"项目ID_组别"分组 Map> individualGroupMap = individualAthletes.stream() .collect(Collectors.groupingBy(a -> - a.getProjectId() + "_" + Func.toStr(a.getCategory(), "未分组") + String.valueOf(a.getProjectId()) )); for (Map.Entry> entry : individualGroupMap.entrySet()) { @@ -460,8 +517,15 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang continue; } + // 根据项目的分组类别过滤运动员(男子项目只保留男性,女子项目只保留女性) + members = filterAthletesByProjectCategory(members, project); + if (members.isEmpty()) { + log.info("项目 '{}' 过滤后无符合性别要求的运动员,跳过", project.getProjectName()); + continue; + } + String projectName = project.getProjectName(); - String categoryName = first.getCategory() != null ? first.getCategory() : "未分组"; + String categoryName = getProjectCategoryName(project); // 计算单人时长 int durationPerPerson = scheduleConfig.getDefaultDurationPerPerson(); @@ -491,6 +555,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang group.setGroupName(projectName + " " + categoryName); group.setProjectId(first.getProjectId()); group.setProjectType(1); + group.setMaxParticipants(maxPeoplePerGroup); // 设置项目单位容纳人数 group.setDisplayOrder(displayOrder++); group.setTotalParticipants(members.size()); group.setAthletes(members); @@ -519,6 +584,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang group.setGroupName(projectName + " " + categoryName + " 第" + (i + 1) + "组"); group.setProjectId(first.getProjectId()); group.setProjectType(1); + group.setMaxParticipants(maxPeoplePerGroup); // 设置项目单位容纳人数 group.setDisplayOrder(displayOrder++); group.setTotalParticipants(subGroupMembers.size()); group.setAthletes(new ArrayList<>(subGroupMembers)); @@ -580,26 +646,36 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang log.info("=== 开始分配场地和时间段 ==="); log.info("场地数量: {}, 时间段数量: {}, 分组数量: {}", venues.size(), timeSlots.size(), groups.size()); - // 为每个场地创建时间轴(记录当前已用时间) - // key = venueId + "_" + timeSlotIndex, value = 当前已用分钟数 - Map venueTimeUsed = new HashMap<>(); - Map venueTimeSlotMap = new HashMap<>(); - - int timeSlotIndex = 0; - for (TimeSlot timeSlot : timeSlots) { - for (MartialVenue venue : venues) { - String key = venue.getId() + "_" + timeSlotIndex; - venueTimeUsed.put(key, 0); - venueTimeSlotMap.put(key, timeSlot); - } - timeSlotIndex++; + // 打印场地容量信息 + for (MartialVenue venue : venues) { + log.info("场地: {}, 容纳人数: {}", venue.getVenueName(), venue.getCapacity()); } - // 按项目ID分组 + // 每个场地的已用时间(跨所有时段累计) + Map venueTotalUsed = new LinkedHashMap<>(); + for (MartialVenue venue : venues) { + venueTotalUsed.put(venue.getId(), 0); + } + + // 每个时段每个场地的已用时间 + Map> slotVenueUsed = new LinkedHashMap<>(); + Map slotMap = new LinkedHashMap<>(); + + for (TimeSlot ts : timeSlots) { + String key = ts.getDate() + "_" + ts.getPeriod(); + if (!slotVenueUsed.containsKey(key)) { + slotVenueUsed.put(key, new LinkedHashMap<>()); + slotMap.put(key, ts); + for (MartialVenue venue : venues) { + slotVenueUsed.get(key).put(venue.getId(), 0); + } + } + } + + // 按项目ID分组并计算总时长 Map> groupsByProject = groups.stream() .collect(Collectors.groupingBy(ScheduleGroupData::getProjectId)); - // 计算每个项目的总时长 Map projectTotalDuration = new HashMap<>(); groupsByProject.forEach((projectId, projectGroups) -> { int total = projectGroups.stream().mapToInt(g -> g.getEstimatedDuration() != null ? g.getEstimatedDuration() : 0).sum(); @@ -624,92 +700,112 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang .collect(Collectors.joining(", "))); int assignedCount = 0; - Map projectVenueSlotMap = new HashMap<>(); // 记录项目分配的场地+时段 for (Long projectId : sortedProjectIds) { List projectGroups = groupsByProject.get(projectId); int projectDuration = projectTotalDuration.get(projectId); String projectName = projectGroups.get(0).getGroupName().split(" ")[0]; - log.info("开始分配项目 '{}': {}个分组, 总时长{}分钟", projectName, projectGroups.size(), projectDuration); - - // 找到能容纳该项目的最佳场地+时段(优先选择已用时间最少的) - String bestKey = null; - int minUsed = Integer.MAX_VALUE; + // 获取项目的单位容纳人数(每组最大人数) + int projectMaxParticipants = projectGroups.get(0).getMaxParticipants() != null + ? projectGroups.get(0).getMaxParticipants() : 1; - for (Map.Entry entry : venueTimeUsed.entrySet()) { - String key = entry.getKey(); - int used = entry.getValue(); - TimeSlot ts = venueTimeSlotMap.get(key); - - // 检查剩余容量是否足够 - if (used + projectDuration <= ts.getCapacity()) { - if (used < minUsed) { - minUsed = used; - bestKey = key; - } - } + log.info("开始分配项目 '{}': {}个分组, 总时长{}分钟, 单位容纳人数={}", + projectName, projectGroups.size(), projectDuration, projectMaxParticipants); + + // 第一步:筛选出容量足够的场地 + List eligibleVenues = venues.stream() + .filter(v -> { + Integer capacity = v.getCapacity(); + // 如果场地没有设置容量,默认允许 + if (capacity == null || capacity <= 0) return true; + // 场地容量必须 >= 项目单位容纳人数 + return capacity >= projectMaxParticipants; + }) + .collect(Collectors.toList()); + + if (eligibleVenues.isEmpty()) { + log.warn("项目 '{}' 需要容纳{}人的场地,但没有符合条件的场地!使用第一个场地", + projectName, projectMaxParticipants); + eligibleVenues = venues; + } else { + log.info("项目 '{}' 可用场地: {}", projectName, + eligibleVenues.stream().map(MartialVenue::getVenueName).collect(Collectors.joining(", "))); } - // 如果没有单个时段能容纳,选择剩余容量最大的 - if (bestKey == null) { + // 第二步:在符合条件的场地中,选择总负载最小的 + Long bestVenueId = null; + int minTotalUsed = Integer.MAX_VALUE; + for (MartialVenue venue : eligibleVenues) { + int used = venueTotalUsed.get(venue.getId()); + if (used < minTotalUsed) { + minTotalUsed = used; + bestVenueId = venue.getId(); + } + } + + // 第三步:在选定的场地中,找第一个能容纳该项目的时段 + String bestSlotKey = null; + for (String slotKey : slotVenueUsed.keySet()) { + TimeSlot ts = slotMap.get(slotKey); + int used = slotVenueUsed.get(slotKey).get(bestVenueId); + int remain = ts.getCapacity() - used; + + if (remain >= projectDuration) { + bestSlotKey = slotKey; + break; + } + } + + // 如果没有单个时段能容纳,选择剩余容量最大的时段 + if (bestSlotKey == null) { int maxRemain = -1; - for (Map.Entry entry : venueTimeUsed.entrySet()) { - String key = entry.getKey(); - int used = entry.getValue(); - TimeSlot ts = venueTimeSlotMap.get(key); + for (String slotKey : slotVenueUsed.keySet()) { + TimeSlot ts = slotMap.get(slotKey); + int used = slotVenueUsed.get(slotKey).get(bestVenueId); int remain = ts.getCapacity() - used; if (remain > maxRemain) { maxRemain = remain; - bestKey = key; + bestSlotKey = slotKey; } } - log.warn("项目 '{}' 总时长{}分钟可能需要跨时段", projectName, projectDuration); + log.warn("项目 '{}' 总时长{}分钟,在场地上没有单个时段能完全容纳", projectName, projectDuration); } - // 解析场地ID和时段索引 - String[] parts = bestKey.split("_"); - Long venueId = Long.parseLong(parts[0]); - int slotIndex = Integer.parseInt(parts[1]); - TimeSlot timeSlot = venueTimeSlotMap.get(bestKey); + TimeSlot targetSlot = slotMap.get(bestSlotKey); + int slotIndex = timeSlots.indexOf(targetSlot); + final Long venueId = bestVenueId; String venueName = venues.stream().filter(v -> v.getId().equals(venueId)).findFirst().map(MartialVenue::getVenueName).orElse("未知场地"); - log.info("项目 '{}' 分配到: 场地={}, 时段={}, 当前已用={}分钟", projectName, venueName, timeSlot.getStartTime(), venueTimeUsed.get(bestKey)); + log.info("项目 '{}' 分配到: 场地={}, 日期={}, 时段={}", + projectName, venueName, targetSlot.getDate(), targetSlot.getStartTime()); - // 分配该项目的所有分组到同一场地+时段 + // 分配该项目的所有分组到同一场地 for (ScheduleGroupData group : projectGroups) { group.setAssignedVenueId(venueId); group.setAssignedVenueName(venueName); - group.setAssignedDate(timeSlot.getDate()); - group.setAssignedTimeSlot(timeSlot.getStartTime()); + group.setAssignedDate(targetSlot.getDate()); + group.setAssignedTimeSlot(targetSlot.getStartTime()); group.setAssignedTimeSlotIndex(slotIndex); - group.setAssignedTimePeriod(timeSlot.getPeriod()); + group.setAssignedTimePeriod(targetSlot.getPeriod()); assignedCount++; - - log.info(" 分组 '{}' 时长={}分钟", group.getGroupName(), group.getEstimatedDuration()); } // 更新已用时间 - venueTimeUsed.put(bestKey, venueTimeUsed.get(bestKey) + projectDuration); - projectVenueSlotMap.put(projectId, bestKey); + slotVenueUsed.get(bestSlotKey).put(venueId, slotVenueUsed.get(bestSlotKey).get(venueId) + projectDuration); + venueTotalUsed.put(venueId, venueTotalUsed.get(venueId) + projectDuration); - log.info(" 场地 {} 时段 {} 已用时间更新为 {} 分钟", venueName, timeSlot.getStartTime(), venueTimeUsed.get(bestKey)); + log.info(" 场地 {} 总负载更新为 {} 分钟", venueName, venueTotalUsed.get(venueId)); } log.info("=== 分配完成: {}/{} 个分组成功分配 ===", assignedCount, groups.size()); - // 输出各场地时段的使用情况 - log.info("=== 各场地时段使用统计 ==="); - for (Map.Entry entry : venueTimeUsed.entrySet()) { - if (entry.getValue() > 0) { - String[] parts = entry.getKey().split("_"); - Long venueId = Long.parseLong(parts[0]); - String venueName = venues.stream().filter(v -> v.getId().equals(venueId)).findFirst().map(MartialVenue::getVenueName).orElse("未知"); - TimeSlot ts = venueTimeSlotMap.get(entry.getKey()); - log.info("场地={}, 日期={}, 时段={}, 已用={}/{}分钟 ({}%)", - venueName, ts.getDate(), ts.getStartTime(), entry.getValue(), ts.getCapacity(), - (int)(entry.getValue() * 100.0 / ts.getCapacity())); - } + // 输出使用统计 + log.info("=== 各场地总负载 ==="); + for (Map.Entry entry : venueTotalUsed.entrySet()) { + final Long vid = entry.getKey(); + String vname = venues.stream().filter(v -> v.getId().equals(vid)).findFirst().map(MartialVenue::getVenueName).orElse("未知"); + log.info("场地={}, 总负载={}分钟", vname, entry.getValue()); } } @@ -858,6 +954,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang private Integer totalParticipants; private Integer totalTeams; private Integer estimatedDuration; + private Integer maxParticipants; // 项目单位容纳人数 private List athletes; // 分配结果 @@ -925,6 +1022,14 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang this.estimatedDuration = estimatedDuration; } + public Integer getMaxParticipants() { + return maxParticipants; + } + + public void setMaxParticipants(Integer maxParticipants) { + this.maxParticipants = maxParticipants; + } + public List getAthletes() { return athletes; }