Merge pull request #11 from sports-match/team-player

team-player
pull/882/head
Chanheng 2025-05-26 18:33:41 -07:00 committed by GitHub
commit 4b4510bfa6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 584 additions and 0 deletions

View File

@ -0,0 +1,2 @@
-- Add average_score column to team table
ALTER TABLE team ADD COLUMN average_score DOUBLE DEFAULT 0.0;

View File

@ -159,6 +159,12 @@ public class Event implements Serializable {
inverseJoinColumns = {@JoinColumn(name = "player_id",referencedColumnName = "id")}) inverseJoinColumns = {@JoinColumn(name = "player_id",referencedColumnName = "id")})
private List<Player> coHostPlayers = new ArrayList<>(); private List<Player> coHostPlayers = new ArrayList<>();
@OneToMany(mappedBy = "event")
private List<MatchGroup> matchGroups = new ArrayList<>();
@OneToMany(mappedBy = "event")
private List<Team> teams = new ArrayList<>();
public void copy(Event source){ public void copy(Event source){
BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true)); BeanUtil.copyProperties(source,this, CopyOptions.create().setIgnoreNullValue(true));
} }

View File

@ -35,6 +35,10 @@ public class Team implements Serializable {
@Column(name = "team_size") @Column(name = "team_size")
@ApiModelProperty(value = "Team size") @ApiModelProperty(value = "Team size")
private int teamSize; private int teamSize;
@Column(name = "average_score")
@ApiModelProperty(value = "Average team score based on player scores")
private Double averageScore = 0.0;
@OneToMany(mappedBy = "team") @OneToMany(mappedBy = "team")
@ApiModelProperty(value = "teamPlayers") @ApiModelProperty(value = "teamPlayers")

View File

@ -6,6 +6,7 @@ import lombok.Setter;
import javax.persistence.*; import javax.persistence.*;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
@Getter @Getter
@Setter @Setter
@ -31,4 +32,37 @@ public class TeamPlayer implements Serializable {
@Column(name = "is_checked_in") @Column(name = "is_checked_in")
private boolean isCheckedIn; 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<TeamPlayer> 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);
}
} }

View File

@ -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;
}

View File

