feat(schedule): 优化编排逻辑,同一项目尽量在同一时间段完成
- 按项目为单位进行分配,而不是按分组 - 计算每个项目的总时长,优先找能容纳整个项目的时间段 - 如果项目总时长不超过时间段容量(480分钟),所有分组都在同一时间段 - 只有当项目总时长超过单个时间段容量时才分散到多个时间段 - 分散时优先选择同一场地的连续时间段
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+112
-92
@@ -589,117 +589,137 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
|
|||||||
|
|
||||||
log.info("总共初始化了 {} 个场地×时间段组合", slots.size());
|
log.info("总共初始化了 {} 个场地×时间段组合", slots.size());
|
||||||
|
|
||||||
// 排序策略: 集体项目(projectType=2)优先,同类型按预计时长降序
|
// 按项目ID分组,计算每个项目的总时长
|
||||||
groups.sort((a, b) -> {
|
Map<Long, List<ScheduleGroupData>> groupsByProject = groups.stream()
|
||||||
int aType = a.getProjectType() != null ? a.getProjectType() : 1;
|
.collect(Collectors.groupingBy(ScheduleGroupData::getProjectId));
|
||||||
int bType = b.getProjectType() != null ? b.getProjectType() : 1;
|
|
||||||
if (aType != bType) {
|
// 计算每个项目的总时长
|
||||||
return bType - aType;
|
Map<Long, Integer> projectTotalDuration = new HashMap<>();
|
||||||
}
|
groupsByProject.forEach((projectId, projectGroups) -> {
|
||||||
int aDuration = a.getEstimatedDuration() != null ? a.getEstimatedDuration() : 0;
|
int total = projectGroups.stream().mapToInt(g -> g.getEstimatedDuration() != null ? g.getEstimatedDuration() : 0).sum();
|
||||||
int bDuration = b.getEstimatedDuration() != null ? b.getEstimatedDuration() : 0;
|
projectTotalDuration.put(projectId, total);
|
||||||
return bDuration - aDuration;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
log.info("排序后分组顺序(集体优先): {}",
|
// 排序策略: 集体项目优先,然后按项目总时长降序(大项目优先安排)
|
||||||
groups.stream().map(g -> g.getGroupName() + "(type=" + g.getProjectType() + ")").collect(java.util.stream.Collectors.joining(", ")));
|
List<Long> sortedProjectIds = new ArrayList<>(groupsByProject.keySet());
|
||||||
|
sortedProjectIds.sort((a, b) -> {
|
||||||
|
List<ScheduleGroupData> groupsA = groupsByProject.get(a);
|
||||||
|
List<ScheduleGroupData> 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<Long, Long> projectVenueMap = new HashMap<>();
|
|
||||||
int assignedCount = 0;
|
int assignedCount = 0;
|
||||||
|
Map<Long, Long> projectVenueMap = new HashMap<>(); // 记录项目分配的场地
|
||||||
|
Map<Long, Integer> projectSlotMap = new HashMap<>(); // 记录项目分配的时间段
|
||||||
|
|
||||||
for (ScheduleGroupData group : groups) {
|
// 按项目分配(同一项目的所有分组尽量在同一场地同一时间段)
|
||||||
Long projectId = group.getProjectId();
|
for (Long projectId : sortedProjectIds) {
|
||||||
Long preferredVenueId = projectVenueMap.get(projectId);
|
List<ScheduleGroupData> projectGroups = groupsByProject.get(projectId);
|
||||||
|
int projectDuration = projectTotalDuration.get(projectId);
|
||||||
|
String projectName = projectGroups.get(0).getGroupName().split(" ")[0];
|
||||||
|
|
||||||
SlotInfo bestSlot = null;
|
log.info("开始分配项目 '{}': {}个分组, 总时长{}分钟", projectName, projectGroups.size(), projectDuration);
|
||||||
|
|
||||||
// 优先策略: 如果该项目已有分配的场地,优先选择同一场地的其他时间段
|
// 找到能容纳整个项目的最佳槽位(优先选择负载最小的)
|
||||||
if (preferredVenueId != null) {
|
SlotInfo bestSlot = slots.stream()
|
||||||
bestSlot = slots.stream()
|
.filter(s -> s.currentLoad + projectDuration <= s.capacity)
|
||||||
.filter(s -> s.venueId.equals(preferredVenueId))
|
.min((a, b) -> {
|
||||||
.filter(s -> s.currentLoad + group.getEstimatedDuration() <= s.capacity)
|
// 优先选择负载最小的槽位
|
||||||
.min((a, b) -> Integer.compare(a.currentLoad, b.currentLoad))
|
return Integer.compare(a.currentLoad, b.currentLoad);
|
||||||
.orElse(null);
|
})
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
if (bestSlot != null) {
|
if (bestSlot != null) {
|
||||||
log.info("项目 '{}' 的分组 '{}' 优先分配到同一场地: {}",
|
// 找到能容纳整个项目的槽位,所有分组都分配到这个槽位
|
||||||
group.getGroupName(), group.getGroupName(), bestSlot.venueName);
|
log.info("项目 '{}' 整体分配到: 场地={}, 时段={}, 剩余容量={}分钟",
|
||||||
}
|
projectName, bestSlot.venueName, bestSlot.timeSlot, bestSlot.capacity - bestSlot.currentLoad);
|
||||||
}
|
|
||||||
|
|
||||||
// 如果优先场地没有足够容量,则按负载均衡策略选择
|
for (ScheduleGroupData group : projectGroups) {
|
||||||
if (bestSlot == null) {
|
group.setAssignedVenueId(bestSlot.venueId);
|
||||||
Map<Long, List<SlotInfo>> slotsByVenue = slots.stream()
|
group.setAssignedVenueName(bestSlot.venueName);
|
||||||
.collect(Collectors.groupingBy(s -> s.venueId));
|
group.setAssignedDate(bestSlot.date);
|
||||||
|
group.setAssignedTimeSlot(bestSlot.timeSlot);
|
||||||
|
group.setAssignedTimeSlotIndex(bestSlot.timeSlotIndex);
|
||||||
|
group.setAssignedTimePeriod(bestSlot.period);
|
||||||
|
|
||||||
bestSlot = slots.stream()
|
bestSlot.currentLoad += group.getEstimatedDuration();
|
||||||
.filter(s -> s.currentLoad + group.getEstimatedDuration() <= s.capacity)
|
assignedCount++;
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有找到容量足够的槽位,选择负载最小的槽位(允许超载)
|
log.info(" 分组 '{}' 分配完成, 时长={}分钟, 槽位负载={}分钟",
|
||||||
if (bestSlot == null) {
|
group.getGroupName(), group.getEstimatedDuration(), bestSlot.currentLoad);
|
||||||
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.setAssignedTimePeriod(bestSlot.period);
|
|
||||||
|
|
||||||
bestSlot.currentLoad += group.getEstimatedDuration();
|
|
||||||
assignedCount++;
|
|
||||||
|
|
||||||
// 记录该项目分配的场地
|
|
||||||
if (!projectVenueMap.containsKey(projectId)) {
|
|
||||||
projectVenueMap.put(projectId, bestSlot.venueId);
|
projectVenueMap.put(projectId, bestSlot.venueId);
|
||||||
log.info("项目 '{}' 首次分配到场地: {}", group.getGroupName(), bestSlot.venueName);
|
projectSlotMap.put(projectId, bestSlot.timeSlotIndex);
|
||||||
}
|
} else {
|
||||||
|
// 没有单个槽位能容纳整个项目,需要分散到多个槽位
|
||||||
|
log.warn("项目 '{}' 总时长{}分钟超过单个时段容量,需要分散分配", projectName, projectDuration);
|
||||||
|
|
||||||
log.info("分组 '{}' 分配到: 场地ID={}, 场地名={}, 日期={}, 时间段={}, 预计时长={}分钟, 新负载={}分钟 ({}%)",
|
// 优先选择同一场地的连续时间段
|
||||||
group.getGroupName(), bestSlot.venueId, bestSlot.venueName, bestSlot.date, bestSlot.timeSlot,
|
final Long[] preferredVenueId = {null};
|
||||||
group.getEstimatedDuration(), bestSlot.currentLoad,
|
|
||||||
(int)(bestSlot.currentLoad * 100.0 / bestSlot.capacity));
|
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("=== 分配完成: {}/{} 个分组成功分配 ===", assignedCount, groups.size());
|
log.info("=== 分配完成: {}/{} 个分组成功分配 ===", assignedCount, groups.size());
|
||||||
|
|
||||||
// 输出每个项目的场地分配情况
|
|
||||||
log.info("=== 项目-场地分配情况 ===");
|
|
||||||
Map<Long, String> 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("=== 各槽位负载统计 ===");
|
log.info("=== 各槽位负载统计 ===");
|
||||||
for (SlotInfo slot : slots) {
|
for (SlotInfo slot : slots) {
|
||||||
|
|||||||
Reference in New Issue
Block a user