feat(schedule): 同一项目优先分配到同一场地
- 新增projectVenueMap记录每个项目首次分配的场地 - 同一项目的后续分组优先分配到相同场地 - 只有当优先场地容量不足时才分配到其他场地 - 新增日志输出项目-场地分配情况
This commit is contained in:
+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