@ -44,6 +44,9 @@ public class TeamDto implements Serializable {
@ApiModelProperty(value = "Team size") @ApiModelProperty(value = "Team size")
private Integer teamSize; private Integer teamSize;
@ApiModelProperty(value = "Average team score based on player scores")
private Double averageScore;
@ApiModelProperty(value = "Team players") @ApiModelProperty(value = "Team players")
private List<TeamPlayerDto> teamPlayers; private List<TeamPlayerDto> teamPlayers;

View File

@ -0,0 +1,38 @@
/*
* 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 TeamPlayerReassignDto {
@NotNull
@ApiModelProperty(value = "Team player ID to reassign")
private Long teamPlayerId;
@NotNull
@ApiModelProperty(value = "Target team ID")
private Long targetTeamId;
}

View File

@ -18,6 +18,10 @@ package com.srr.repository;
import com.srr.domain.TeamPlayer; import com.srr.domain.TeamPlayer;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
/** /**
* @website https://eladmin.vip * @website https://eladmin.vip
@ -26,4 +30,9 @@ import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
**/ **/
public interface TeamPlayerRepository extends JpaRepository<TeamPlayer, Long>, JpaSpecificationExecutor<TeamPlayer> { public interface TeamPlayerRepository extends JpaRepository<TeamPlayer, Long>, JpaSpecificationExecutor<TeamPlayer> {
boolean existsByTeamIdAndPlayerId(Long teamId, Long playerId); boolean existsByTeamIdAndPlayerId(Long teamId, Long playerId);
TeamPlayer findByTeamIdAndPlayerId(Long teamId, Long playerId);
@Query("SELECT tp FROM TeamPlayer tp JOIN tp.team t JOIN t.event e WHERE e.id = :eventId")
List<TeamPlayer> findByEventId(@Param("eventId") Long eventId);
} }

View File

@ -19,8 +19,12 @@ import com.srr.domain.Event;
import com.srr.dto.EventDto; import com.srr.dto.EventDto;
import com.srr.dto.EventQueryCriteria; import com.srr.dto.EventQueryCriteria;
import com.srr.dto.JoinEventDto; import com.srr.dto.JoinEventDto;
import com.srr.dto.MatchGroupGenerationDto;
import com.srr.dto.TeamPlayerDto;
import com.srr.enumeration.EventStatus; import com.srr.enumeration.EventStatus;
import com.srr.service.EventService; import com.srr.service.EventService;
import com.srr.service.MatchGroupService;
import com.srr.service.TeamPlayerService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiParam;
@ -36,6 +40,9 @@ import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** /**
* @author Chanheng * @author Chanheng
@ -49,6 +56,8 @@ import java.io.IOException;
public class EventController { public class EventController {
private final EventService eventService; private final EventService eventService;
private final TeamPlayerService teamPlayerService;
private final MatchGroupService matchGroupService;
@ApiOperation("Export Data") @ApiOperation("Export Data")
@GetMapping(value = "/download") @GetMapping(value = "/download")
@ -104,6 +113,27 @@ public class EventController {
return new ResponseEntity<>(result, HttpStatus.OK); return new ResponseEntity<>(result, HttpStatus.OK);
} }
@GetMapping("/{id}/players")
@ApiOperation("Find all team players in an event")
@PreAuthorize("@el.check('event:list')")
public ResponseEntity<List<TeamPlayerDto>> 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<Object> generateMatchGroups(@Validated @RequestBody MatchGroupGenerationDto dto) {
Integer groupsCreated = matchGroupService.generateMatchGroups(dto);
Map<String, Object> 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 @DeleteMapping
@Log("Delete event") @Log("Delete event")
@ApiOperation("Delete event") @ApiOperation("Delete event")

View File

@ -0,0 +1,66 @@
/*
* 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.rest;
import com.srr.dto.TeamPlayerDto;
import com.srr.dto.TeamPlayerReassignDto;
import com.srr.service.TeamPlayerService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import me.zhengjie.annotation.Log;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* @author Chanheng
* @website https://eladmin.vip
* @date 2025-05-26
**/
@RestController
@RequiredArgsConstructor
@Api(tags = "Team Player Management")
@RequestMapping("/api/team-player")
public class TeamPlayerController {
private final TeamPlayerService teamPlayerService;
@GetMapping("/{id}")
@ApiOperation("Get team player details")
@PreAuthorize("@el.check('event:list')")
public ResponseEntity<TeamPlayerDto> getTeamPlayer(@PathVariable Long id) {
return new ResponseEntity<>(teamPlayerService.findById(id), HttpStatus.OK);
}
@PutMapping("/{id}/check-in")
@Log("Check in player")
@ApiOperation("Check in player for an event")
@PreAuthorize("@el.check('event:edit')")
public ResponseEntity<TeamPlayerDto> checkIn(@PathVariable Long id) {
return new ResponseEntity<>(teamPlayerService.checkIn(id), HttpStatus.OK);
}
@PostMapping("/reassign")
@Log("Reassign player to another team")
@ApiOperation("Reassign player to another team")
@PreAuthorize("@el.check('event:admin')")
public ResponseEntity<TeamPlayerDto> reassignPlayer(@Validated @RequestBody TeamPlayerReassignDto dto) {
return new ResponseEntity<>(teamPlayerService.reassignPlayer(dto), HttpStatus.OK);
}
}

View File

@ -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);
}

View File

@ -0,0 +1,61 @@
/*
* 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.domain.TeamPlayer;
import com.srr.dto.TeamPlayerDto;
import com.srr.dto.TeamPlayerReassignDto;
import org.springframework.data.domain.Pageable;
import me.zhengjie.utils.PageResult;
import java.util.List;
/**
* @author Chanheng
* @website https://eladmin.vip
* @description Service interface for TeamPlayer
* @date 2025-05-26
**/
public interface TeamPlayerService {
/**
* Get a specific TeamPlayer by ID
* @param id TeamPlayer ID
* @return TeamPlayerDto
*/
TeamPlayerDto findById(Long id);
/**
* Check in a player for an event
* @param id TeamPlayer ID
* @return The updated TeamPlayerDto
*/
TeamPlayerDto checkIn(Long id);
/**
* Find all TeamPlayers by event ID
* @param eventId the event ID
* @return List of TeamPlayerDto objects
*/
List<TeamPlayerDto> findByEventId(Long eventId);
/**
* Reassign a player from one team to another
* @param dto Contains teamPlayerId and targetTeamId
* @return The updated TeamPlayerDto
*/
TeamPlayerDto reassignPlayer(TeamPlayerReassignDto dto);
}

