feat(schedule): 优化编排时间分配逻辑
- 添加晚上时段(19:00-22:00, 180分钟) - 调整时段容量: 上午240分钟, 下午240分钟, 晚上180分钟 - 按实际时长累加分配,同一时段可安排多个项目 - 同一项目的所有分组优先安排在同一场地同一时段
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.
+99
-117
@@ -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);
|
||||
// 找到能容纳该项目的最佳场地+时段(优先选择已用时间最少的)
|
||||
String bestKey = null;
|
||||
int minUsed = Integer.MAX_VALUE;
|
||||
|
||||
if (bestSlot != null) {
|
||||
// 找到能容纳整个项目的槽位,所有分组都分配到这个槽位
|
||||
log.info("项目 '{}' 整体分配到: 场地={}, 时段={}, 剩余容量={}分钟",
|
||||
projectName, bestSlot.venueName, bestSlot.timeSlot, bestSlot.capacity - bestSlot.currentLoad);
|
||||
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