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>`
|
3. Include the access token in requests: `Authorization: Bearer <token>`
|
||||||
4. Refresh an expired token: `POST /refresh`
|
4. Refresh an expired token: `POST /refresh`
|
||||||
5. Log out: `POST /logout`
|
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.HuntId
|
||||||
import net.halfbinary.scavengerhuntapi.model.ItemId
|
import net.halfbinary.scavengerhuntapi.model.ItemId
|
||||||
import net.halfbinary.scavengerhuntapi.model.PhotoId
|
import net.halfbinary.scavengerhuntapi.model.PhotoId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.PhotoStatus
|
||||||
import net.halfbinary.scavengerhuntapi.model.record.PhotoRecord
|
import net.halfbinary.scavengerhuntapi.model.record.PhotoRecord
|
||||||
import org.springframework.data.jpa.repository.JpaRepository
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
import org.springframework.stereotype.Repository
|
import org.springframework.stereotype.Repository
|
||||||
@@ -11,4 +12,5 @@ interface PhotoRepository : JpaRepository<PhotoRecord, PhotoId> {
|
|||||||
fun findByItemId(itemId: ItemId): List<PhotoRecord>
|
fun findByItemId(itemId: ItemId): List<PhotoRecord>
|
||||||
fun findByHuntIdAndItemId(huntId: HuntId, itemId: ItemId): List<PhotoRecord>
|
fun findByHuntIdAndItemId(huntId: HuntId, itemId: ItemId): List<PhotoRecord>
|
||||||
fun findByIdAndItemIdAndHuntId(id: PhotoId, itemId: ItemId, huntId: HuntId): 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