feat(schedule): 同一项目优先分配到同一场地
- 新增projectVenueMap记录每个项目首次分配的场地 - 同一项目的后续分组优先分配到相同场地 - 只有当优先场地容量不足时才分配到其他场地 - 新增日志输出项目-场地分配情况
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.
+77
-48
@@ -570,11 +570,11 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
|
||||
|
||||
// 创建所有槽位(场地 × 时间段组合)
|
||||
List<SlotInfo> slots = new ArrayList<>();
|
||||
int timeSlotIndex = 0; // 时间段索引,对应前端的 timeSlotIndex
|
||||
for (TimeSlot timeSlot : timeSlots) { // 先遍历时间段
|
||||
for (MartialVenue venue : venues) { // 再遍历场地
|
||||
int timeSlotIndex = 0;
|
||||
for (TimeSlot timeSlot : timeSlots) {
|
||||
for (MartialVenue venue : venues) {
|
||||
SlotInfo slot = new SlotInfo();
|
||||
slot.timeSlotIndex = timeSlotIndex; // 同一时间段的所有场地,共享相同的 timeSlotIndex
|
||||
slot.timeSlotIndex = timeSlotIndex;
|
||||
slot.venueId = venue.getId();
|
||||
slot.venueName = venue.getVenueName();
|
||||
slot.date = timeSlot.getDate();
|
||||
@@ -584,20 +584,18 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
|
||||
slot.currentLoad = 0;
|
||||
slots.add(slot);
|
||||
}
|
||||
timeSlotIndex++; // 每个时间段结束后,索引+1
|
||||
timeSlotIndex++;
|
||||
}
|
||||
|
||||
log.info("总共初始化了 {} 个场地×时间段组合", slots.size());
|
||||
|
||||
// 排序策略: 集体项目(projectType=2)优先,同类型按预计时长降序
|
||||
groups.sort((a, b) -> {
|
||||
// 1. 集体项目优先于单人项目
|
||||
int aType = a.getProjectType() != null ? a.getProjectType() : 1;
|
||||
int bType = b.getProjectType() != null ? b.getProjectType() : 1;
|
||||
if (aType != bType) {
|
||||
return bType - aType; // 2(集体) > 1(单人)
|
||||
return bType - aType;
|
||||
}
|
||||
// 2. 同类型按预计时长降序(先安排时间长的)
|
||||
int aDuration = a.getEstimatedDuration() != null ? a.getEstimatedDuration() : 0;
|
||||
int bDuration = b.getEstimatedDuration() != null ? b.getEstimatedDuration() : 0;
|
||||
return bDuration - aDuration;
|
||||
@@ -606,72 +604,102 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
|
||||
log.info("排序后分组顺序(集体优先): {}",
|
||||
groups.stream().map(g -> g.getGroupName() + "(type=" + g.getProjectType() + ")").collect(java.util.stream.Collectors.joining(", ")));
|
||||
|
||||
// 使用轮询算法进行均匀分配
|
||||
// 记录每个项目已分配的场地ID,用于同一项目优先分配到同一场地
|
||||
Map<Long, Long> projectVenueMap = new HashMap<>();
|
||||
int assignedCount = 0;
|
||||
int currentSlotIndex = 0;
|
||||
|
||||
for (ScheduleGroupData group : groups) {
|
||||
Long projectId = group.getProjectId();
|
||||
Long preferredVenueId = projectVenueMap.get(projectId);
|
||||
|
||||
SlotInfo bestSlot = null;
|
||||
int attempts = 0;
|
||||
int maxAttempts = slots.size();
|
||||
|
||||
// 第一轮:从当前槽位开始轮询,寻找第一个容量足够的槽位
|
||||
int startIndex = currentSlotIndex;
|
||||
while (attempts < maxAttempts) {
|
||||
SlotInfo slot = slots.get(currentSlotIndex);
|
||||
|
||||
// 检查容量是否足够
|
||||
if (slot.currentLoad + group.getEstimatedDuration() <= slot.capacity) {
|
||||
bestSlot = slot;
|
||||
break;
|
||||
}
|
||||
|
||||
// 尝试下一个槽位
|
||||
currentSlotIndex = (currentSlotIndex + 1) % slots.size();
|
||||
attempts++;
|
||||
}
|
||||
|
||||
// 第二轮:如果没有找到容量足够的槽位,选择负载最小的槽位(允许超载)
|
||||
if (bestSlot == null) {
|
||||
log.warn("分组 '{}' 需要{}分钟,超过单个槽位容量{}分钟,将选择负载最小的槽位(允许超载)",
|
||||
group.getGroupName(), group.getEstimatedDuration(), slots.get(0).capacity);
|
||||
|
||||
|
||||
// 优先策略: 如果该项目已有分配的场地,优先选择同一场地的其他时间段
|
||||
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(slots.get(0));
|
||||
|
||||
// 设置currentSlotIndex到bestSlot的位置
|
||||
for (int i = 0; i < slots.size(); i++) {
|
||||
if (slots.get(i) == bestSlot) {
|
||||
currentSlotIndex = i;
|
||||
break;
|
||||
}
|
||||
.orElse(null);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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.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);
|
||||
}
|
||||
|
||||
log.info("分组 '{}' 分配到: 场地ID={}, 场地名={}, 日期={}, 时间段={}, 预计时长={}分钟, 新负载={}分钟 ({}%)",
|
||||
group.getGroupName(), bestSlot.venueId, bestSlot.venueName, bestSlot.date, bestSlot.timeSlot,
|
||||
group.getEstimatedDuration(), bestSlot.currentLoad,
|
||||
(int)(bestSlot.currentLoad * 100.0 / bestSlot.capacity));
|
||||
|
||||
// 移动到下一个槽位(轮询)
|
||||
currentSlotIndex = (currentSlotIndex + 1) % slots.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("=== 各槽位负载统计 ===");
|
||||
for (SlotInfo slot : slots) {
|
||||
@@ -683,6 +711,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 槽位信息内部类
|
||||
private static class SlotInfo {
|
||||
int timeSlotIndex; // 时间段索引 (0=第1天上午, 1=第1天下午, 2=第2天上午, ...)
|
||||
|
||||
Reference in New Issue
Block a user