feat(home): 크리에이터 랭킹 조회 fallback을 재조회로 변경한다
This commit is contained in:
@@ -2,19 +2,13 @@ package kr.co.vividnext.sodalive.v2.ranking.application
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingItem
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingPeriodPolicy
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingScorePolicy
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingUtcRange
|
||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingAggregationPort
|
||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingBlockPort
|
||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotPort
|
||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotRecord
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
@@ -23,42 +17,21 @@ import java.time.ZonedDateTime
|
||||
class CreatorRankingQueryService(
|
||||
private val snapshotPort: CreatorRankingSnapshotPort,
|
||||
private val blockPort: CreatorRankingBlockPort,
|
||||
private val aggregationPort: CreatorRankingAggregationPort,
|
||||
private val snapshotJobService: CreatorRankingSnapshotJobService,
|
||||
private val nowProvider: () -> ZonedDateTime = { ZonedDateTime.now() },
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val cloudFrontHost: String
|
||||
) {
|
||||
private val log = LoggerFactory.getLogger(javaClass)
|
||||
private val periodPolicy = CreatorRankingPeriodPolicy()
|
||||
private val scorePolicy = CreatorRankingScorePolicy()
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getCreatorRankings(viewerMemberId: Long?): CreatorRankingResult {
|
||||
val startedAt = System.currentTimeMillis()
|
||||
return runCatching {
|
||||
val nowUtc = nowUtc()
|
||||
val latestSnapshots = snapshotPort.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, nowUtc)
|
||||
val latestSnapshots = findLatestVisibleSnapshots(nowUtc)
|
||||
val latestItems = latestSnapshots.toRankedItems()
|
||||
if (latestItems.isEmpty()) {
|
||||
if (snapshotPort.isSnapshotTableEmpty()) {
|
||||
val fallbackItems = aggregateColdStartFallback(nowUtc).toRankedItems()
|
||||
if (fallbackItems.isNotEmpty()) {
|
||||
delegateColdStartSnapshotRefresh()
|
||||
}
|
||||
val blockedCreatorIds = findBlockedCreatorIds(viewerMemberId = viewerMemberId, items = fallbackItems)
|
||||
return@runCatching QueryLogResult(
|
||||
result = CreatorRankingResult(
|
||||
showRankChange = false,
|
||||
items = fallbackItems.map { it.maskIfBlocked(blockedCreatorIds) }
|
||||
),
|
||||
blockedCreatorCount = blockedCreatorIds.size
|
||||
)
|
||||
}
|
||||
return@runCatching QueryLogResult(
|
||||
result = CreatorRankingResult(showRankChange = false, items = emptyList()),
|
||||
blockedCreatorCount = 0
|
||||
)
|
||||
return@runCatching QueryLogResult(CreatorRankingResult(showRankChange = false, items = emptyList()), 0)
|
||||
}
|
||||
|
||||
val previousItems = snapshotPort.findPreviousVisibleSnapshots(
|
||||
@@ -77,10 +50,7 @@ class CreatorRankingQueryService(
|
||||
).maskIfBlocked(blockedCreatorIds)
|
||||
}
|
||||
|
||||
QueryLogResult(
|
||||
result = CreatorRankingResult(showRankChange = showRankChange, items = items),
|
||||
blockedCreatorCount = blockedCreatorIds.size
|
||||
)
|
||||
QueryLogResult(CreatorRankingResult(showRankChange = showRankChange, items = items), blockedCreatorIds.size)
|
||||
}.onSuccess { logResult ->
|
||||
log.info(
|
||||
"event=creator_ranking_query_success showRankChange={} itemCount={} blockedCreatorCount={} elapsedMs={}",
|
||||
@@ -99,75 +69,30 @@ class CreatorRankingQueryService(
|
||||
}.getOrThrow().result
|
||||
}
|
||||
|
||||
private fun findLatestVisibleSnapshots(nowUtc: LocalDateTime): List<CreatorRankingSnapshotRecord> {
|
||||
val latestSnapshots = snapshotPort.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, nowUtc)
|
||||
if (latestSnapshots.isNotEmpty()) return latestSnapshots
|
||||
|
||||
runCatching { snapshotJobService.refreshLastCompletedWeekByFallback() }
|
||||
.onFailure { ex ->
|
||||
log.warn("event=creator_ranking_query_fallback_failure error={}", ex.message, ex)
|
||||
}
|
||||
return snapshotPort.findLatestVisibleSnapshots(CreatorRankingType.WEEKLY, nowUtc)
|
||||
}
|
||||
|
||||
private data class QueryLogResult(
|
||||
val result: CreatorRankingResult,
|
||||
val blockedCreatorCount: Int
|
||||
)
|
||||
|
||||
private fun aggregateColdStartFallback(nowUtc: LocalDateTime): List<CreatorRankingSnapshotRecord> {
|
||||
val startedAt = System.currentTimeMillis()
|
||||
val period = periodPolicy.resolveLastCompletedWeek(nowProvider())
|
||||
val utcRange = periodPolicy.toUtcRange(period)
|
||||
val visibleFromAtUtc = periodPolicy.resolveVisibleFromAtUtc(period.endExclusiveKst)
|
||||
if (visibleFromAtUtc > nowUtc) {
|
||||
return emptyList()
|
||||
}
|
||||
log.info(
|
||||
"event=creator_ranking_query_cold_start_fallback_attempt " +
|
||||
"aggregationStartAtUtc={} aggregationEndAtUtc={}",
|
||||
utcRange.startInclusiveUtc,
|
||||
utcRange.endExclusiveUtc
|
||||
)
|
||||
return runCatching {
|
||||
aggregationPort.aggregateCandidates(
|
||||
startInclusiveUtc = utcRange.startInclusiveUtc,
|
||||
endExclusiveUtc = utcRange.endExclusiveUtc
|
||||
).map { it.toSnapshotRecord(utcRange) }
|
||||
}.onSuccess { snapshots ->
|
||||
log.info(
|
||||
"event=creator_ranking_query_cold_start_fallback_success " +
|
||||
"aggregationStartAtUtc={} aggregationEndAtUtc={} itemCount={} elapsedMs={}",
|
||||
utcRange.startInclusiveUtc,
|
||||
utcRange.endExclusiveUtc,
|
||||
snapshots.size.coerceAtMost(RANKING_LIMIT),
|
||||
System.currentTimeMillis() - startedAt
|
||||
)
|
||||
}.onFailure { ex ->
|
||||
log.warn(
|
||||
"event=creator_ranking_query_cold_start_fallback_failure " +
|
||||
"aggregationStartAtUtc={} aggregationEndAtUtc={} elapsedMs={} error={}",
|
||||
utcRange.startInclusiveUtc,
|
||||
utcRange.endExclusiveUtc,
|
||||
System.currentTimeMillis() - startedAt,
|
||||
ex.message,
|
||||
ex
|
||||
)
|
||||
}.getOrThrow()
|
||||
}
|
||||
|
||||
private fun delegateColdStartSnapshotRefresh() {
|
||||
runCatching {
|
||||
snapshotJobService.ensureLastCompletedWeekSnapshotForColdStart()
|
||||
}.onFailure { ex ->
|
||||
log.warn(
|
||||
"event=creator_ranking_query_cold_start_snapshot_refresh_failure error={}",
|
||||
ex.message,
|
||||
ex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun nowUtc(): LocalDateTime {
|
||||
return nowProvider().withZoneSameInstant(UTC_ZONE).toLocalDateTime()
|
||||
}
|
||||
|
||||
private fun List<CreatorRankingSnapshotRecord>.toRankedItems(): List<CreatorRankingItem> {
|
||||
return groupBy { it.finalScore }
|
||||
.toSortedMap(compareByDescending { it })
|
||||
.values
|
||||
.flatMap { it.shuffled() }
|
||||
return sortedBy { it.rankNo }
|
||||
.take(RANKING_LIMIT)
|
||||
.mapIndexed { index, snapshot -> snapshot.toItem(rank = index + 1) }
|
||||
.map { snapshot -> snapshot.toItem(rank = snapshot.rankNo) }
|
||||
}
|
||||
|
||||
private fun CreatorRankingSnapshotRecord.toItem(rank: Int): CreatorRankingItem {
|
||||
@@ -181,70 +106,13 @@ class CreatorRankingQueryService(
|
||||
)
|
||||
}
|
||||
|
||||
private fun CreatorRankingSnapshotCandidate.toSnapshotRecord(utcRange: CreatorRankingUtcRange): CreatorRankingSnapshotRecord {
|
||||
val calculatedContentLiveScore = scorePolicy.calculateContentLiveScore(
|
||||
liveCanAmount = liveCanAmount,
|
||||
contentPurchaseCanAmount = contentPurchaseCanAmount
|
||||
)
|
||||
val calculatedEngagementScore = scorePolicy.calculateEngagementScore(
|
||||
contentLikeCount = contentLikeCount,
|
||||
contentCommentCount = contentCommentCount
|
||||
)
|
||||
val calculatedSupportScore = scorePolicy.calculateSupportScore(
|
||||
channelDonationCanAmount = channelDonationCanAmount,
|
||||
channelDonationCount = channelDonationCount,
|
||||
fanTalkCount = fanTalkCount
|
||||
)
|
||||
val calculatedFanLoyaltyScore = scorePolicy.calculateFanLoyaltyScore(
|
||||
finalFollowerCount = finalFollowerCount,
|
||||
followIncrease = followIncrease
|
||||
)
|
||||
val calculatedFinalScore = scorePolicy.calculateFinalScore(
|
||||
contentLiveScore = calculatedContentLiveScore,
|
||||
engagementScore = calculatedEngagementScore,
|
||||
supportScore = calculatedSupportScore,
|
||||
fanLoyaltyScore = calculatedFanLoyaltyScore
|
||||
)
|
||||
|
||||
return CreatorRankingSnapshotRecord(
|
||||
rankingType = CreatorRankingType.WEEKLY,
|
||||
aggregationStartAtUtc = utcRange.startInclusiveUtc,
|
||||
aggregationEndAtUtc = utcRange.endExclusiveUtc,
|
||||
visibleFromAtUtc = utcRange.endExclusiveUtc.plusHours(9),
|
||||
creatorId = creatorId,
|
||||
nickname = nickname,
|
||||
profileImageUrl = profileImageUrl,
|
||||
finalScore = calculatedFinalScore,
|
||||
contentLiveScore = calculatedContentLiveScore,
|
||||
engagementScore = calculatedEngagementScore,
|
||||
supportScore = calculatedSupportScore,
|
||||
fanLoyaltyScore = calculatedFanLoyaltyScore,
|
||||
liveCanAmount = liveCanAmount,
|
||||
contentPurchaseCanAmount = contentPurchaseCanAmount,
|
||||
contentLikeCount = contentLikeCount,
|
||||
contentCommentCount = contentCommentCount,
|
||||
channelDonationCanAmount = channelDonationCanAmount,
|
||||
channelDonationCount = channelDonationCount,
|
||||
fanTalkCount = fanTalkCount,
|
||||
finalFollowerCount = finalFollowerCount,
|
||||
followIncrease = followIncrease
|
||||
)
|
||||
}
|
||||
|
||||
private fun findBlockedCreatorIds(viewerMemberId: Long?, items: List<CreatorRankingItem>): Set<Long> {
|
||||
if (viewerMemberId == null) {
|
||||
return emptySet()
|
||||
}
|
||||
return blockPort.findBlockedCreatorIds(
|
||||
memberId = viewerMemberId,
|
||||
creatorIds = items.map { it.creatorId }
|
||||
)
|
||||
if (viewerMemberId == null) return emptySet()
|
||||
return blockPort.findBlockedCreatorIds(memberId = viewerMemberId, creatorIds = items.map { it.creatorId })
|
||||
}
|
||||
|
||||
private fun CreatorRankingItem.maskIfBlocked(blockedCreatorIds: Set<Long>): CreatorRankingItem {
|
||||
if (!blockedCreatorIds.contains(creatorId)) {
|
||||
return this
|
||||
}
|
||||
if (!blockedCreatorIds.contains(creatorId)) return this
|
||||
return copy(
|
||||
creatorId = MASKED_CREATOR_ID,
|
||||
nickname = MASKED_NICKNAME,
|
||||
|
||||
Reference in New Issue
Block a user