feat(schedule): 优化编排逻辑,同一项目尽量在同一时间段完成
- 按项目为单位进行分配,而不是按分组 - 计算每个项目的总时长,优先找能容纳整个项目的时间段 - 如果项目总时长不超过时间段容量(480分钟),所有分组都在同一时间段 - 只有当项目总时长超过单个时间段容量时才分散到多个时间段 - 分散时优先选择同一场地的连续时间段
This commit is contained in:
+116
-96
@@ -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<Long, List<ScheduleGroupData>> groupsByProject = groups.stream()
|
||||
.collect(Collectors.groupingBy(ScheduleGroupData::getProjectId));
|
||||
|
||||
// 计算每个项目的总时长
|
||||
Map<Long, Integer> 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<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;
|
||||
Map<Long, Long> projectVenueMap = new HashMap<>(); // 记录项目分配的场地
|
||||
Map<Long, Integer> projectSlotMap = new HashMap<>(); // 记录项目分配的时间段
|
||||
|
||||
for (ScheduleGroupData group : groups) {
|
||||
Long projectId = group.getProjectId();
|
||||
Long preferredVenueId = projectVenueMap.get(projectId);
|
||||
// 按项目分配(同一项目的所有分组尽量在同一场地同一时间段)
|
||||
for (Long projectId : sortedProjectIds) {
|
||||
List<ScheduleGroupData> 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<Long, List<SlotInfo>> 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<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("=== 各槽位负载统计 ===");
|
||||
for (SlotInfo slot : slots) {
|
||||
|
||||
Reference in New Issue
Block a user