diff --git a/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta b/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta index 145f5c9..013bd58 100644 Binary files a/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta and b/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta differ 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 8fe8b70..b38fe2c 100644 Binary files a/minio_data/.minio.sys/buckets/.usage-cache.bin.bkp/xl.meta and b/minio_data/.minio.sys/buckets/.usage-cache.bin.bkp/xl.meta differ 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 d0e02b4..8efdcc1 100644 Binary files a/minio_data/.minio.sys/buckets/.usage-cache.bin/xl.meta and b/minio_data/.minio.sys/buckets/.usage-cache.bin/xl.meta differ diff --git a/minio_data/.minio.sys/buckets/.usage.json/xl.meta b/minio_data/.minio.sys/buckets/.usage.json/xl.meta index f009352..31727de 100644 Binary files a/minio_data/.minio.sys/buckets/.usage.json/xl.meta and b/minio_data/.minio.sys/buckets/.usage.json/xl.meta differ diff --git a/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin.bkp/xl.meta b/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin.bkp/xl.meta index a9611f0..d6d9816 100644 Binary files a/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin.bkp/xl.meta and b/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin.bkp/xl.meta differ diff --git a/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin/xl.meta b/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin/xl.meta index a77b66c..9df63ca 100644 Binary files a/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin/xl.meta and b/minio_data/.minio.sys/buckets/000000-assets/.usage-cache.bin/xl.meta differ 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 9463a1b..e974690 100644 Binary files a/minio_data/.minio.sys/buckets/assets/.usage-cache.bin.bkp/xl.meta and b/minio_data/.minio.sys/buckets/assets/.usage-cache.bin.bkp/xl.meta differ diff --git a/minio_data/.minio.sys/buckets/assets/.usage-cache.bin/xl.meta b/minio_data/.minio.sys/buckets/assets/.usage-cache.bin/xl.meta index 983c982..5177857 100644 Binary files a/minio_data/.minio.sys/buckets/assets/.usage-cache.bin/xl.meta and b/minio_data/.minio.sys/buckets/assets/.usage-cache.bin/xl.meta differ 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 40c82d3..d3c6634 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 @@ -589,117 +589,137 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang log.info("总共初始化了 {} 个场地×时间段组合", slots.size()); - // 排序策略: 集体项目(projectType=2)优先,同类型按预计时长降序 - groups.sort((a, b) -> { - int aType = a.getProjectType() != null ? a.getProjectType() : 1; - int bType = b.getProjectType() != null ? b.getProjectType() : 1; - if (aType != bType) { - return bType - aType; - } - int aDuration = a.getEstimatedDuration() != null ? a.getEstimatedDuration() : 0; - int bDuration = b.getEstimatedDuration() != null ? b.getEstimatedDuration() : 0; - return bDuration - aDuration; + // 按项目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(); + projectTotalDuration.put(projectId, total); }); - log.info("排序后分组顺序(集体优先): {}", - groups.stream().map(g -> g.getGroupName() + "(type=" + g.getProjectType() + ")").collect(java.util.stream.Collectors.joining(", "))); + // 排序策略: 集体项目优先,然后按项目总时长降序(大项目优先安排) + List sortedProjectIds = new ArrayList<>(groupsByProject.keySet()); + sortedProjectIds.sort((a, b) -> { + List groupsA = groupsByProject.get(a); + List groupsB = groupsByProject.get(b); + int typeA = groupsA.get(0).getProjectType() != null ? groupsA.get(0).getProjectType() : 1; + int typeB = groupsB.get(0).getProjectType() != null ? groupsB.get(0).getProjectType() : 1; + if (typeA != typeB) { + return typeB - typeA; // 集体项目优先 + } + return projectTotalDuration.get(b) - projectTotalDuration.get(a); // 时长长的优先 + }); + + log.info("项目排序(集体优先,时长降序): {}", sortedProjectIds.stream() + .map(id -> groupsByProject.get(id).get(0).getGroupName().split(" ")[0] + "(" + projectTotalDuration.get(id) + "分钟)") + .collect(Collectors.joining(", "))); - // 记录每个项目已分配的场地ID,用于同一项目优先分配到同一场地 - Map projectVenueMap = new HashMap<>(); int assignedCount = 0; + Map projectVenueMap = new HashMap<>(); // 记录项目分配的场地 + Map projectSlotMap = new HashMap<>(); // 记录项目分配的时间段 - for (ScheduleGroupData group : groups) { - Long projectId = group.getProjectId(); - Long preferredVenueId = projectVenueMap.get(projectId); + // 按项目分配(同一项目的所有分组尽量在同一场地同一时间段) + for (Long projectId : sortedProjectIds) { + List projectGroups = groupsByProject.get(projectId); + int projectDuration = projectTotalDuration.get(projectId); + String projectName = projectGroups.get(0).getGroupName().split(" ")[0]; - SlotInfo bestSlot = null; - - // 优先策略: 如果该项目已有分配的场地,优先选择同一场地的其他时间段 - if (preferredVenueId != null) { - bestSlot = slots.stream() - .filter(s -> s.venueId.equals(preferredVenueId)) - .filter(s -> s.currentLoad + group.getEstimatedDuration() <= s.capacity) - .min((a, b) -> Integer.compare(a.currentLoad, b.currentLoad)) - .orElse(null); + log.info("开始分配项目 '{}': {}个分组, 总时长{}分钟", projectName, projectGroups.size(), projectDuration); + + // 找到能容纳整个项目的最佳槽位(优先选择负载最小的) + SlotInfo bestSlot = slots.stream() + .filter(s -> s.currentLoad + projectDuration <= s.capacity) + .min((a, b) -> { + // 优先选择负载最小的槽位 + return Integer.compare(a.currentLoad, b.currentLoad); + }) + .orElse(null); + + if (bestSlot != null) { + // 找到能容纳整个项目的槽位,所有分组都分配到这个槽位 + log.info("项目 '{}' 整体分配到: 场地={}, 时段={}, 剩余容量={}分钟", + projectName, bestSlot.venueName, bestSlot.timeSlot, bestSlot.capacity - bestSlot.currentLoad); - if (bestSlot != null) { - log.info("项目 '{}' 的分组 '{}' 优先分配到同一场地: {}", - group.getGroupName(), group.getGroupName(), bestSlot.venueName); - } - } - - // 如果优先场地没有足够容量,则按负载均衡策略选择 - if (bestSlot == null) { - Map> slotsByVenue = slots.stream() - .collect(Collectors.groupingBy(s -> s.venueId)); - - bestSlot = slots.stream() - .filter(s -> s.currentLoad + group.getEstimatedDuration() <= s.capacity) - .min((a, b) -> { - int loadA = slotsByVenue.get(a.venueId).stream().mapToInt(s -> s.currentLoad).sum(); - int loadB = slotsByVenue.get(b.venueId).stream().mapToInt(s -> s.currentLoad).sum(); - if (loadA != loadB) { - return Integer.compare(loadA, loadB); - } - return Integer.compare(a.currentLoad, b.currentLoad); - }) - .orElse(null); - } - - // 如果没有找到容量足够的槽位,选择负载最小的槽位(允许超载) - if (bestSlot == null) { - log.warn("分组 '{}' 需要{}分钟,超过单个槽位容量,将选择负载最小的槽位(允许超载)", - group.getGroupName(), group.getEstimatedDuration()); - - if (preferredVenueId != null) { - bestSlot = slots.stream() - .filter(s -> s.venueId.equals(preferredVenueId)) - .min((a, b) -> Integer.compare(a.currentLoad, b.currentLoad)) - .orElse(null); + for (ScheduleGroupData group : projectGroups) { + group.setAssignedVenueId(bestSlot.venueId); + group.setAssignedVenueName(bestSlot.venueName); + group.setAssignedDate(bestSlot.date); + group.setAssignedTimeSlot(bestSlot.timeSlot); + group.setAssignedTimeSlotIndex(bestSlot.timeSlotIndex); + group.setAssignedTimePeriod(bestSlot.period); + + bestSlot.currentLoad += group.getEstimatedDuration(); + assignedCount++; + + log.info(" 分组 '{}' 分配完成, 时长={}分钟, 槽位负载={}分钟", + group.getGroupName(), group.getEstimatedDuration(), bestSlot.currentLoad); } - if (bestSlot == null) { - bestSlot = slots.stream() - .min((a, b) -> Integer.compare(a.currentLoad, b.currentLoad)) - .orElse(slots.get(0)); - } - } - - group.setAssignedVenueId(bestSlot.venueId); - group.setAssignedVenueName(bestSlot.venueName); - group.setAssignedDate(bestSlot.date); - group.setAssignedTimeSlot(bestSlot.timeSlot); - group.setAssignedTimeSlotIndex(bestSlot.timeSlotIndex); - group.setAssignedTimePeriod(bestSlot.period); - - bestSlot.currentLoad += group.getEstimatedDuration(); - assignedCount++; - - // 记录该项目分配的场地 - if (!projectVenueMap.containsKey(projectId)) { projectVenueMap.put(projectId, bestSlot.venueId); - log.info("项目 '{}' 首次分配到场地: {}", group.getGroupName(), bestSlot.venueName); + projectSlotMap.put(projectId, bestSlot.timeSlotIndex); + } else { + // 没有单个槽位能容纳整个项目,需要分散到多个槽位 + log.warn("项目 '{}' 总时长{}分钟超过单个时段容量,需要分散分配", projectName, projectDuration); + + // 优先选择同一场地的连续时间段 + final Long[] preferredVenueId = {null}; + + for (ScheduleGroupData group : projectGroups) { + SlotInfo targetSlot = null; + + // 如果已有优先场地,先尝试在同一场地找槽位 + if (preferredVenueId[0] != null) { + targetSlot = slots.stream() + .filter(s -> s.venueId.equals(preferredVenueId[0])) + .filter(s -> s.currentLoad + group.getEstimatedDuration() <= s.capacity) + .min((a, b) -> Integer.compare(a.timeSlotIndex, b.timeSlotIndex)) // 优先选择较早的时间段 + .orElse(null); + } + + // 如果优先场地没有容量,选择任意有容量的槽位 + if (targetSlot == null) { + targetSlot = slots.stream() + .filter(s -> s.currentLoad + group.getEstimatedDuration() <= s.capacity) + .min((a, b) -> Integer.compare(a.currentLoad, b.currentLoad)) + .orElse(null); + } + + // 如果都没有,选择负载最小的(允许超载) + if (targetSlot == null) { + targetSlot = slots.stream() + .min((a, b) -> Integer.compare(a.currentLoad, b.currentLoad)) + .orElse(slots.get(0)); + log.warn(" 分组 '{}' 需要超载分配", group.getGroupName()); + } + + group.setAssignedVenueId(targetSlot.venueId); + group.setAssignedVenueName(targetSlot.venueName); + group.setAssignedDate(targetSlot.date); + group.setAssignedTimeSlot(targetSlot.timeSlot); + group.setAssignedTimeSlotIndex(targetSlot.timeSlotIndex); + group.setAssignedTimePeriod(targetSlot.period); + + targetSlot.currentLoad += group.getEstimatedDuration(); + assignedCount++; + + // 记录优先场地 + if (preferredVenueId[0] == null) { + preferredVenueId[0] = targetSlot.venueId; + } + + log.info(" 分组 '{}' 分配到: 场地={}, 时段={}, 时长={}分钟", + group.getGroupName(), targetSlot.venueName, targetSlot.timeSlot, group.getEstimatedDuration()); + } + + projectVenueMap.put(projectId, preferredVenueId[0]); } - - log.info("分组 '{}' 分配到: 场地ID={}, 场地名={}, 日期={}, 时间段={}, 预计时长={}分钟, 新负载={}分钟 ({}%)", - group.getGroupName(), bestSlot.venueId, bestSlot.venueName, bestSlot.date, bestSlot.timeSlot, - group.getEstimatedDuration(), bestSlot.currentLoad, - (int)(bestSlot.currentLoad * 100.0 / bestSlot.capacity)); } log.info("=== 分配完成: {}/{} 个分组成功分配 ===", assignedCount, groups.size()); - // 输出每个项目的场地分配情况 - log.info("=== 项目-场地分配情况 ==="); - Map venueNameMap = venues.stream().collect(Collectors.toMap(MartialVenue::getId, MartialVenue::getVenueName)); - projectVenueMap.forEach((projectId, venueId) -> { - String projectName = groups.stream() - .filter(g -> g.getProjectId().equals(projectId)) - .map(ScheduleGroupData::getGroupName) - .findFirst().orElse("未知项目"); - log.info("项目: {} -> 场地: {}", projectName, venueNameMap.get(venueId)); - }); - // 输出每个槽位的负载情况 log.info("=== 各槽位负载统计 ==="); for (SlotInfo slot : slots) {