diff --git a/eladmin-system/src/main/resources/db/migration/V14__Add_Average_Score_To_Team.sql b/eladmin-system/src/main/resources/db/migration/V14__Add_Average_Score_To_Team.sql new file mode 100644 index 00000000..1b012acc --- /dev/null +++ b/eladmin-system/src/main/resources/db/migration/V14__Add_Average_Score_To_Team.sql @@ -0,0 +1,2 @@ +-- Add average_score column to team table +ALTER TABLE team ADD COLUMN average_score DOUBLE DEFAULT 0.0; diff --git a/sport/src/main/java/com/srr/domain/Event.java b/sport/src/main/java/com/srr/domain/Event.java index 57ff4a7d..bb341a19 100644 --- a/sport/src/main/java/com/srr/domain/Event.java +++ b/sport/src/main/java/com/srr/domain/Event.java @@ -159,6 +159,12 @@ public class Event implements Serializable { inverseJoinColumns = {@JoinColumn(name = "player_id",referencedColumnName = "id")}) private List coHostPlayers = new ArrayList<>(); + @OneToMany(mappedBy = "event") + private List matchGroups = new ArrayList<>(); + + @OneToMany(mappedBy = "event") + private List teams = new ArrayList<>(); + public void copy(Event source){ BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true)); } diff --git a/sport/src/main/java/com/srr/domain/Team.java b/sport/src/main/java/com/srr/domain/Team.java index 488fd593..6a915dcd 100644 --- a/sport/src/main/java/com/srr/domain/Team.java +++ b/sport/src/main/java/com/srr/domain/Team.java @@ -35,6 +35,10 @@ public class Team implements Serializable { @Column(name = "team_size") @ApiModelProperty(value = "Team size") private int teamSize; + + @Column(name = "average_score") + @ApiModelProperty(value = "Average team score based on player scores") + private Double averageScore = 0.0; @OneToMany(mappedBy = "team") @ApiModelProperty(value = "teamPlayers") diff --git a/sport/src/main/java/com/srr/domain/TeamPlayer.java b/sport/src/main/java/com/srr/domain/TeamPlayer.java index 88c6d1d7..61acb544 100644 --- a/sport/src/main/java/com/srr/domain/TeamPlayer.java +++ b/sport/src/main/java/com/srr/domain/TeamPlayer.java @@ -6,6 +6,7 @@ import lombok.Setter; import javax.persistence.*; import java.io.Serializable; +import java.util.List; @Getter @Setter @@ -31,4 +32,37 @@ public class TeamPlayer implements Serializable { @Column(name = "is_checked_in") private boolean isCheckedIn; + + @PrePersist + @PreUpdate + public void updateTeamScore() { + if (team != null) { + calculateAndUpdateTeamScore(team); + } + } + + /** + * Calculates and updates the average score for the team + * @param team The team to update the score for + */ + private void calculateAndUpdateTeamScore(Team team) { + List players = team.getTeamPlayers(); + if (players == null || players.isEmpty()) { + team.setAverageScore(0.0); + return; + } + + double totalScore = 0; + int playerCount = 0; + + for (TeamPlayer player : players) { + if (player.getScore() != null) { + totalScore += player.getScore(); + playerCount++; + } + } + + double averageScore = playerCount > 0 ? totalScore / playerCount : 0; + team.setAverageScore(averageScore); + } } diff --git a/sport/src/main/java/com/srr/dto/MatchGroupGenerationDto.java b/sport/src/main/java/com/srr/dto/MatchGroupGenerationDto.java new file mode 100644 index 00000000..830bb7f7 --- /dev/null +++ b/sport/src/main/java/com/srr/dto/MatchGroupGenerationDto.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2025 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.srr.dto; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * @author Chanheng + * @website https://eladmin.vip + * @date 2025-05-26 + **/ +@Data +public class MatchGroupGenerationDto { + + @NotNull + @ApiModelProperty(value = "Event ID") + private Long eventId; +} diff --git a/sport/src/main/java/com/srr/dto/TeamDto.java b/sport/src/main/java/com/srr/dto/TeamDto.java index b3850122..ded4f069 100644 --- a/sport/src/main/java/com/srr/dto/TeamDto.java +++ b/sport/src/main/java/com/srr/dto/TeamDto.java @@ -44,6 +44,9 @@ public class TeamDto implements Serializable { @ApiModelProperty(value = "Team size") private Integer teamSize; + + @ApiModelProperty(value = "Average team score based on player scores") + private Double averageScore; @ApiModelProperty(value = "Team players") private List teamPlayers; diff --git a/sport/src/main/java/com/srr/rest/EventController.java b/sport/src/main/java/com/srr/rest/EventController.java index 7b7219ce..c4a9f89e 100644 --- a/sport/src/main/java/com/srr/rest/EventController.java +++ b/sport/src/main/java/com/srr/rest/EventController.java @@ -19,9 +19,11 @@ import com.srr.domain.Event; import com.srr.dto.EventDto; import com.srr.dto.EventQueryCriteria; import com.srr.dto.JoinEventDto; +import com.srr.dto.MatchGroupGenerationDto; import com.srr.dto.TeamPlayerDto; import com.srr.enumeration.EventStatus; import com.srr.service.EventService; +import com.srr.service.MatchGroupService; import com.srr.service.TeamPlayerService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -38,7 +40,9 @@ import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * @author Chanheng @@ -53,6 +57,7 @@ public class EventController { private final EventService eventService; private final TeamPlayerService teamPlayerService; + private final MatchGroupService matchGroupService; @ApiOperation("Export Data") @GetMapping(value = "/download") @@ -114,6 +119,20 @@ public class EventController { public ResponseEntity> findEventPlayers(@PathVariable("id") Long eventId) { return new ResponseEntity<>(teamPlayerService.findByEventId(eventId), HttpStatus.OK); } + + @PostMapping("/generate-groups") + @Log("Generate match groups") + @ApiOperation("Generate match groups based on team scores") + @PreAuthorize("@el.check('event:admin')") + public ResponseEntity generateMatchGroups(@Validated @RequestBody MatchGroupGenerationDto dto) { + Integer groupsCreated = matchGroupService.generateMatchGroups(dto); + + Map result = new HashMap<>(); + result.put("groupsCreated", groupsCreated); + result.put("message", "Successfully created " + groupsCreated + " match groups based on team scores"); + + return new ResponseEntity<>(result, HttpStatus.OK); + } @DeleteMapping @Log("Delete event") diff --git a/sport/src/main/java/com/srr/service/MatchGroupService.java b/sport/src/main/java/com/srr/service/MatchGroupService.java new file mode 100644 index 00000000..0402c1bf --- /dev/null +++ b/sport/src/main/java/com/srr/service/MatchGroupService.java @@ -0,0 +1,34 @@ +/* + * Copyright 2019-2025 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.srr.service; + +import com.srr.dto.MatchGroupGenerationDto; +import java.util.List; + +/** + * @author Chanheng + * @website https://eladmin.vip + * @date 2025-05-26 + **/ +public interface MatchGroupService { + + /** + * Generate match groups for teams in an event based on score similarity + * @param dto parameters for group generation + * @return Number of groups created + */ + Integer generateMatchGroups(MatchGroupGenerationDto dto); +} diff --git a/sport/src/main/java/com/srr/service/impl/MatchGroupServiceImpl.java b/sport/src/main/java/com/srr/service/impl/MatchGroupServiceImpl.java new file mode 100644 index 00000000..4bc3831c --- /dev/null +++ b/sport/src/main/java/com/srr/service/impl/MatchGroupServiceImpl.java @@ -0,0 +1,146 @@ +/* + * Copyright 2019-2025 Zheng Jie + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.srr.service.impl; + +import com.srr.domain.Event; +import com.srr.domain.MatchGroup; +import com.srr.domain.Team; +import com.srr.dto.MatchGroupGenerationDto; +import com.srr.repository.EventRepository; +import com.srr.repository.MatchGroupRepository; +import com.srr.repository.TeamRepository; +import com.srr.service.MatchGroupService; +import lombok.RequiredArgsConstructor; +import me.zhengjie.exception.BadRequestException; +import me.zhengjie.exception.EntityNotFoundException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Chanheng + * @website https://eladmin.vip + * @date 2025-05-26 + **/ +@Service +@RequiredArgsConstructor +public class MatchGroupServiceImpl implements MatchGroupService { + + private final EventRepository eventRepository; + private final TeamRepository teamRepository; + private final MatchGroupRepository matchGroupRepository; + + @Override + @Transactional + public Integer generateMatchGroups(MatchGroupGenerationDto dto) { + // Find the event + Event event = eventRepository.findById(dto.getEventId()) + .orElseThrow(() -> new EntityNotFoundException(Event.class, "id", dto.getEventId().toString())); + + // Check if the event has a group count + if (event.getGroupCount() == null || event.getGroupCount() <= 0) { + throw new BadRequestException("Event has no valid group count defined"); + } + + // Get all teams for the event + List teams = event.getTeams(); + + if (teams.isEmpty()) { + throw new BadRequestException("No teams found for event with ID: " + dto.getEventId()); + } + + // Clear existing match groups for this event + List existingGroups = event.getMatchGroups(); + if (existingGroups != null && !existingGroups.isEmpty()) { + for (MatchGroup group : existingGroups) { + // Detach teams from groups + for (Team team : group.getTeams()) { + team.setMatchGroup(null); + } + matchGroupRepository.delete(group); + } + } + + // Sort teams by their average score (which is now stored on the Team entity) + List sortedTeams = teams.stream() + .sorted(Comparator.comparing(Team::getAverageScore, Comparator.nullsLast(Comparator.naturalOrder()))) + .collect(Collectors.toList()); + + // Group teams based on their sorted order and the target group count + int targetGroupCount = event.getGroupCount(); + List> teamGroups = createTeamGroups(sortedTeams, targetGroupCount); + + // Create match groups + int groupCount = 0; + for (List teamGroup : teamGroups) { + if (!teamGroup.isEmpty()) { + createMatchGroup(event, teamGroup, "Group " + (++groupCount), teamGroup.size()); + } + } + + return groupCount; + } + + /** + * Group teams based strictly on their score order + */ + private List> createTeamGroups(List sortedTeams, int targetGroupCount) { + int totalTeams = sortedTeams.size(); + + // Don't create more groups than we have teams + int actualGroupCount = Math.min(targetGroupCount, totalTeams); + + // Initialize the groups + List> groups = new ArrayList<>(actualGroupCount); + for (int i = 0; i < actualGroupCount; i++) { + groups.add(new ArrayList<>()); + } + + // Distribute teams to groups in a round-robin fashion based on their sorted order + // Teams with similar scores will naturally be placed in different groups + for (int i = 0; i < sortedTeams.size(); i++) { + Team team = sortedTeams.get(i); + // Use modulo to distribute teams evenly among groups + int groupIndex = i % actualGroupCount; + groups.get(groupIndex).add(team); + } + + return groups; + } + + /** + * Create a match group from a list of teams + */ + private void createMatchGroup(Event event, List teams, String name, int groupTeamSize) { + MatchGroup matchGroup = new MatchGroup(); + matchGroup.setName(name); + matchGroup.setEvent(event); + matchGroup.setGroupTeamSize(groupTeamSize); + + // Save the match group first + matchGroup = matchGroupRepository.save(matchGroup); + + // Update the teams with the match group + for (Team team : teams) { + team.setMatchGroup(matchGroup); + teamRepository.save(team); + } + } +}