feat(content-ranking): 랭킹 스냅샷 갱신 서비스를 추가한다
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
package kr.co.vividnext.sodalive.v2.content.ranking.application
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingPeriod
|
||||
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingPeriodPolicy
|
||||
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingSchedulePolicy
|
||||
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingScorePolicy
|
||||
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingSnapshotCandidate
|
||||
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingType
|
||||
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingUtcRange
|
||||
import kr.co.vividnext.sodalive.v2.content.ranking.port.out.AudioRankingAggregationPort
|
||||
import kr.co.vividnext.sodalive.v2.content.ranking.port.out.AudioRankingSnapshotPort
|
||||
import kr.co.vividnext.sodalive.v2.content.ranking.port.out.AudioRankingSnapshotRecord
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.math.max
|
||||
|
||||
@Service
|
||||
class AudioRankingSnapshotRefreshService(
|
||||
private val aggregationPort: AudioRankingAggregationPort,
|
||||
private val snapshotPort: AudioRankingSnapshotPort
|
||||
) {
|
||||
private val periodPolicy = AudioRankingPeriodPolicy()
|
||||
private val schedulePolicy = AudioRankingSchedulePolicy()
|
||||
private val scorePolicy = AudioRankingScorePolicy()
|
||||
|
||||
@Transactional
|
||||
fun refreshLastCompletedWeek(type: AudioRankingType, now: ZonedDateTime) {
|
||||
val period = periodPolicy.resolveLastCompletedWeek(now)
|
||||
val utcRange = periodPolicy.toUtcRange(period)
|
||||
val visibleFromAtUtc = schedulePolicy.resolveVisibleFromAt(period.endExclusiveKst)
|
||||
val candidates = resolveCandidates(type, utcRange)
|
||||
val snapshots = candidates.toSnapshotRecords(type, period, utcRange, visibleFromAtUtc)
|
||||
|
||||
snapshotPort.replaceSnapshots(
|
||||
rankingType = type,
|
||||
aggregationStartAtUtc = utcRange.startInclusiveUtc,
|
||||
aggregationEndAtUtc = utcRange.endExclusiveUtc,
|
||||
visibleFromAtUtc = visibleFromAtUtc,
|
||||
newSnapshots = snapshots
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveCandidates(
|
||||
type: AudioRankingType,
|
||||
utcRange: AudioRankingUtcRange
|
||||
): List<AudioRankingSnapshotCandidate> {
|
||||
return when (type) {
|
||||
AudioRankingType.WEEKLY_POPULAR -> aggregationPort.aggregateWeeklyPopularCandidates(
|
||||
utcRange.startInclusiveUtc,
|
||||
utcRange.endExclusiveUtc
|
||||
)
|
||||
AudioRankingType.RISING -> aggregationPort.aggregateRisingCandidates(
|
||||
utcRange.startInclusiveUtc,
|
||||
utcRange.endExclusiveUtc
|
||||
)
|
||||
AudioRankingType.REVENUE -> aggregationPort.aggregateRevenueCandidates(
|
||||
utcRange.startInclusiveUtc,
|
||||
utcRange.endExclusiveUtc
|
||||
)
|
||||
AudioRankingType.SALES_COUNT -> aggregationPort.aggregateSalesCountCandidates(
|
||||
utcRange.startInclusiveUtc,
|
||||
utcRange.endExclusiveUtc
|
||||
)
|
||||
AudioRankingType.COMMENT_COUNT -> aggregationPort.aggregateCommentCountCandidates(
|
||||
utcRange.startInclusiveUtc,
|
||||
utcRange.endExclusiveUtc
|
||||
)
|
||||
AudioRankingType.LIKE_COUNT -> aggregationPort.aggregateLikeCountCandidates(
|
||||
utcRange.startInclusiveUtc,
|
||||
utcRange.endExclusiveUtc
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<AudioRankingSnapshotCandidate>.toSnapshotRecords(
|
||||
type: AudioRankingType,
|
||||
period: AudioRankingPeriod,
|
||||
utcRange: AudioRankingUtcRange,
|
||||
visibleFromAtUtc: java.time.LocalDateTime
|
||||
): List<AudioRankingSnapshotRecord> {
|
||||
val scoredCandidates = when (type) {
|
||||
AudioRankingType.WEEKLY_POPULAR -> withWeeklyPopularScores()
|
||||
AudioRankingType.RISING -> withRisingScores(period)
|
||||
AudioRankingType.REVENUE,
|
||||
AudioRankingType.SALES_COUNT,
|
||||
AudioRankingType.COMMENT_COUNT,
|
||||
AudioRankingType.LIKE_COUNT -> this
|
||||
}
|
||||
|
||||
val rankedRecords = scoredCandidates
|
||||
.sortedWith(
|
||||
compareByDescending<AudioRankingSnapshotCandidate> { it.finalScore }
|
||||
.thenByDescending { it.releaseDate }
|
||||
.thenByDescending { it.contentId }
|
||||
)
|
||||
.mapIndexed { index, candidate -> candidate.toSnapshotRecord(type, utcRange, visibleFromAtUtc, index + 1) }
|
||||
|
||||
val globalContentIds = rankedRecords.take(SNAPSHOT_LIMIT).map { it.contentId }.toSet()
|
||||
val safeContentIds = rankedRecords.filter { !it.isAdult }.take(SNAPSHOT_LIMIT).map { it.contentId }.toSet()
|
||||
val selectedContentIds = globalContentIds + safeContentIds
|
||||
|
||||
return rankedRecords.filter { it.contentId in selectedContentIds }
|
||||
}
|
||||
|
||||
private fun List<AudioRankingSnapshotCandidate>.withWeeklyPopularScores(): List<AudioRankingSnapshotCandidate> {
|
||||
val rawScores = associateWith { candidate ->
|
||||
scorePolicy.calculateWeeklyPopularScore(
|
||||
revenue = candidate.revenueCanAmount,
|
||||
salesCount = candidate.salesCount,
|
||||
viewCount = candidate.viewCount,
|
||||
likeCount = candidate.likeCount,
|
||||
commentCount = candidate.commentCount,
|
||||
isPaid = candidate.isPaid
|
||||
)
|
||||
}
|
||||
val paidMaxScore = rawScores.filterKeys { it.isPaid }.values.maxOrNull() ?: 0.0
|
||||
val freeMaxScore = rawScores.filterKeys { !it.isPaid }.values.maxOrNull() ?: 0.0
|
||||
|
||||
return map { candidate ->
|
||||
val rawScore = rawScores.getValue(candidate)
|
||||
candidate.copy(
|
||||
finalScore = scorePolicy.normalizeScore(
|
||||
rawScore,
|
||||
if (candidate.isPaid) paidMaxScore else freeMaxScore
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AudioRankingSnapshotCandidate.withRisingScore(period: AudioRankingPeriod): AudioRankingSnapshotCandidate {
|
||||
return copy(
|
||||
finalScore = scorePolicy.calculateRisingScore(
|
||||
recentSalesCount = salesCount,
|
||||
previousSalesCount = previousSalesCount,
|
||||
recentViewCount = viewCount,
|
||||
previousViewCount = previousViewCount,
|
||||
recentLikeCount = likeCount,
|
||||
previousLikeCount = previousLikeCount,
|
||||
recentCommentCount = commentCount,
|
||||
previousCommentCount = previousCommentCount,
|
||||
releaseDate = releaseDate,
|
||||
aggregationEndAt = period.endExclusiveKst,
|
||||
isPaid = isPaid
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun List<AudioRankingSnapshotCandidate>.withRisingScores(
|
||||
period: AudioRankingPeriod
|
||||
): List<AudioRankingSnapshotCandidate> {
|
||||
val scoredCandidates = map { it.withRisingScore(period) }
|
||||
val paidMaxScore = scoredCandidates.filter { it.isPaid }.maxOfOrNull { it.finalScore } ?: 0.0
|
||||
val freeMaxScore = scoredCandidates.filter { !it.isPaid }.maxOfOrNull { it.finalScore } ?: 0.0
|
||||
|
||||
return scoredCandidates.map { candidate ->
|
||||
candidate.copy(
|
||||
finalScore = scorePolicy.normalizeScore(
|
||||
candidate.finalScore,
|
||||
if (candidate.isPaid) paidMaxScore else freeMaxScore
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun AudioRankingSnapshotCandidate.toSnapshotRecord(
|
||||
type: AudioRankingType,
|
||||
utcRange: AudioRankingUtcRange,
|
||||
visibleFromAtUtc: java.time.LocalDateTime,
|
||||
rank: Int
|
||||
): AudioRankingSnapshotRecord {
|
||||
return AudioRankingSnapshotRecord(
|
||||
rankingType = type,
|
||||
aggregationStartAtUtc = utcRange.startInclusiveUtc,
|
||||
aggregationEndAtUtc = utcRange.endExclusiveUtc,
|
||||
visibleFromAtUtc = visibleFromAtUtc,
|
||||
contentId = contentId,
|
||||
title = title,
|
||||
creatorMemberId = creatorMemberId,
|
||||
creatorNickname = creatorNickname,
|
||||
coverImageUrl = coverImageUrl,
|
||||
releaseDate = releaseDate,
|
||||
isAdult = isAdult,
|
||||
rank = rank,
|
||||
finalScore = max(finalScore, 0.0),
|
||||
revenueCanAmount = revenueCanAmount,
|
||||
salesCount = salesCount,
|
||||
viewCount = viewCount,
|
||||
likeCount = likeCount,
|
||||
commentCount = commentCount,
|
||||
previousSalesCount = previousSalesCount,
|
||||
previousViewCount = previousViewCount,
|
||||
previousLikeCount = previousLikeCount,
|
||||
previousCommentCount = previousCommentCount
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SNAPSHOT_LIMIT = 20
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user