diff --git a/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta b/minio_data/.minio.sys/buckets/.bloomcycle.bin/xl.meta index 013bd58..c8adccc 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 b38fe2c..08c7f8f 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 8efdcc1..3f518d5 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 31727de..ade8937 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 d6d9816..05e16cc 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 9df63ca..4256545 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 e974690..3bec8f0 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 5177857..71f5400 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 d3c6634..672dc13 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 @@ -325,24 +325,36 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang TimeSlot morning = new TimeSlot(); morning.setDate(currentDate); morning.setPeriod("morning"); - morning.setStartTime("08:30"); - morning.setCapacity(480); // 上午480分钟(8小时,允许并发/灵活安排) + morning.setStartTime("08:00"); + morning.setCapacity(240); // 4小时 = 240分钟 timeSlots.add(morning); - // 下午时段 (13:00-18:00, 共300分钟) + // 下午时段 (14:00-18:00, 共240分钟) TimeSlot afternoon = new TimeSlot(); afternoon.setDate(currentDate); afternoon.setPeriod("afternoon"); - afternoon.setStartTime("13:30"); - afternoon.setCapacity(480); // 下午480分钟(8小时,允许并发/灵活安排) + afternoon.setStartTime("14:00"); + afternoon.setCapacity(240); // 4小时 = 240分钟 timeSlots.add(afternoon); + // 晚上时段 (19:00-22:00, 共180分钟) + TimeSlot evening = new TimeSlot(); + evening.setDate(currentDate); + evening.setPeriod("evening"); + evening.setStartTime("19:00"); + evening.setCapacity(180); // 3小时 = 180分钟 + timeSlots.add(evening); + currentDate = currentDate.plusDays(1); } + log.info("生成时间段: {}天, 每天3个时段(上午/下午/晚上), 共{}个时段", + java.time.temporal.ChronoUnit.DAYS.between(startTime.toLocalDate(), endDate) + 1, timeSlots.size()); + return timeSlots; } + private List autoGroupParticipants(List athletes) { List groups = new ArrayList<>(); int displayOrder = 1; @@ -568,28 +580,22 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang log.info("=== 开始分配场地和时间段 ==="); log.info("场地数量: {}, 时间段数量: {}, 分组数量: {}", venues.size(), timeSlots.size(), groups.size()); - // 创建所有槽位(场地 × 时间段组合) - List slots = new ArrayList<>(); + // 为每个场地创建时间轴(记录当前已用时间) + // key = venueId + "_" + timeSlotIndex, value = 当前已用分钟数 + Map venueTimeUsed = new HashMap<>(); + Map venueTimeSlotMap = new HashMap<>(); + int timeSlotIndex = 0; for (TimeSlot timeSlot : timeSlots) { for (MartialVenue venue : venues) { - SlotInfo slot = new SlotInfo(); - slot.timeSlotIndex = timeSlotIndex; - slot.venueId = venue.getId(); - slot.venueName = venue.getVenueName(); - slot.date = timeSlot.getDate(); - slot.timeSlot = timeSlot.getStartTime(); - slot.period = timeSlot.getPeriod(); - slot.capacity = timeSlot.getCapacity(); - slot.currentLoad = 0; - slots.add(slot); + String key = venue.getId() + "_" + timeSlotIndex; + venueTimeUsed.put(key, 0); + venueTimeSlotMap.put(key, timeSlot); } timeSlotIndex++; } - log.info("总共初始化了 {} 个场地×时间段组合", slots.size()); - - // 按项目ID分组,计算每个项目的总时长 + // 按项目ID分组 Map> groupsByProject = groups.stream() .collect(Collectors.groupingBy(ScheduleGroupData::getProjectId)); @@ -600,7 +606,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang projectTotalDuration.put(projectId, total); }); - // 排序策略: 集体项目优先,然后按项目总时长降序(大项目优先安排) + // 排序: 集体项目优先,然后按时长降序 List sortedProjectIds = new ArrayList<>(groupsByProject.keySet()); sortedProjectIds.sort((a, b) -> { List groupsA = groupsByProject.get(a); @@ -608,20 +614,18 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang 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 typeB - typeA; } - return projectTotalDuration.get(b) - projectTotalDuration.get(a); // 时长长的优先 + return projectTotalDuration.get(b) - projectTotalDuration.get(a); }); - log.info("项目排序(集体优先,时长降序): {}", sortedProjectIds.stream() + log.info("项目排序: {}", sortedProjectIds.stream() .map(id -> groupsByProject.get(id).get(0).getGroupName().split(" ")[0] + "(" + projectTotalDuration.get(id) + "分钟)") .collect(Collectors.joining(", "))); int assignedCount = 0; - Map projectVenueMap = new HashMap<>(); // 记录项目分配的场地 - Map projectSlotMap = new HashMap<>(); // 记录项目分配的时间段 + Map projectVenueSlotMap = new HashMap<>(); // 记录项目分配的场地+时段 - // 按项目分配(同一项目的所有分组尽量在同一场地同一时间段) for (Long projectId : sortedProjectIds) { List projectGroups = groupsByProject.get(projectId); int projectDuration = projectTotalDuration.get(projectId); @@ -629,104 +633,82 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang 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); + // 找到能容纳该项目的最佳场地+时段(优先选择已用时间最少的) + String bestKey = null; + int minUsed = Integer.MAX_VALUE; + + for (Map.Entry entry : venueTimeUsed.entrySet()) { + String key = entry.getKey(); + int used = entry.getValue(); + TimeSlot ts = venueTimeSlotMap.get(key); - 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 (used + projectDuration <= ts.getCapacity()) { + if (used < minUsed) { + minUsed = used; + bestKey = key; + } } - - projectVenueMap.put(projectId, bestSlot.venueId); - 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]); } + + // 如果没有单个时段能容纳,选择剩余容量最大的 + if (bestKey == null) { + int maxRemain = -1; + for (Map.Entry entry : venueTimeUsed.entrySet()) { + String key = entry.getKey(); + int used = entry.getValue(); + TimeSlot ts = venueTimeSlotMap.get(key); + int remain = ts.getCapacity() - used; + if (remain > maxRemain) { + maxRemain = remain; + bestKey = key; + } + } + 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); + String venueName = venues.stream().filter(v -> v.getId().equals(venueId)).findFirst().map(MartialVenue::getVenueName).orElse("未知场地"); + + log.info("项目 '{}' 分配到: 场地={}, 时段={}, 当前已用={}分钟", projectName, venueName, timeSlot.getStartTime(), venueTimeUsed.get(bestKey)); + + // 分配该项目的所有分组到同一场地+时段 + for (ScheduleGroupData group : projectGroups) { + group.setAssignedVenueId(venueId); + group.setAssignedVenueName(venueName); + group.setAssignedDate(timeSlot.getDate()); + group.setAssignedTimeSlot(timeSlot.getStartTime()); + group.setAssignedTimeSlotIndex(slotIndex); + group.setAssignedTimePeriod(timeSlot.getPeriod()); + assignedCount++; + + log.info(" 分组 '{}' 时长={}分钟", group.getGroupName(), group.getEstimatedDuration()); + } + + // 更新已用时间 + venueTimeUsed.put(bestKey, venueTimeUsed.get(bestKey) + projectDuration); + projectVenueSlotMap.put(projectId, bestKey); + + log.info(" 场地 {} 时段 {} 已用时间更新为 {} 分钟", venueName, timeSlot.getStartTime(), venueTimeUsed.get(bestKey)); } log.info("=== 分配完成: {}/{} 个分组成功分配 ===", assignedCount, groups.size()); - // 输出每个槽位的负载情况 - log.info("=== 各槽位负载统计 ==="); - for (SlotInfo slot : slots) { - if (slot.currentLoad > 0) { - log.info("场地={}, 日期={}, 时段={}, 负载={}/{}分钟 ({}%)", - slot.venueName, slot.date, slot.timeSlot, slot.currentLoad, slot.capacity, - (int)(slot.currentLoad * 100.0 / slot.capacity)); + // 输出各场地时段的使用情况 + 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())); } } }