feat(schedule): 优化编排时间分配逻辑
- 添加晚上时段(19:00-22:00, 180分钟) - 调整时段容量: 上午240分钟, 下午240分钟, 晚上180分钟 - 按实际时长累加分配,同一时段可安排多个项目 - 同一项目的所有分组优先安排在同一场地同一时段
This commit is contained in:
+100
-118
@@ -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<ScheduleGroupData> autoGroupParticipants(List<MartialAthlete> athletes) {
|
||||
List<ScheduleGroupData> 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<SlotInfo> slots = new ArrayList<>();
|
||||
// 为每个场地创建时间轴(记录当前已用时间)
|
||||
// key = venueId + "_" + timeSlotIndex, value = 当前已用分钟数
|
||||
Map<String, Integer> venueTimeUsed = new HashMap<>();
|
||||
Map<String, TimeSlot> 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<Long, List<ScheduleGroupData>> groupsByProject = groups.stream()
|
||||
.collect(Collectors.groupingBy(ScheduleGroupData::getProjectId));
|
||||
|
||||
@@ -600,7 +606,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
|
||||
projectTotalDuration.put(projectId, total);
|
||||
});
|
||||
|
||||
// 排序策略: 集体项目优先,然后按项目总时长降序(大项目优先安排)
|
||||
// 排序: 集体项目优先,然后按时长降序
|
||||
List<Long> sortedProjectIds = new ArrayList<>(groupsByProject.keySet());
|
||||
sortedProjectIds.sort((a, b) -> {
|
||||
List<ScheduleGroupData> 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<Long, Long> projectVenueMap = new HashMap<>(); // 记录项目分配的场地
|
||||
Map<Long, Integer> projectSlotMap = new HashMap<>(); // 记录项目分配的时间段
|
||||
Map<Long, String> projectVenueSlotMap = new HashMap<>(); // 记录项目分配的场地+时段
|
||||
|
||||
// 按项目分配(同一项目的所有分组尽量在同一场地同一时间段)
|
||||
for (Long projectId : sortedProjectIds) {
|
||||
List<ScheduleGroupData> 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<String, Integer> 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<String, Integer> 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<String, Integer> 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user