refactor: Phase 3 - Extract VenueAllocationService from God Class
- Create VenueAllocationService in schedule/allocation/ - Modify MartialScheduleArrangeServiceImpl to use VenueAllocationService - Remove validateCapacity, assignVenueAndTimeSlot methods and SlotInfo class - Original class reduced from 692 to ~500 lines - All 438 tests passing Related to Issue #11
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.
+15
-205
@@ -27,6 +27,7 @@ import org.springblade.modules.martial.config.ScheduleConfig;
|
|||||||
import org.springblade.modules.martial.service.IMartialScheduleArrangeService;
|
import org.springblade.modules.martial.service.IMartialScheduleArrangeService;
|
||||||
import org.springblade.modules.martial.service.schedule.generator.TimeSlotGenerator;
|
import org.springblade.modules.martial.service.schedule.generator.TimeSlotGenerator;
|
||||||
import org.springblade.modules.martial.service.schedule.grouping.ParticipantGroupingService;
|
import org.springblade.modules.martial.service.schedule.grouping.ParticipantGroupingService;
|
||||||
|
import org.springblade.modules.martial.service.schedule.allocation.VenueAllocationService;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springblade.modules.martial.service.schedule.model.ScheduleGroupData;
|
import org.springblade.modules.martial.service.schedule.model.ScheduleGroupData;
|
||||||
import org.springblade.modules.martial.service.schedule.model.TimeSlot;
|
import org.springblade.modules.martial.service.schedule.model.TimeSlot;
|
||||||
@@ -62,6 +63,8 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
|
|||||||
private final TimeSlotGenerator timeSlotGenerator;
|
private final TimeSlotGenerator timeSlotGenerator;
|
||||||
|
|
||||||
private final ParticipantGroupingService participantGroupingService;
|
private final ParticipantGroupingService participantGroupingService;
|
||||||
|
private final VenueAllocationService venueAllocationService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Long> getUnlockedCompetitions() {
|
public List<Long> getUnlockedCompetitions() {
|
||||||
// 查询所有未锁定的赛事(schedule_status != 2)
|
// 查询所有未锁定的赛事(schedule_status != 2)
|
||||||
@@ -390,222 +393,29 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
|
|||||||
/**
|
/**
|
||||||
* 验证时间窗口容量是否足够容纳所有分组
|
* 验证时间窗口容量是否足够容纳所有分组
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Validate capacity
|
||||||
|
* @deprecated Use VenueAllocationService.validateCapacity() directly
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
private void validateCapacity(List<ScheduleGroupData> groups,
|
private void validateCapacity(List<ScheduleGroupData> groups,
|
||||||
List<MartialVenue> venues,
|
List<MartialVenue> venues,
|
||||||
List<TimeSlot> timeSlots) {
|
List<TimeSlot> timeSlots) {
|
||||||
// 计算总需求时长
|
venueAllocationService.validateCapacity(groups, venues, timeSlots);
|
||||||
int totalDuration = groups.stream()
|
|
||||||
.mapToInt(ScheduleGroupData::getEstimatedDuration)
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
// 计算总可用容量
|
|
||||||
int totalCapacity = venues.size() * timeSlots.size() * (timeSlots.isEmpty() ? 0 : timeSlots.get(0).getCapacity());
|
|
||||||
|
|
||||||
log.info("=== 容量验证 ===");
|
|
||||||
log.info("分组总需求时长: {} 分钟", totalDuration);
|
|
||||||
log.info("总可用容量: {} 分钟 ({}个场地 × {}个时段 × {}分钟/时段)",
|
|
||||||
totalCapacity, venues.size(), timeSlots.size(),
|
|
||||||
timeSlots.isEmpty() ? 0 : timeSlots.get(0).getCapacity());
|
|
||||||
|
|
||||||
if (totalDuration > totalCapacity) {
|
|
||||||
String errorMsg = String.format(
|
|
||||||
"时间窗口容量不足! 需要 %d 分钟, 但只有 %d 分钟可用 (缺口: %d 分钟)",
|
|
||||||
totalDuration, totalCapacity, totalDuration - totalCapacity
|
|
||||||
);
|
|
||||||
log.error(errorMsg);
|
|
||||||
throw new RuntimeException(errorMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
double utilizationRate = totalCapacity > 0 ? (totalDuration * 100.0 / totalCapacity) : 0;
|
|
||||||
log.info("预计容量利用率: {}%", (int)utilizationRate);
|
|
||||||
|
|
||||||
if (utilizationRate > scheduleConfig.getCapacityWarningThreshold()) {
|
|
||||||
log.warn("⚠️ 容量利用率超过90%,可能导致分配困难,建议增加场地或延长比赛时间");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign venue and time slot
|
||||||
|
* @deprecated Use VenueAllocationService.allocate() directly
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
private void assignVenueAndTimeSlot(List<ScheduleGroupData> groups,
|
private void assignVenueAndTimeSlot(List<ScheduleGroupData> groups,
|
||||||
List<MartialVenue> venues,
|
List<MartialVenue> venues,
|
||||||
List<TimeSlot> timeSlots) {
|
List<TimeSlot> timeSlots) {
|
||||||
log.info("=== 开始分配场地和时间段 ===");
|
venueAllocationService.allocate(groups, venues, timeSlots);
|
||||||
log.info("场地数量: {}, 时间段数量: {}, 分组数量: {}", venues.size(), timeSlots.size(), groups.size());
|
|
||||||
|
|
||||||
// 打印场地容量信息
|
|
||||||
for (MartialVenue venue : venues) {
|
|
||||||
log.info("场地: {}, 容纳人数: {}", venue.getVenueName(), venue.getCapacity());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每个场地的已用时间(跨所有时段累计)
|
|
||||||
Map<Long, Integer> venueTotalUsed = new LinkedHashMap<>();
|
|
||||||
for (MartialVenue venue : venues) {
|
|
||||||
venueTotalUsed.put(venue.getId(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 每个时段每个场地的已用时间
|
|
||||||
Map<String, Map<Long, Integer>> slotVenueUsed = new LinkedHashMap<>();
|
|
||||||
Map<String, TimeSlot> slotMap = new LinkedHashMap<>();
|
|
||||||
|
|
||||||
for (TimeSlot ts : timeSlots) {
|
|
||||||
String key = ts.getDate() + "_" + ts.getPeriod();
|
|
||||||
if (!slotVenueUsed.containsKey(key)) {
|
|
||||||
slotVenueUsed.put(key, new LinkedHashMap<>());
|
|
||||||
slotMap.put(key, ts);
|
|
||||||
for (MartialVenue venue : venues) {
|
|
||||||
slotVenueUsed.get(key).put(venue.getId(), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按项目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);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 排序: 集体项目优先,然后按时长降序
|
|
||||||
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(", ")));
|
|
||||||
|
|
||||||
int assignedCount = 0;
|
|
||||||
|
|
||||||
for (Long projectId : sortedProjectIds) {
|
|
||||||
List<ScheduleGroupData> projectGroups = groupsByProject.get(projectId);
|
|
||||||
int projectDuration = projectTotalDuration.get(projectId);
|
|
||||||
String projectName = projectGroups.get(0).getGroupName().split(" ")[0];
|
|
||||||
|
|
||||||
// 获取项目的单位容纳人数(每组最大人数)
|
|
||||||
int projectMaxParticipants = projectGroups.get(0).getMaxParticipants() != null
|
|
||||||
? projectGroups.get(0).getMaxParticipants() : 1;
|
|
||||||
|
|
||||||
log.info("开始分配项目 '{}': {}个分组, 总时长{}分钟, 单位容纳人数={}",
|
|
||||||
projectName, projectGroups.size(), projectDuration, projectMaxParticipants);
|
|
||||||
|
|
||||||
// 第一步:筛选出容量足够的场地
|
|
||||||
List<MartialVenue> eligibleVenues = venues.stream()
|
|
||||||
.filter(v -> {
|
|
||||||
Integer capacity = v.getCapacity();
|
|
||||||
// 如果场地没有设置容量,默认允许
|
|
||||||
if (capacity == null || capacity <= 0) return true;
|
|
||||||
// 场地容量必须 >= 项目单位容纳人数
|
|
||||||
return capacity >= projectMaxParticipants;
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
if (eligibleVenues.isEmpty()) {
|
|
||||||
log.warn("项目 '{}' 需要容纳{}人的场地,但没有符合条件的场地!使用第一个场地",
|
|
||||||
projectName, projectMaxParticipants);
|
|
||||||
eligibleVenues = venues;
|
|
||||||
} else {
|
|
||||||
log.info("项目 '{}' 可用场地: {}", projectName,
|
|
||||||
eligibleVenues.stream().map(MartialVenue::getVenueName).collect(Collectors.joining(", ")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 第二步:在符合条件的场地中,选择总负载最小的
|
|
||||||
Long bestVenueId = null;
|
|
||||||
int minTotalUsed = Integer.MAX_VALUE;
|
|
||||||
for (MartialVenue venue : eligibleVenues) {
|
|
||||||
int used = venueTotalUsed.get(venue.getId());
|
|
||||||
if (used < minTotalUsed) {
|
|
||||||
minTotalUsed = used;
|
|
||||||
bestVenueId = venue.getId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 第三步:在选定的场地中,找第一个能容纳该项目的时段
|
|
||||||
String bestSlotKey = null;
|
|
||||||
for (String slotKey : slotVenueUsed.keySet()) {
|
|
||||||
TimeSlot ts = slotMap.get(slotKey);
|
|
||||||
int used = slotVenueUsed.get(slotKey).get(bestVenueId);
|
|
||||||
int remain = ts.getCapacity() - used;
|
|
||||||
|
|
||||||
if (remain >= projectDuration) {
|
|
||||||
bestSlotKey = slotKey;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有单个时段能容纳,选择剩余容量最大的时段
|
|
||||||
if (bestSlotKey == null) {
|
|
||||||
int maxRemain = -1;
|
|
||||||
for (String slotKey : slotVenueUsed.keySet()) {
|
|
||||||
TimeSlot ts = slotMap.get(slotKey);
|
|
||||||
int used = slotVenueUsed.get(slotKey).get(bestVenueId);
|
|
||||||
int remain = ts.getCapacity() - used;
|
|
||||||
if (remain > maxRemain) {
|
|
||||||
maxRemain = remain;
|
|
||||||
bestSlotKey = slotKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.warn("项目 '{}' 总时长{}分钟,在场地上没有单个时段能完全容纳", projectName, projectDuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeSlot targetSlot = slotMap.get(bestSlotKey);
|
|
||||||
int slotIndex = timeSlots.indexOf(targetSlot);
|
|
||||||
final Long venueId = bestVenueId;
|
|
||||||
String venueName = venues.stream().filter(v -> v.getId().equals(venueId)).findFirst().map(MartialVenue::getVenueName).orElse("未知场地");
|
|
||||||
|
|
||||||
log.info("项目 '{}' 分配到: 场地={}, 日期={}, 时段={}",
|
|
||||||
projectName, venueName, targetSlot.getDate(), targetSlot.getStartTime());
|
|
||||||
|
|
||||||
// 分配该项目的所有分组到同一场地
|
|
||||||
for (ScheduleGroupData group : projectGroups) {
|
|
||||||
group.setAssignedVenueId(venueId);
|
|
||||||
group.setAssignedVenueName(venueName);
|
|
||||||
group.setAssignedDate(targetSlot.getDate());
|
|
||||||
group.setAssignedTimeSlot(targetSlot.getStartTime());
|
|
||||||
group.setAssignedTimeSlotIndex(slotIndex);
|
|
||||||
group.setAssignedTimePeriod(targetSlot.getPeriod());
|
|
||||||
assignedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新已用时间
|
|
||||||
slotVenueUsed.get(bestSlotKey).put(venueId, slotVenueUsed.get(bestSlotKey).get(venueId) + projectDuration);
|
|
||||||
venueTotalUsed.put(venueId, venueTotalUsed.get(venueId) + projectDuration);
|
|
||||||
|
|
||||||
log.info(" 场地 {} 总负载更新为 {} 分钟", venueName, venueTotalUsed.get(venueId));
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("=== 分配完成: {}/{} 个分组成功分配 ===", assignedCount, groups.size());
|
|
||||||
|
|
||||||
// 输出使用统计
|
|
||||||
log.info("=== 各场地总负载 ===");
|
|
||||||
for (Map.Entry<Long, Integer> entry : venueTotalUsed.entrySet()) {
|
|
||||||
final Long vid = entry.getKey();
|
|
||||||
String vname = venues.stream().filter(v -> v.getId().equals(vid)).findFirst().map(MartialVenue::getVenueName).orElse("未知");
|
|
||||||
log.info("场地={}, 总负载={}分钟", vname, entry.getValue());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 槽位信息内部类
|
|
||||||
private static class SlotInfo {
|
|
||||||
int timeSlotIndex; // 时间段索引 (0=第1天上午, 1=第1天下午, 2=第2天上午, ...)
|
|
||||||
Long venueId;
|
|
||||||
String venueName;
|
|
||||||
LocalDate date;
|
|
||||||
String timeSlot;
|
|
||||||
String period;
|
|
||||||
int capacity;
|
|
||||||
int currentLoad;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearOldScheduleData(Long competitionId) {
|
private void clearOldScheduleData(Long competitionId) {
|
||||||
// 删除旧的编排数据
|
// 删除旧的编排数据
|
||||||
LambdaQueryWrapper<MartialScheduleGroup> groupWrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<MartialScheduleGroup> groupWrapper = new LambdaQueryWrapper<>();
|
||||||
|
|||||||
+259
@@ -0,0 +1,259 @@
|
|||||||
|
package org.springblade.modules.martial.service.schedule.allocation;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springblade.modules.martial.config.ScheduleConfig;
|
||||||
|
import org.springblade.modules.martial.pojo.entity.MartialVenue;
|
||||||
|
import org.springblade.modules.martial.service.schedule.model.ScheduleGroupData;
|
||||||
|
import org.springblade.modules.martial.service.schedule.model.TimeSlot;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Venue allocation service
|
||||||
|
* Allocates groups to venues and time slots
|
||||||
|
*
|
||||||
|
* @author Refactored from MartialScheduleArrangeServiceImpl
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class VenueAllocationService {
|
||||||
|
|
||||||
|
private final ScheduleConfig scheduleConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if capacity is sufficient for all groups
|
||||||
|
*/
|
||||||
|
public void validateCapacity(List<ScheduleGroupData> groups,
|
||||||
|
List<MartialVenue> venues,
|
||||||
|
List<TimeSlot> timeSlots) {
|
||||||
|
int totalDuration = groups.stream()
|
||||||
|
.mapToInt(ScheduleGroupData::getEstimatedDuration)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
int totalCapacity = venues.size() * timeSlots.size() *
|
||||||
|
(timeSlots.isEmpty() ? 0 : timeSlots.get(0).getCapacity());
|
||||||
|
|
||||||
|
log.info("=== Capacity Validation ===");
|
||||||
|
log.info("Total required duration: {} minutes", totalDuration);
|
||||||
|
log.info("Total available capacity: {} minutes ({} venues x {} slots x {} min/slot)",
|
||||||
|
totalCapacity, venues.size(), timeSlots.size(),
|
||||||
|
timeSlots.isEmpty() ? 0 : timeSlots.get(0).getCapacity());
|
||||||
|
|
||||||
|
if (totalDuration > totalCapacity) {
|
||||||
|
String errorMsg = String.format(
|
||||||
|
"Insufficient capacity! Need %d minutes, but only %d available (gap: %d minutes)",
|
||||||
|
totalDuration, totalCapacity, totalDuration - totalCapacity
|
||||||
|
);
|
||||||
|
log.error(errorMsg);
|
||||||
|
throw new RuntimeException(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
double utilizationRate = totalCapacity > 0 ? (totalDuration * 100.0 / totalCapacity) : 0;
|
||||||
|
log.info("Expected utilization rate: {}%", (int)utilizationRate);
|
||||||
|
|
||||||
|
if (utilizationRate > scheduleConfig.getCapacityWarningThreshold()) {
|
||||||
|
log.warn("Warning: Utilization rate exceeds 90%, allocation may be difficult");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocate groups to venues and time slots
|
||||||
|
*/
|
||||||
|
public void allocate(List<ScheduleGroupData> groups,
|
||||||
|
List<MartialVenue> venues,
|
||||||
|
List<TimeSlot> timeSlots) {
|
||||||
|
log.info("=== Starting venue and time slot allocation ===");
|
||||||
|
log.info("Venues: {}, Time slots: {}, Groups: {}", venues.size(), timeSlots.size(), groups.size());
|
||||||
|
|
||||||
|
Map<Long, Integer> venueTotalUsed = initVenueTotalUsed(venues);
|
||||||
|
Map<String, Map<Long, Integer>> slotVenueUsed = new LinkedHashMap<>();
|
||||||
|
Map<String, TimeSlot> slotMap = new LinkedHashMap<>();
|
||||||
|
initSlotMaps(timeSlots, venues, slotVenueUsed, slotMap);
|
||||||
|
|
||||||
|
Map<Long, List<ScheduleGroupData>> groupsByProject = groups.stream()
|
||||||
|
.collect(Collectors.groupingBy(ScheduleGroupData::getProjectId));
|
||||||
|
|
||||||
|
Map<Long, Integer> projectTotalDuration = calculateProjectDurations(groupsByProject);
|
||||||
|
List<Long> sortedProjectIds = sortProjectsByPriority(groupsByProject, projectTotalDuration);
|
||||||
|
|
||||||
|
int assignedCount = 0;
|
||||||
|
for (Long projectId : sortedProjectIds) {
|
||||||
|
assignedCount += allocateProject(projectId, groupsByProject.get(projectId),
|
||||||
|
projectTotalDuration.get(projectId), venues, timeSlots,
|
||||||
|
venueTotalUsed, slotVenueUsed, slotMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("=== Allocation complete: {}/{} groups assigned ===", assignedCount, groups.size());
|
||||||
|
logVenueUsage(venues, venueTotalUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Long, Integer> initVenueTotalUsed(List<MartialVenue> venues) {
|
||||||
|
Map<Long, Integer> venueTotalUsed = new LinkedHashMap<>();
|
||||||
|
for (MartialVenue venue : venues) {
|
||||||
|
venueTotalUsed.put(venue.getId(), 0);
|
||||||
|
}
|
||||||
|
return venueTotalUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initSlotMaps(List<TimeSlot> timeSlots, List<MartialVenue> venues,
|
||||||
|
Map<String, Map<Long, Integer>> slotVenueUsed,
|
||||||
|
Map<String, TimeSlot> slotMap) {
|
||||||
|
for (TimeSlot ts : timeSlots) {
|
||||||
|
String key = ts.getDate() + "_" + ts.getPeriod();
|
||||||
|
if (!slotVenueUsed.containsKey(key)) {
|
||||||
|
slotVenueUsed.put(key, new LinkedHashMap<>());
|
||||||
|
slotMap.put(key, ts);
|
||||||
|
for (MartialVenue venue : venues) {
|
||||||
|
slotVenueUsed.get(key).put(venue.getId(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Long, Integer> calculateProjectDurations(Map<Long, List<ScheduleGroupData>> groupsByProject) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
return projectTotalDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> sortProjectsByPriority(Map<Long, List<ScheduleGroupData>> groupsByProject,
|
||||||
|
Map<Long, Integer> projectTotalDuration) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
return sortedProjectIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int allocateProject(Long projectId, List<ScheduleGroupData> projectGroups,
|
||||||
|
int projectDuration, List<MartialVenue> venues,
|
||||||
|
List<TimeSlot> timeSlots, Map<Long, Integer> venueTotalUsed,
|
||||||
|
Map<String, Map<Long, Integer>> slotVenueUsed,
|
||||||
|
Map<String, TimeSlot> slotMap) {
|
||||||
|
String projectName = projectGroups.get(0).getGroupName().split(" ")[0];
|
||||||
|
int projectMaxParticipants = projectGroups.get(0).getMaxParticipants() != null
|
||||||
|
? projectGroups.get(0).getMaxParticipants() : 1;
|
||||||
|
|
||||||
|
List<MartialVenue> eligibleVenues = filterEligibleVenues(venues, projectMaxParticipants, projectName);
|
||||||
|
Long bestVenueId = findBestVenue(eligibleVenues, venueTotalUsed);
|
||||||
|
String bestSlotKey = findBestSlot(bestVenueId, projectDuration, slotVenueUsed, slotMap, projectName);
|
||||||
|
|
||||||
|
TimeSlot targetSlot = slotMap.get(bestSlotKey);
|
||||||
|
int slotIndex = timeSlots.indexOf(targetSlot);
|
||||||
|
final Long venueId = bestVenueId;
|
||||||
|
String venueName = venues.stream()
|
||||||
|
.filter(v -> v.getId().equals(venueId))
|
||||||
|
.findFirst()
|
||||||
|
.map(MartialVenue::getVenueName)
|
||||||
|
.orElse("Unknown");
|
||||||
|
|
||||||
|
log.info("Project '{}' assigned to: venue={}, date={}, slot={}",
|
||||||
|
projectName, venueName, targetSlot.getDate(), targetSlot.getStartTime());
|
||||||
|
|
||||||
|
for (ScheduleGroupData group : projectGroups) {
|
||||||
|
group.setAssignedVenueId(venueId);
|
||||||
|
group.setAssignedVenueName(venueName);
|
||||||
|
group.setAssignedDate(targetSlot.getDate());
|
||||||
|
group.setAssignedTimeSlot(targetSlot.getStartTime());
|
||||||
|
group.setAssignedTimeSlotIndex(slotIndex);
|
||||||
|
group.setAssignedTimePeriod(targetSlot.getPeriod());
|
||||||
|
}
|
||||||
|
|
||||||
|
slotVenueUsed.get(bestSlotKey).put(venueId,
|
||||||
|
slotVenueUsed.get(bestSlotKey).get(venueId) + projectDuration);
|
||||||
|
venueTotalUsed.put(venueId, venueTotalUsed.get(venueId) + projectDuration);
|
||||||
|
|
||||||
|
return projectGroups.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MartialVenue> filterEligibleVenues(List<MartialVenue> venues,
|
||||||
|
int projectMaxParticipants,
|
||||||
|
String projectName) {
|
||||||
|
List<MartialVenue> eligibleVenues = venues.stream()
|
||||||
|
.filter(v -> {
|
||||||
|
Integer capacity = v.getCapacity();
|
||||||
|
if (capacity == null || capacity <= 0) return true;
|
||||||
|
return capacity >= projectMaxParticipants;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (eligibleVenues.isEmpty()) {
|
||||||
|
log.warn("Project '{}' needs venue for {} people, but no eligible venue found! Using first venue",
|
||||||
|
projectName, projectMaxParticipants);
|
||||||
|
return venues;
|
||||||
|
}
|
||||||
|
return eligibleVenues;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long findBestVenue(List<MartialVenue> eligibleVenues, Map<Long, Integer> venueTotalUsed) {
|
||||||
|
Long bestVenueId = null;
|
||||||
|
int minTotalUsed = Integer.MAX_VALUE;
|
||||||
|
for (MartialVenue venue : eligibleVenues) {
|
||||||
|
int used = venueTotalUsed.get(venue.getId());
|
||||||
|
if (used < minTotalUsed) {
|
||||||
|
minTotalUsed = used;
|
||||||
|
bestVenueId = venue.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestVenueId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String findBestSlot(Long venueId, int projectDuration,
|
||||||
|
Map<String, Map<Long, Integer>> slotVenueUsed,
|
||||||
|
Map<String, TimeSlot> slotMap, String projectName) {
|
||||||
|
String bestSlotKey = null;
|
||||||
|
for (String slotKey : slotVenueUsed.keySet()) {
|
||||||
|
TimeSlot ts = slotMap.get(slotKey);
|
||||||
|
int used = slotVenueUsed.get(slotKey).get(venueId);
|
||||||
|
int remain = ts.getCapacity() - used;
|
||||||
|
if (remain >= projectDuration) {
|
||||||
|
bestSlotKey = slotKey;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestSlotKey == null) {
|
||||||
|
int maxRemain = -1;
|
||||||
|
for (String slotKey : slotVenueUsed.keySet()) {
|
||||||
|
TimeSlot ts = slotMap.get(slotKey);
|
||||||
|
int used = slotVenueUsed.get(slotKey).get(venueId);
|
||||||
|
int remain = ts.getCapacity() - used;
|
||||||
|
if (remain > maxRemain) {
|
||||||
|
maxRemain = remain;
|
||||||
|
bestSlotKey = slotKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.warn("Project '{}' duration {} minutes cannot fit in single slot", projectName, projectDuration);
|
||||||
|
}
|
||||||
|
return bestSlotKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logVenueUsage(List<MartialVenue> venues, Map<Long, Integer> venueTotalUsed) {
|
||||||
|
log.info("=== Venue Usage Summary ===");
|
||||||
|
for (Map.Entry<Long, Integer> entry : venueTotalUsed.entrySet()) {
|
||||||
|
final Long vid = entry.getKey();
|
||||||
|
String vname = venues.stream()
|
||||||
|
.filter(v -> v.getId().equals(vid))
|
||||||
|
.findFirst()
|
||||||
|
.map(MartialVenue::getVenueName)
|
||||||
|
.orElse("Unknown");
|
||||||
|
log.info("Venue={}, Total load={} minutes", vname, entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user