View File

@ -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<Team> 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<MatchGroup> 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<Team> 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<List<Team>> teamGroups = createTeamGroups(sortedTeams, targetGroupCount);
// Create match groups
int groupCount = 0;
for (List<Team> teamGroup : teamGroups) {
if (!teamGroup.isEmpty()) {
createMatchGroup(event, teamGroup, "Group " + (++groupCount), teamGroup.size());
}
}
return groupCount;
}
/**
* Group teams based strictly on their score order
*/
private List<List<Team>> createTeamGroups(List<Team> 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<List<Team>> 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<Team> 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);
}
}
}

View File

@ -0,0 +1,117 @@
/*
* 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.Team;
import com.srr.domain.TeamPlayer;
import com.srr.dto.TeamPlayerDto;
import com.srr.dto.TeamPlayerReassignDto;
import com.srr.dto.mapstruct.TeamPlayerMapper;
import com.srr.repository.TeamPlayerRepository;
import com.srr.repository.TeamRepository;
import com.srr.service.TeamPlayerService;
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.List;
import java.util.stream.Collectors;
/**
* @author Chanheng
* @website https://eladmin.vip
* @date 2025-05-26
**/
@Service
@RequiredArgsConstructor
public class TeamPlayerServiceImpl implements TeamPlayerService {
private final TeamPlayerRepository teamPlayerRepository;
private final TeamRepository teamRepository;
private final TeamPlayerMapper teamPlayerMapper;
@Override
@Transactional(readOnly = true)
public TeamPlayerDto findById(Long id) {
TeamPlayer teamPlayer = teamPlayerRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(TeamPlayer.class, "id", id.toString()));
return teamPlayerMapper.toDto(teamPlayer);
}
@Override
@Transactional
public TeamPlayerDto checkIn(Long id) {
TeamPlayer teamPlayer = teamPlayerRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException(TeamPlayer.class, "id", id.toString()));
if (teamPlayer.isCheckedIn()) {
throw new BadRequestException("Player is already checked in");
}
teamPlayer.setCheckedIn(true);
teamPlayerRepository.save(teamPlayer);
return teamPlayerMapper.toDto(teamPlayer);
}
@Override
@Transactional(readOnly = true)
public List<TeamPlayerDto> findByEventId(Long eventId) {
List<TeamPlayer> teamPlayers = teamPlayerRepository.findByEventId(eventId);
return teamPlayers.stream()
.map(teamPlayerMapper::toDto)
.collect(Collectors.toList());
}
@Override
@Transactional
public TeamPlayerDto reassignPlayer(TeamPlayerReassignDto dto) {
// Find the team player to reassign
TeamPlayer teamPlayer = teamPlayerRepository.findById(dto.getTeamPlayerId())
.orElseThrow(() -> new EntityNotFoundException(TeamPlayer.class, "id", dto.getTeamPlayerId().toString()));
// Find the target team
Team targetTeam = teamRepository.findById(dto.getTargetTeamId())
.orElseThrow(() -> new EntityNotFoundException(Team.class, "id", dto.getTargetTeamId().toString()));
// Store the original team for potential deletion
Team originalTeam = teamPlayer.getTeam();
// Check if target team is in the same event as the original team
if (!originalTeam.getEvent().getId().equals(targetTeam.getEvent().getId())) {
throw new BadRequestException("Cannot reassign player to a team in a different event");
}
// Check if the target team is full
if (targetTeam.getTeamPlayers().size() >= targetTeam.getTeamSize()) {
throw new BadRequestException("Target team is already full");
}
// Reassign the player to the new team
teamPlayer.setTeam(targetTeam);
teamPlayerRepository.save(teamPlayer);
// Check if original team is now empty, and delete if it is
if (originalTeam.getTeamPlayers().stream()
.noneMatch(tp -> !tp.getId().equals(teamPlayer.getId()))) {
teamRepository.delete(originalTeam);
}
return teamPlayerMapper.toDto(teamPlayer);
}
}