refactor: Phase 2 - Extract ParticipantGroupingService from God Class
- Create ScheduleGroupData model class in schedule/model/ - Create ParticipantGroupingService in schedule/grouping/ - Modify MartialScheduleArrangeServiceImpl to use ParticipantGroupingService - Remove internal ScheduleGroupData class and grouping methods - 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.
+9
-388
@@ -26,7 +26,9 @@ import org.springblade.modules.martial.pojo.entity.*;
|
|||||||
import org.springblade.modules.martial.config.ScheduleConfig;
|
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.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.TimeSlot;
|
import org.springblade.modules.martial.service.schedule.model.TimeSlot;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
|
|||||||
private final ScheduleConfig scheduleConfig;
|
private final ScheduleConfig scheduleConfig;
|
||||||
private final TimeSlotGenerator timeSlotGenerator;
|
private final TimeSlotGenerator timeSlotGenerator;
|
||||||
|
|
||||||
|
private final ParticipantGroupingService participantGroupingService;
|
||||||
@Override
|
@Override
|
||||||
public List<Long> getUnlockedCompetitions() {
|
public List<Long> getUnlockedCompetitions() {
|
||||||
// 查询所有未锁定的赛事(schedule_status != 2)
|
// 查询所有未锁定的赛事(schedule_status != 2)
|
||||||
@@ -372,250 +375,18 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
|
|||||||
* @param project 项目
|
* @param project 项目
|
||||||
* @return 分组类别名称: 男子/女子/混合/团体
|
* @return 分组类别名称: 男子/女子/混合/团体
|
||||||
*/
|
*/
|
||||||
private String getProjectCategoryName(MartialProject project) {
|
|
||||||
if (project == null || project.getCategory() == null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
String category = project.getCategory();
|
|
||||||
switch (category) {
|
|
||||||
case "1": return "男子";
|
|
||||||
case "2": return "女子";
|
|
||||||
case "3": return "混合";
|
|
||||||
case "4": return "团体";
|
|
||||||
default: return category; // 如果是自定义文字,直接返回
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据项目的分组类别过滤运动员
|
* Auto group participants
|
||||||
* @param athletes 运动员列表
|
* @deprecated Use ParticipantGroupingService.autoGroup() directly
|
||||||
* @param project 项目
|
|
||||||
* @return 过滤后的运动员列表
|
|
||||||
*/
|
*/
|
||||||
private List<MartialAthlete> filterAthletesByProjectCategory(List<MartialAthlete> athletes, MartialProject project) {
|
@Deprecated
|
||||||
if (project == null || project.getCategory() == null) {
|
|
||||||
return athletes;
|
|
||||||
}
|
|
||||||
String category = project.getCategory();
|
|
||||||
switch (category) {
|
|
||||||
case "1": // 男子项目,只保留男性运动员
|
|
||||||
return athletes.stream()
|
|
||||||
.filter(a -> a.getGender() == null || a.getGender() == 1)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
case "2": // 女子项目,只保留女性运动员
|
|
||||||
return athletes.stream()
|
|
||||||
.filter(a -> a.getGender() != null && a.getGender() == 2)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
case "3": // 混合项目,所有运动员
|
|
||||||
case "4": // 团体项目,所有运动员
|
|
||||||
default:
|
|
||||||
return athletes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ScheduleGroupData> autoGroupParticipants(List<MartialAthlete> athletes) {
|
private List<ScheduleGroupData> autoGroupParticipants(List<MartialAthlete> athletes) {
|
||||||
List<ScheduleGroupData> groups = new ArrayList<>();
|
return participantGroupingService.autoGroup(athletes);
|
||||||
int displayOrder = 1;
|
|
||||||
|
|
||||||
// 先加载所有项目信息(用于获取项目类型和名称)
|
|
||||||
Set<Long> projectIds = athletes.stream()
|
|
||||||
.map(MartialAthlete::getProjectId)
|
|
||||||
.filter(id -> id != null)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
|
|
||||||
Map<Long, MartialProject> projectMap = new HashMap<>();
|
|
||||||
for (Long projectId : projectIds) {
|
|
||||||
MartialProject project = projectMapper.selectById(projectId);
|
|
||||||
if (project != null) {
|
|
||||||
projectMap.put(projectId, project);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分离集体和个人项目(根据项目表的type字段: 1=个人, 2=双人, 3=集体)
|
|
||||||
List<MartialAthlete> teamAthletes = athletes.stream()
|
|
||||||
.filter(a -> {
|
|
||||||
MartialProject project = projectMap.get(a.getProjectId());
|
|
||||||
return project != null && (project.getType() == 2 || project.getType() == 3);
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
List<MartialAthlete> individualAthletes = athletes.stream()
|
|
||||||
.filter(a -> {
|
|
||||||
MartialProject project = projectMap.get(a.getProjectId());
|
|
||||||
return project != null && project.getType() == 1;
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 集体项目分组:按"项目ID_组别"分组
|
|
||||||
Map<String, List<MartialAthlete>> teamGroupMap = teamAthletes.stream()
|
|
||||||
.collect(Collectors.groupingBy(a ->
|
|
||||||
String.valueOf(a.getProjectId())
|
|
||||||
));
|
|
||||||
|
|
||||||
for (Map.Entry<String, List<MartialAthlete>> entry : teamGroupMap.entrySet()) {
|
|
||||||
List<MartialAthlete> members = entry.getValue();
|
|
||||||
if (members.isEmpty()) continue;
|
|
||||||
|
|
||||||
MartialAthlete first = members.get(0);
|
|
||||||
MartialProject project = projectMap.get(first.getProjectId());
|
|
||||||
|
|
||||||
// 跳过没有项目信息的分组
|
|
||||||
if (project == null) {
|
|
||||||
log.warn("项目不存在, projectId: {}, 跳过该分组", first.getProjectId());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据项目的分组类别过滤运动员(男子项目只保留男性,女子项目只保留女性)
|
|
||||||
members = filterAthletesByProjectCategory(members, project);
|
|
||||||
if (members.isEmpty()) {
|
|
||||||
log.info("项目 '{}' 过滤后无符合性别要求的运动员,跳过", project.getProjectName());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 统计队伍数(按单位分组)
|
|
||||||
long teamCount = members.stream()
|
|
||||||
.map(MartialAthlete::getTeamName)
|
|
||||||
.filter(org -> org != null && !org.isEmpty())
|
|
||||||
.distinct()
|
|
||||||
.count();
|
|
||||||
|
|
||||||
ScheduleGroupData group = new ScheduleGroupData();
|
|
||||||
String projectName = project.getProjectName();
|
|
||||||
String categoryName = getProjectCategoryName(project);
|
|
||||||
group.setGroupName(projectName + (categoryName.isEmpty() ? "" : " " + categoryName));
|
|
||||||
group.setProjectId(first.getProjectId());
|
|
||||||
group.setProjectType((project.getType() == 2 || project.getType() == 3) ? 2 : 1); // type=2(双人)或type=3(集体)映射为projectType=2(集体)
|
|
||||||
group.setMaxParticipants(project.getMaxParticipants()); // 设置项目单位容纳人数
|
|
||||||
group.setCategory(categoryName); // 设置组别
|
|
||||||
group.setDisplayOrder(displayOrder++);
|
|
||||||
group.setTotalParticipants(members.size());
|
|
||||||
group.setTotalTeams((int) teamCount);
|
|
||||||
group.setAthletes(members);
|
|
||||||
|
|
||||||
// 计算预计时长:使用项目的 estimatedDuration
|
|
||||||
// 如果项目设置了时长,使用 队伍数 × 项目时长
|
|
||||||
// 否则使用默认计算: 队伍数 × 5分钟 + 间隔时间
|
|
||||||
int duration;
|
|
||||||
if (project.getEstimatedDuration() != null && project.getEstimatedDuration() > 0) {
|
|
||||||
duration = (int) teamCount * project.getEstimatedDuration();
|
|
||||||
log.debug("集体项目 '{}': 使用项目时长 {}分钟/队, {}队, 总计{}分钟",
|
|
||||||
projectName, project.getEstimatedDuration(), teamCount, duration);
|
|
||||||
} else {
|
|
||||||
duration = (int) teamCount * 5 + Math.max(0, (int) teamCount - 1) * 2;
|
|
||||||
log.debug("集体项目 '{}': 使用默认时长, {}队, 总计{}分钟", projectName, teamCount, duration);
|
|
||||||
}
|
|
||||||
group.setEstimatedDuration(duration);
|
|
||||||
|
|
||||||
groups.add(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 个人项目分组:按"项目ID_组别"分组
|
|
||||||
Map<String, List<MartialAthlete>> individualGroupMap = individualAthletes.stream()
|
|
||||||
.collect(Collectors.groupingBy(a ->
|
|
||||||
String.valueOf(a.getProjectId())
|
|
||||||
));
|
|
||||||
|
|
||||||
for (Map.Entry<String, List<MartialAthlete>> entry : individualGroupMap.entrySet()) {
|
|
||||||
List<MartialAthlete> members = entry.getValue();
|
|
||||||
if (members.isEmpty()) continue;
|
|
||||||
|
|
||||||
MartialAthlete first = members.get(0);
|
|
||||||
MartialProject project = projectMap.get(first.getProjectId());
|
|
||||||
|
|
||||||
// 跳过没有项目信息的分组
|
|
||||||
if (project == null) {
|
|
||||||
log.warn("项目不存在, projectId: {}, 跳过该分组", first.getProjectId());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据项目的分组类别过滤运动员(男子项目只保留男性,女子项目只保留女性)
|
|
||||||
members = filterAthletesByProjectCategory(members, project);
|
|
||||||
if (members.isEmpty()) {
|
|
||||||
log.info("项目 '{}' 过滤后无符合性别要求的运动员,跳过", project.getProjectName());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String projectName = project.getProjectName();
|
|
||||||
String categoryName = getProjectCategoryName(project);
|
|
||||||
|
|
||||||
// 计算单人时长
|
|
||||||
int durationPerPerson = scheduleConfig.getDefaultDurationPerPerson();
|
|
||||||
if (project.getEstimatedDuration() != null && project.getEstimatedDuration() > 0) {
|
|
||||||
durationPerPerson = project.getEstimatedDuration();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自动拆分大组:如果人数过多,拆分成多个小组
|
|
||||||
// 优先使用项目配置的单位容纳人数(maxParticipants)
|
|
||||||
int maxPeoplePerGroup;
|
|
||||||
Integer projectMax = project.getMaxParticipants();
|
|
||||||
if (projectMax != null && projectMax > 0) {
|
|
||||||
// 使用项目配置的单位容纳人数
|
|
||||||
maxPeoplePerGroup = projectMax;
|
|
||||||
log.debug("项目 '{}' 使用自定义容纳人数: {}", projectName, projectMax);
|
|
||||||
} else if (members.size() <= scheduleConfig.getMaxPeoplePerGroup()) {
|
|
||||||
// 35人以内不拆分
|
|
||||||
maxPeoplePerGroup = members.size();
|
|
||||||
} else {
|
|
||||||
// 超过35人,按每组30-35人拆分
|
|
||||||
maxPeoplePerGroup = scheduleConfig.getTargetPeoplePerGroup();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (members.size() <= maxPeoplePerGroup) {
|
|
||||||
// 人数不多,不需要拆分
|
|
||||||
ScheduleGroupData group = new ScheduleGroupData();
|
|
||||||
group.setGroupName(projectName + " " + categoryName);
|
|
||||||
group.setProjectId(first.getProjectId());
|
|
||||||
group.setProjectType(1);
|
|
||||||
group.setMaxParticipants(maxPeoplePerGroup); // 设置项目单位容纳人数
|
|
||||||
group.setDisplayOrder(displayOrder++);
|
|
||||||
group.setCategory(categoryName); // 设置组别
|
|
||||||
group.setTotalParticipants(members.size());
|
|
||||||
group.setAthletes(members);
|
|
||||||
|
|
||||||
int duration = members.size() * durationPerPerson;
|
|
||||||
group.setEstimatedDuration(duration);
|
|
||||||
|
|
||||||
log.debug("个人项目 '{}': {}人, {}分钟/人, 总计{}分钟",
|
|
||||||
projectName + " " + categoryName, members.size(), durationPerPerson, duration);
|
|
||||||
|
|
||||||
groups.add(group);
|
|
||||||
} else {
|
|
||||||
// 人数太多,需要拆分成多个小组
|
|
||||||
int totalPeople = members.size();
|
|
||||||
int numSubGroups = (int) Math.ceil((double) totalPeople / maxPeoplePerGroup);
|
|
||||||
|
|
||||||
log.info("个人项目 '{}' 有{}人,将拆分成{}个小组(每组最多{}人)",
|
|
||||||
projectName + " " + categoryName, totalPeople, numSubGroups, maxPeoplePerGroup);
|
|
||||||
|
|
||||||
for (int i = 0; i < numSubGroups; i++) {
|
|
||||||
int startIdx = i * maxPeoplePerGroup;
|
|
||||||
int endIdx = Math.min(startIdx + maxPeoplePerGroup, totalPeople);
|
|
||||||
List<MartialAthlete> subGroupMembers = members.subList(startIdx, endIdx);
|
|
||||||
|
|
||||||
ScheduleGroupData group = new ScheduleGroupData();
|
|
||||||
group.setGroupName(projectName + " " + categoryName + " 第" + (i + 1) + "组");
|
|
||||||
group.setProjectId(first.getProjectId());
|
|
||||||
group.setProjectType(1);
|
|
||||||
group.setMaxParticipants(maxPeoplePerGroup); // 设置项目单位容纳人数
|
|
||||||
group.setDisplayOrder(displayOrder++);
|
|
||||||
group.setCategory(categoryName); // 设置组别
|
|
||||||
group.setTotalParticipants(subGroupMembers.size());
|
|
||||||
group.setAthletes(new ArrayList<>(subGroupMembers));
|
|
||||||
|
|
||||||
int duration = subGroupMembers.size() * durationPerPerson;
|
|
||||||
group.setEstimatedDuration(duration);
|
|
||||||
|
|
||||||
log.debug(" 拆分子组 '第{}组': {}人, 总计{}分钟",
|
|
||||||
i + 1, subGroupMembers.size(), duration);
|
|
||||||
|
|
||||||
groups.add(group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return groups;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证时间窗口容量是否足够容纳所有分组
|
* 验证时间窗口容量是否足够容纳所有分组
|
||||||
*/
|
*/
|
||||||
@@ -918,154 +689,4 @@ public class MartialScheduleArrangeServiceImpl implements IMartialScheduleArrang
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class ScheduleGroupData {
|
|
||||||
private String groupName;
|
|
||||||
private Long projectId;
|
|
||||||
private Integer projectType;
|
|
||||||
private Integer displayOrder;
|
|
||||||
private Integer totalParticipants;
|
|
||||||
private Integer totalTeams;
|
|
||||||
private Integer estimatedDuration;
|
|
||||||
private Integer maxParticipants; // 项目单位容纳人数
|
|
||||||
private String category; // 组别(男子/女子/混合)
|
|
||||||
private List<MartialAthlete> athletes;
|
|
||||||
|
|
||||||
// 分配结果
|
|
||||||
private Long assignedVenueId;
|
|
||||||
private String assignedVenueName;
|
|
||||||
private LocalDate assignedDate;
|
|
||||||
private String assignedTimePeriod;
|
|
||||||
private String assignedTimeSlot;
|
|
||||||
private Integer assignedTimeSlotIndex; // 时间段索引
|
|
||||||
|
|
||||||
// Getters and Setters
|
|
||||||
public String getGroupName() {
|
|
||||||
return groupName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setGroupName(String groupName) {
|
|
||||||
this.groupName = groupName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getProjectId() {
|
|
||||||
return projectId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProjectId(Long projectId) {
|
|
||||||
this.projectId = projectId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getProjectType() {
|
|
||||||
return projectType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProjectType(Integer projectType) {
|
|
||||||
this.projectType = projectType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getDisplayOrder() {
|
|
||||||
return displayOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDisplayOrder(Integer displayOrder) {
|
|
||||||
this.displayOrder = displayOrder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getTotalParticipants() {
|
|
||||||
return totalParticipants;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalParticipants(Integer totalParticipants) {
|
|
||||||
this.totalParticipants = totalParticipants;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getTotalTeams() {
|
|
||||||
return totalTeams;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTotalTeams(Integer totalTeams) {
|
|
||||||
this.totalTeams = totalTeams;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getEstimatedDuration() {
|
|
||||||
return estimatedDuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEstimatedDuration(Integer estimatedDuration) {
|
|
||||||
this.estimatedDuration = estimatedDuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getMaxParticipants() {
|
|
||||||
return maxParticipants;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMaxParticipants(Integer maxParticipants) {
|
|
||||||
this.maxParticipants = maxParticipants;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCategory() {
|
|
||||||
return category;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCategory(String category) {
|
|
||||||
this.category = category;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<MartialAthlete> getAthletes() {
|
|
||||||
return athletes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAthletes(List<MartialAthlete> athletes) {
|
|
||||||
this.athletes = athletes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getAssignedVenueId() {
|
|
||||||
return assignedVenueId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAssignedVenueId(Long assignedVenueId) {
|
|
||||||
this.assignedVenueId = assignedVenueId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAssignedVenueName() {
|
|
||||||
return assignedVenueName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAssignedVenueName(String assignedVenueName) {
|
|
||||||
this.assignedVenueName = assignedVenueName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDate getAssignedDate() {
|
|
||||||
return assignedDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAssignedDate(LocalDate assignedDate) {
|
|
||||||
this.assignedDate = assignedDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAssignedTimePeriod() {
|
|
||||||
return assignedTimePeriod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAssignedTimePeriod(String assignedTimePeriod) {
|
|
||||||
this.assignedTimePeriod = assignedTimePeriod;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAssignedTimeSlot() {
|
|
||||||
return assignedTimeSlot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAssignedTimeSlot(String assignedTimeSlot) {
|
|
||||||
this.assignedTimeSlot = assignedTimeSlot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Integer getAssignedTimeSlotIndex() {
|
|
||||||
return assignedTimeSlotIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAssignedTimeSlotIndex(Integer assignedTimeSlotIndex) {
|
|
||||||
this.assignedTimeSlotIndex = assignedTimeSlotIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+252
@@ -0,0 +1,252 @@
|
|||||||
|
package org.springblade.modules.martial.service.schedule.grouping;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springblade.modules.martial.config.ScheduleConfig;
|
||||||
|
import org.springblade.modules.martial.mapper.MartialProjectMapper;
|
||||||
|
import org.springblade.modules.martial.pojo.entity.MartialAthlete;
|
||||||
|
import org.springblade.modules.martial.pojo.entity.MartialProject;
|
||||||
|
import org.springblade.modules.martial.service.schedule.model.ScheduleGroupData;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Participant grouping service
|
||||||
|
* Groups athletes by project and category for schedule arrangement
|
||||||
|
*
|
||||||
|
* @author Refactored from MartialScheduleArrangeServiceImpl
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ParticipantGroupingService {
|
||||||
|
|
||||||
|
private final MartialProjectMapper projectMapper;
|
||||||
|
private final ScheduleConfig scheduleConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto group participants by project and category
|
||||||
|
*/
|
||||||
|
public List<ScheduleGroupData> autoGroup(List<MartialAthlete> athletes) {
|
||||||
|
List<ScheduleGroupData> groups = new ArrayList<>();
|
||||||
|
int displayOrder = 1;
|
||||||
|
|
||||||
|
Map<Long, MartialProject> projectMap = loadProjectMap(athletes);
|
||||||
|
List<MartialAthlete> teamAthletes = filterTeamAthletes(athletes, projectMap);
|
||||||
|
List<MartialAthlete> individualAthletes = filterIndividualAthletes(athletes, projectMap);
|
||||||
|
|
||||||
|
displayOrder = groupTeamAthletes(teamAthletes, projectMap, groups, displayOrder);
|
||||||
|
groupIndividualAthletes(individualAthletes, projectMap, groups, displayOrder);
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProjectCategoryName(MartialProject project) {
|
||||||
|
if (project == null || project.getCategory() == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
switch (project.getCategory()) {
|
||||||
|
case "1": return "男子";
|
||||||
|
case "2": return "女子";
|
||||||
|
case "3": return "混合";
|
||||||
|
case "4": return "团体";
|
||||||
|
default: return project.getCategory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MartialAthlete> filterByCategory(List<MartialAthlete> athletes, MartialProject project) {
|
||||||
|
if (project == null || project.getCategory() == null) {
|
||||||
|
return athletes;
|
||||||
|
}
|
||||||
|
switch (project.getCategory()) {
|
||||||
|
case "1":
|
||||||
|
return athletes.stream()
|
||||||
|
.filter(a -> a.getGender() == null || a.getGender() == 1)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
case "2":
|
||||||
|
return athletes.stream()
|
||||||
|
.filter(a -> a.getGender() != null && a.getGender() == 2)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
default:
|
||||||
|
return athletes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<Long, MartialProject> loadProjectMap(List<MartialAthlete> athletes) {
|
||||||
|
Set<Long> projectIds = athletes.stream()
|
||||||
|
.map(MartialAthlete::getProjectId)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
Map<Long, MartialProject> projectMap = new HashMap<>();
|
||||||
|
for (Long projectId : projectIds) {
|
||||||
|
MartialProject project = projectMapper.selectById(projectId);
|
||||||
|
if (project != null) {
|
||||||
|
projectMap.put(projectId, project);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return projectMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MartialAthlete> filterTeamAthletes(List<MartialAthlete> athletes, Map<Long, MartialProject> projectMap) {
|
||||||
|
return athletes.stream()
|
||||||
|
.filter(a -> {
|
||||||
|
MartialProject project = projectMap.get(a.getProjectId());
|
||||||
|
return project != null && (project.getType() == 2 || project.getType() == 3);
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MartialAthlete> filterIndividualAthletes(List<MartialAthlete> athletes, Map<Long, MartialProject> projectMap) {
|
||||||
|
return athletes.stream()
|
||||||
|
.filter(a -> {
|
||||||
|
MartialProject project = projectMap.get(a.getProjectId());
|
||||||
|
return project != null && project.getType() == 1;
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int groupTeamAthletes(List<MartialAthlete> teamAthletes, Map<Long, MartialProject> projectMap,
|
||||||
|
List<ScheduleGroupData> groups, int displayOrder) {
|
||||||
|
Map<String, List<MartialAthlete>> teamGroupMap = teamAthletes.stream()
|
||||||
|
.collect(Collectors.groupingBy(a -> String.valueOf(a.getProjectId())));
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<MartialAthlete>> entry : teamGroupMap.entrySet()) {
|
||||||
|
List<MartialAthlete> members = entry.getValue();
|
||||||
|
if (members.isEmpty()) continue;
|
||||||
|
|
||||||
|
MartialAthlete first = members.get(0);
|
||||||
|
MartialProject project = projectMap.get(first.getProjectId());
|
||||||
|
if (project == null) {
|
||||||
|
log.warn("Project not found, projectId: {}, skipping", first.getProjectId());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
members = filterByCategory(members, project);
|
||||||
|
if (members.isEmpty()) {
|
||||||
|
log.info("Project '{}' has no athletes after gender filter, skipping", project.getProjectName());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long teamCount = members.stream()
|
||||||
|
.map(MartialAthlete::getTeamName)
|
||||||
|
.filter(org -> org != null && !org.isEmpty())
|
||||||
|
.distinct()
|
||||||
|
.count();
|
||||||
|
|
||||||
|
String projectName = project.getProjectName();
|
||||||
|
String categoryName = getProjectCategoryName(project);
|
||||||
|
|
||||||
|
ScheduleGroupData group = ScheduleGroupData.builder()
|
||||||
|
.groupName(projectName + (categoryName.isEmpty() ? "" : " " + categoryName))
|
||||||
|
.projectId(first.getProjectId())
|
||||||
|
.projectType((project.getType() == 2 || project.getType() == 3) ? 2 : 1)
|
||||||
|
.maxParticipants(project.getMaxParticipants())
|
||||||
|
.category(categoryName)
|
||||||
|
.displayOrder(displayOrder++)
|
||||||
|
.totalParticipants(members.size())
|
||||||
|
.totalTeams((int) teamCount)
|
||||||
|
.athletes(members)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
int duration;
|
||||||
|
if (project.getEstimatedDuration() != null && project.getEstimatedDuration() > 0) {
|
||||||
|
duration = (int) teamCount * project.getEstimatedDuration();
|
||||||
|
} else {
|
||||||
|
duration = (int) teamCount * 5 + Math.max(0, (int) teamCount - 1) * 2;
|
||||||
|
}
|
||||||
|
group.setEstimatedDuration(duration);
|
||||||
|
groups.add(group);
|
||||||
|
}
|
||||||
|
return displayOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void groupIndividualAthletes(List<MartialAthlete> individualAthletes, Map<Long, MartialProject> projectMap,
|
||||||
|
List<ScheduleGroupData> groups, int displayOrder) {
|
||||||
|
Map<String, List<MartialAthlete>> individualGroupMap = individualAthletes.stream()
|
||||||
|
.collect(Collectors.groupingBy(a -> String.valueOf(a.getProjectId())));
|
||||||
|
|
||||||
|
for (Map.Entry<String, List<MartialAthlete>> entry : individualGroupMap.entrySet()) {
|
||||||
|
List<MartialAthlete> members = entry.getValue();
|
||||||
|
if (members.isEmpty()) continue;
|
||||||
|
|
||||||
|
MartialAthlete first = members.get(0);
|
||||||
|
MartialProject project = projectMap.get(first.getProjectId());
|
||||||
|
if (project == null) {
|
||||||
|
log.warn("Project not found, projectId: {}, skipping", first.getProjectId());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
members = filterByCategory(members, project);
|
||||||
|
if (members.isEmpty()) continue;
|
||||||
|
|
||||||
|
String projectName = project.getProjectName();
|
||||||
|
String categoryName = getProjectCategoryName(project);
|
||||||
|
int durationPerPerson = (project.getEstimatedDuration() != null && project.getEstimatedDuration() > 0)
|
||||||
|
? project.getEstimatedDuration() : scheduleConfig.getDefaultDurationPerPerson();
|
||||||
|
|
||||||
|
int maxPeoplePerGroup = calculateMaxPeoplePerGroup(project, members.size());
|
||||||
|
|
||||||
|
if (members.size() <= maxPeoplePerGroup) {
|
||||||
|
ScheduleGroupData group = ScheduleGroupData.builder()
|
||||||
|
.groupName(projectName + " " + categoryName)
|
||||||
|
.projectId(first.getProjectId())
|
||||||
|
.projectType(1)
|
||||||
|
.maxParticipants(maxPeoplePerGroup)
|
||||||
|
.displayOrder(displayOrder++)
|
||||||
|
.category(categoryName)
|
||||||
|
.totalParticipants(members.size())
|
||||||
|
.athletes(members)
|
||||||
|
.estimatedDuration(members.size() * durationPerPerson)
|
||||||
|
.build();
|
||||||
|
groups.add(group);
|
||||||
|
} else {
|
||||||
|
displayOrder = splitIntoSubGroups(members, project, projectName, categoryName,
|
||||||
|
maxPeoplePerGroup, durationPerPerson, groups, displayOrder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int calculateMaxPeoplePerGroup(MartialProject project, int memberCount) {
|
||||||
|
Integer projectMax = project.getMaxParticipants();
|
||||||
|
if (projectMax != null && projectMax > 0) {
|
||||||
|
return projectMax;
|
||||||
|
} else if (memberCount <= scheduleConfig.getMaxPeoplePerGroup()) {
|
||||||
|
return memberCount;
|
||||||
|
} else {
|
||||||
|
return scheduleConfig.getTargetPeoplePerGroup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int splitIntoSubGroups(List<MartialAthlete> members, MartialProject project,
|
||||||
|
String projectName, String categoryName, int maxPeoplePerGroup,
|
||||||
|
int durationPerPerson, List<ScheduleGroupData> groups, int displayOrder) {
|
||||||
|
int totalPeople = members.size();
|
||||||
|
int numSubGroups = (int) Math.ceil((double) totalPeople / maxPeoplePerGroup);
|
||||||
|
|
||||||
|
log.info("Individual project '{}' has {} people, splitting into {} groups (max {} per group)",
|
||||||
|
projectName + " " + categoryName, totalPeople, numSubGroups, maxPeoplePerGroup);
|
||||||
|
|
||||||
|
for (int i = 0; i < numSubGroups; i++) {
|
||||||
|
int startIdx = i * maxPeoplePerGroup;
|
||||||
|
int endIdx = Math.min(startIdx + maxPeoplePerGroup, totalPeople);
|
||||||
|
List<MartialAthlete> subGroupMembers = new ArrayList<>(members.subList(startIdx, endIdx));
|
||||||
|
|
||||||
|
ScheduleGroupData group = ScheduleGroupData.builder()
|
||||||
|
.groupName(projectName + " " + categoryName + " 第" + (i + 1) + "组")
|
||||||
|
.projectId(project.getId())
|
||||||
|
.projectType(1)
|
||||||
|
.maxParticipants(maxPeoplePerGroup)
|
||||||
|
.displayOrder(displayOrder++)
|
||||||
|
.category(categoryName)
|
||||||
|
.totalParticipants(subGroupMembers.size())
|
||||||
|
.athletes(subGroupMembers)
|
||||||
|
.estimatedDuration(subGroupMembers.size() * durationPerPerson)
|
||||||
|
.build();
|
||||||
|
groups.add(group);
|
||||||
|
}
|
||||||
|
return displayOrder;
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package org.springblade.modules.martial.service.schedule.model;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import org.springblade.modules.martial.pojo.entity.MartialAthlete;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule group data model
|
||||||
|
* Represents a group of participants for schedule arrangement
|
||||||
|
*
|
||||||
|
* @author Refactored from MartialScheduleArrangeServiceImpl
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class ScheduleGroupData {
|
||||||
|
|
||||||
|
private String groupName;
|
||||||
|
private Long projectId;
|
||||||
|
private Integer projectType;
|
||||||
|
private Integer displayOrder;
|
||||||
|
private Integer totalParticipants;
|
||||||
|
private Integer totalTeams;
|
||||||
|
private Integer estimatedDuration;
|
||||||
|
private Integer maxParticipants;
|
||||||
|
private String category;
|
||||||
|
private List<MartialAthlete> athletes;
|
||||||
|
|
||||||
|
// Assignment results
|
||||||
|
private Long assignedVenueId;
|
||||||
|
private String assignedVenueName;
|
||||||
|
private LocalDate assignedDate;
|
||||||
|
private String assignedTimePeriod;
|
||||||
|
private String assignedTimeSlot;
|
||||||
|
private Integer assignedTimeSlotIndex;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user