diff --git a/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta b/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta index 7b70dcc..145f5c9 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 8c6bf7a..8fe8b70 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 966512b..d0e02b4 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 08266bb..f009352 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 ecb413f..a9611f0 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 563925b..a77b66c 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 321100a..9463a1b 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 a87a6ed..983c982 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 ebb6012..40c82d3 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 @@ -570,11 +570,11 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang // 创建所有槽位(场地 × 时间段组合) List slots = new ArrayList<>(); - int timeSlotIndex = 0; // 时间段索引,对应前端的 timeSlotIndex - for (TimeSlot timeSlot : timeSlots) { // 先遍历时间段 - for (MartialVenue venue : venues) { // 再遍历场地 + int timeSlotIndex = 0; + for (TimeSlot timeSlot : timeSlots) { + for (MartialVenue venue : venues) { SlotInfo slot = new SlotInfo(); - slot.timeSlotIndex = timeSlotIndex; // 同一时间段的所有场地,共享相同的 timeSlotIndex + slot.timeSlotIndex = timeSlotIndex; slot.venueId = venue.getId(); slot.venueName = venue.getVenueName(); slot.date = timeSlot.getDate(); @@ -584,20 +584,18 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang slot.currentLoad = 0; slots.add(slot); } - timeSlotIndex++; // 每个时间段结束后,索引+1 + timeSlotIndex++; } log.info("总共初始化了 {} 个场地×时间段组合", slots.size()); // 排序策略: 集体项目(projectType=2)优先,同类型按预计时长降序 groups.sort((a, b) -> { - // 1. 集体项目优先于单人项目 int aType = a.getProjectType() != null ? a.getProjectType() : 1; int bType = b.getProjectType() != null ? b.getProjectType() : 1; if (aType != bType) { - return bType - aType; // 2(集体) > 1(单人) + return bType - aType; } - // 2. 同类型按预计时长降序(先安排时间长的) int aDuration = a.getEstimatedDuration() != null ? a.getEstimatedDuration() : 0; int bDuration = b.getEstimatedDuration() != null ? b.getEstimatedDuration() : 0; return bDuration - aDuration; @@ -606,72 +604,102 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang log.info("排序后分组顺序(集体优先): {}", groups.stream().map(g -> g.getGroupName() + "(type=" + g.getProjectType() + ")").collect(java.util.stream.Collectors.joining(", "))); - // 使用轮询算法进行均匀分配 + // 记录每个项目已分配的场地ID,用于同一项目优先分配到同一场地 + Map projectVenueMap = new HashMap<>(); int assignedCount = 0; - int currentSlotIndex = 0; for (ScheduleGroupData group : groups) { + Long projectId = group.getProjectId(); + Long preferredVenueId = projectVenueMap.get(projectId); + SlotInfo bestSlot = null; - int attempts = 0; - int maxAttempts = slots.size(); - - // 第一轮:从当前槽位开始轮询,寻找第一个容量足够的槽位 - int startIndex = currentSlotIndex; - while (attempts < maxAttempts) { - SlotInfo slot = slots.get(currentSlotIndex); - - // 检查容量是否足够 - if (slot.currentLoad + group.getEstimatedDuration() <= slot.capacity) { - bestSlot = slot; - break; - } - - // 尝试下一个槽位 - currentSlotIndex = (currentSlotIndex + 1) % slots.size(); - attempts++; - } - - // 第二轮:如果没有找到容量足够的槽位,选择负载最小的槽位(允许超载) - if (bestSlot == null) { - log.warn("分组 '{}' 需要{}分钟,超过单个槽位容量{}分钟,将选择负载最小的槽位(允许超载)", - group.getGroupName(), group.getEstimatedDuration(), slots.get(0).capacity); - + + // 优先策略: 如果该项目已有分配的场地,优先选择同一场地的其他时间段 + 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(slots.get(0)); - - // 设置currentSlotIndex到bestSlot的位置 - for (int i = 0; i < slots.size(); i++) { - if (slots.get(i) == bestSlot) { - currentSlotIndex = i; - break; - } + .orElse(null); + + 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); + } + + 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.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); + } log.info("分组 '{}' 分配到: 场地ID={}, 场地名={}, 日期={}, 时间段={}, 预计时长={}分钟, 新负载={}分钟 ({}%)", group.getGroupName(), bestSlot.venueId, bestSlot.venueName, bestSlot.date, bestSlot.timeSlot, group.getEstimatedDuration(), bestSlot.currentLoad, (int)(bestSlot.currentLoad * 100.0 / bestSlot.capacity)); - - // 移动到下一个槽位(轮询) - currentSlotIndex = (currentSlotIndex + 1) % slots.size(); } 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) { @@ -683,6 +711,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang } } + // 槽位信息内部类 private static class SlotInfo { int timeSlotIndex; // 时间段索引 (0=第1天上午, 1=第1天下午, 2=第2天上午, ...)