Adds team and hunter leaderboard endpoints
This commit is contained in:
@@ -40,8 +40,3 @@ All endpoints except `/signup` and `/login` require a JWT bearer token.
|
||||
3. Include the access token in requests: `Authorization: Bearer <token>`
|
||||
4. Refresh an expired token: `POST /refresh`
|
||||
5. Log out: `POST /logout`
|
||||
|
||||
## TODO
|
||||
|
||||
- `GET /lead/hunt/{huntId}/team` — leaderboard: teams with scores for a hunt
|
||||
- `GET /lead/hunt/{huntId}/hunter` — leaderboard: hunters with scores for a hunt
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package net.halfbinary.scavengerhuntapi.controller
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation
|
||||
import net.halfbinary.scavengerhuntapi.model.HuntId
|
||||
import net.halfbinary.scavengerhuntapi.model.converter.toResponse
|
||||
import net.halfbinary.scavengerhuntapi.model.response.HunterLeaderboardResponse
|
||||
import net.halfbinary.scavengerhuntapi.model.response.TeamLeaderboardResponse
|
||||
import net.halfbinary.scavengerhuntapi.service.StatsService
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("stats/lead/hunt/{huntId}")
|
||||
class StatsController(private val statsService: StatsService) {
|
||||
|
||||
@GetMapping("/team")
|
||||
@Operation(summary = "Ranked teams with current total scores for a hunt")
|
||||
fun getTeamLeaderboard(@PathVariable huntId: HuntId): ResponseEntity<List<TeamLeaderboardResponse>> {
|
||||
return ResponseEntity.ok(statsService.getTeamLeaderboard(huntId).map { it.toResponse() })
|
||||
}
|
||||
|
||||
@GetMapping("/hunter")
|
||||
@Operation(summary = "Ranked hunters with current total scores for a hunt")
|
||||
fun getHunterLeaderboard(@PathVariable huntId: HuntId): ResponseEntity<List<HunterLeaderboardResponse>> {
|
||||
return ResponseEntity.ok(statsService.getHunterLeaderboard(huntId).map { it.toResponse() })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package net.halfbinary.scavengerhuntapi.model.converter
|
||||
|
||||
import net.halfbinary.scavengerhuntapi.model.domain.HunterLeaderboardEntry
|
||||
import net.halfbinary.scavengerhuntapi.model.response.HunterLeaderboardResponse
|
||||
|
||||
fun HunterLeaderboardEntry.toResponse() = HunterLeaderboardResponse(
|
||||
rank = rank,
|
||||
hunterName = hunterName,
|
||||
score = score
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package net.halfbinary.scavengerhuntapi.model.converter
|
||||
|
||||
import net.halfbinary.scavengerhuntapi.model.domain.TeamLeaderboardEntry
|
||||
import net.halfbinary.scavengerhuntapi.model.response.TeamLeaderboardResponse
|
||||
|
||||
fun TeamLeaderboardEntry.toResponse() = TeamLeaderboardResponse(
|
||||
rank = rank,
|
||||
teamName = teamName,
|
||||
score = score
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package net.halfbinary.scavengerhuntapi.model.domain
|
||||
|
||||
data class HunterLeaderboardEntry(
|
||||
val rank: Int,
|
||||
val hunterName: String,
|
||||
val score: Int
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package net.halfbinary.scavengerhuntapi.model.domain
|
||||
|
||||
data class TeamLeaderboardEntry(
|
||||
val rank: Int,
|
||||
val teamName: String,
|
||||
val score: Int
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package net.halfbinary.scavengerhuntapi.model.response
|
||||
|
||||
data class HunterLeaderboardResponse(
|
||||
val rank: Int,
|
||||
val hunterName: String,
|
||||
val score: Int
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package net.halfbinary.scavengerhuntapi.model.response
|
||||
|
||||
data class TeamLeaderboardResponse(
|
||||
val rank: Int,
|
||||
val teamName: String,
|
||||
val score: Int
|
||||
)
|
||||
@@ -3,6 +3,7 @@ package net.halfbinary.scavengerhuntapi.repository
|
||||
import net.halfbinary.scavengerhuntapi.model.HuntId
|
||||
import net.halfbinary.scavengerhuntapi.model.ItemId
|
||||
import net.halfbinary.scavengerhuntapi.model.PhotoId
|
||||
import net.halfbinary.scavengerhuntapi.model.PhotoStatus
|
||||
import net.halfbinary.scavengerhuntapi.model.record.PhotoRecord
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
@@ -11,4 +12,5 @@ interface PhotoRepository : JpaRepository<PhotoRecord, PhotoId> {
|
||||
fun findByItemId(itemId: ItemId): List<PhotoRecord>
|
||||
fun findByHuntIdAndItemId(huntId: HuntId, itemId: ItemId): List<PhotoRecord>
|
||||
fun findByIdAndItemIdAndHuntId(id: PhotoId, itemId: ItemId, huntId: HuntId): PhotoRecord?
|
||||
fun findByHuntIdAndStatus(huntId: HuntId, status: PhotoStatus): List<PhotoRecord>
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package net.halfbinary.scavengerhuntapi.service
|
||||
|
||||
import net.halfbinary.scavengerhuntapi.model.HuntId
|
||||
import net.halfbinary.scavengerhuntapi.model.PhotoStatus
|
||||
import net.halfbinary.scavengerhuntapi.model.domain.HunterLeaderboardEntry
|
||||
import net.halfbinary.scavengerhuntapi.model.domain.TeamLeaderboardEntry
|
||||
import net.halfbinary.scavengerhuntapi.repository.ItemRepository
|
||||
import net.halfbinary.scavengerhuntapi.repository.PhotoRepository
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
class StatsService(
|
||||
private val photoRepository: PhotoRepository,
|
||||
private val itemRepository: ItemRepository,
|
||||
private val teamService: TeamService,
|
||||
private val hunterService: HunterService
|
||||
) {
|
||||
fun getTeamLeaderboard(huntId: HuntId): List<TeamLeaderboardEntry> {
|
||||
val approvedPhotos = photoRepository.findByHuntIdAndStatus(huntId, PhotoStatus.APPROVED)
|
||||
val itemPoints = itemRepository.findAllByHuntId(huntId).associate { it.id to it.points }
|
||||
val teams = teamService.getListOfTeamsForHunt(huntId)
|
||||
|
||||
val sortedScores = teams.map { team ->
|
||||
val teamHunterIds = teamService.getHunterIdsForTeam(team.id)
|
||||
val score = approvedPhotos
|
||||
.filter { it.hunterId in teamHunterIds }
|
||||
.distinctBy { it.itemId }
|
||||
.sumOf { itemPoints[it.itemId] ?: 0 }
|
||||
team to score
|
||||
}.sortedByDescending { (_, score) -> score }
|
||||
|
||||
var rank = 1
|
||||
return sortedScores.mapIndexed { index, (team, score) ->
|
||||
if (index > 0 && sortedScores[index - 1].second != score) rank = index + 1
|
||||
TeamLeaderboardEntry(rank = rank, teamName = team.name, score = score)
|
||||
}
|
||||
}
|
||||
|
||||
fun getHunterLeaderboard(huntId: HuntId): List<HunterLeaderboardEntry> {
|
||||
val approvedPhotos = photoRepository.findByHuntIdAndStatus(huntId, PhotoStatus.APPROVED)
|
||||
val itemPoints = itemRepository.findAllByHuntId(huntId).associate { it.id to it.points }
|
||||
|
||||
val sortedScores = approvedPhotos
|
||||
.groupBy { it.hunterId }
|
||||
.map { (hunterId, photos) ->
|
||||
val score = photos.distinctBy { it.itemId }.sumOf { itemPoints[it.itemId] ?: 0 }
|
||||
hunterId to score
|
||||
}
|
||||
.sortedByDescending { (_, score) -> score }
|
||||
|
||||
var rank = 1
|
||||
return sortedScores.mapIndexed { index, (hunterId, score) ->
|
||||
if (index > 0 && sortedScores[index - 1].second != score) rank = index + 1
|
||||
val hunter = hunterService.getHunterById(hunterId)
|
||||
HunterLeaderboardEntry(rank = rank, hunterName = hunter.name, score = score)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user