feat(content-ranking): 랭킹 후보 집계를 추가한다
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
package kr.co.vividnext.sodalive.v2.content.ranking.adapter.out.persistence
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingSnapshotCandidate
|
||||
import kr.co.vividnext.sodalive.v2.content.ranking.port.out.AudioRankingAggregationPort
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.sql.Timestamp
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@Repository
|
||||
class DefaultAudioRankingAggregationRepository(
|
||||
private val entityManager: EntityManager
|
||||
) : AudioRankingAggregationPort {
|
||||
override fun aggregateWeeklyPopularCandidates(
|
||||
startInclusiveUtc: LocalDateTime,
|
||||
endExclusiveUtc: LocalDateTime
|
||||
): List<AudioRankingSnapshotCandidate> {
|
||||
return aggregateCandidates(startInclusiveUtc, endExclusiveUtc, null, null)
|
||||
}
|
||||
|
||||
override fun aggregateRisingCandidates(
|
||||
startInclusiveUtc: LocalDateTime,
|
||||
endExclusiveUtc: LocalDateTime
|
||||
): List<AudioRankingSnapshotCandidate> {
|
||||
val previousStartInclusiveUtc = startInclusiveUtc.minusWeeks(1)
|
||||
val previousEndExclusiveUtc = startInclusiveUtc
|
||||
return aggregateCandidates(startInclusiveUtc, endExclusiveUtc, previousStartInclusiveUtc, previousEndExclusiveUtc)
|
||||
}
|
||||
|
||||
override fun aggregateRevenueCandidates(
|
||||
startInclusiveUtc: LocalDateTime,
|
||||
endExclusiveUtc: LocalDateTime
|
||||
): List<AudioRankingSnapshotCandidate> {
|
||||
return aggregateCandidates(startInclusiveUtc, endExclusiveUtc, null, null)
|
||||
.filter { it.revenueCanAmount > 0 }
|
||||
.map { it.copy(finalScore = it.revenueCanAmount.toDouble()) }
|
||||
}
|
||||
|
||||
override fun aggregateSalesCountCandidates(
|
||||
startInclusiveUtc: LocalDateTime,
|
||||
endExclusiveUtc: LocalDateTime
|
||||
): List<AudioRankingSnapshotCandidate> {
|
||||
return aggregateCandidates(startInclusiveUtc, endExclusiveUtc, null, null)
|
||||
.filter { it.salesCount > 0 }
|
||||
.map { it.copy(finalScore = it.salesCount.toDouble()) }
|
||||
}
|
||||
|
||||
override fun aggregateCommentCountCandidates(
|
||||
startInclusiveUtc: LocalDateTime,
|
||||
endExclusiveUtc: LocalDateTime
|
||||
): List<AudioRankingSnapshotCandidate> {
|
||||
return aggregateCandidates(startInclusiveUtc, endExclusiveUtc, null, null)
|
||||
.filter { it.commentCount > 0 }
|
||||
.map { it.copy(finalScore = it.commentCount.toDouble()) }
|
||||
}
|
||||
|
||||
override fun aggregateLikeCountCandidates(
|
||||
startInclusiveUtc: LocalDateTime,
|
||||
endExclusiveUtc: LocalDateTime
|
||||
): List<AudioRankingSnapshotCandidate> {
|
||||
return aggregateCandidates(startInclusiveUtc, endExclusiveUtc, null, null)
|
||||
.filter { it.likeCount > 0 }
|
||||
.map { it.copy(finalScore = it.likeCount.toDouble()) }
|
||||
}
|
||||
|
||||
private fun aggregateCandidates(
|
||||
startInclusiveUtc: LocalDateTime,
|
||||
endExclusiveUtc: LocalDateTime,
|
||||
previousStartInclusiveUtc: LocalDateTime?,
|
||||
previousEndExclusiveUtc: LocalDateTime?
|
||||
): List<AudioRankingSnapshotCandidate> {
|
||||
val rows = entityManager.createNativeQuery(AGGREGATION_SQL)
|
||||
.setParameter("startInclusiveUtc", startInclusiveUtc)
|
||||
.setParameter("endExclusiveUtc", endExclusiveUtc)
|
||||
.setParameter("previousStartInclusiveUtc", previousStartInclusiveUtc ?: startInclusiveUtc)
|
||||
.setParameter("previousEndExclusiveUtc", previousEndExclusiveUtc ?: startInclusiveUtc)
|
||||
.resultList
|
||||
|
||||
return rows.map { row -> (row as Array<*>).toCandidate() }
|
||||
}
|
||||
|
||||
private fun Array<*>.toCandidate(): AudioRankingSnapshotCandidate {
|
||||
return AudioRankingSnapshotCandidate(
|
||||
contentId = this[0].toLong(),
|
||||
title = this[1] as String,
|
||||
creatorMemberId = this[2].toLong(),
|
||||
creatorNickname = this[3] as String,
|
||||
coverImageUrl = this[4] as String?,
|
||||
releaseDate = this[5].toLocalDateTime(),
|
||||
isAdult = this[6].toBoolean(),
|
||||
isPaid = this[7].toLong() > 0,
|
||||
revenueCanAmount = this[8].toLong(),
|
||||
salesCount = this[9].toLong(),
|
||||
viewCount = this[10].toLong(),
|
||||
likeCount = this[11].toLong(),
|
||||
commentCount = this[12].toLong(),
|
||||
previousSalesCount = this[13].toLong(),
|
||||
previousViewCount = this[14].toLong(),
|
||||
previousLikeCount = this[15].toLong(),
|
||||
previousCommentCount = this[16].toLong()
|
||||
)
|
||||
}
|
||||
|
||||
private fun Any?.toLong(): Long {
|
||||
return (this as Number?)?.toLong() ?: 0L
|
||||
}
|
||||
|
||||
private fun Any?.toBoolean(): Boolean {
|
||||
return when (this) {
|
||||
is Boolean -> this
|
||||
is Number -> toInt() != 0
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun Any?.toLocalDateTime(): LocalDateTime {
|
||||
return when (this) {
|
||||
is LocalDateTime -> this
|
||||
is Timestamp -> toLocalDateTime()
|
||||
else -> error("Unsupported datetime value: $this")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val AGGREGATION_SQL = """
|
||||
with eligible_content as (
|
||||
select c.id as content_id,
|
||||
c.title as title,
|
||||
c.member_id as creator_member_id,
|
||||
m.nickname as creator_nickname,
|
||||
c.cover_image as cover_image_url,
|
||||
c.release_date as release_date,
|
||||
c.is_adult as is_adult,
|
||||
c.price as price
|
||||
from content c
|
||||
join member m on m.id = c.member_id
|
||||
join content_theme ct on ct.id = c.theme_id
|
||||
where c.is_active = true
|
||||
and c.release_date is not null
|
||||
and c.release_date < :endExclusiveUtc
|
||||
and c.duration is not null
|
||||
and c.limited is null
|
||||
and ct.is_active = true
|
||||
and m.role = 'CREATOR'
|
||||
and m.is_active = true
|
||||
), order_metrics as (
|
||||
select o.content_id,
|
||||
sum(o.can) as revenue_can_amount,
|
||||
count(o.id) as sales_count
|
||||
from orders o
|
||||
where o.is_active = true
|
||||
and o.created_at >= :startInclusiveUtc
|
||||
and o.created_at < :endExclusiveUtc
|
||||
group by o.content_id
|
||||
), view_metrics as (
|
||||
select ccvh.content_id,
|
||||
count(ccvh.id) as view_count
|
||||
from creator_content_view_history ccvh
|
||||
where ccvh.viewed_at >= :startInclusiveUtc
|
||||
and ccvh.viewed_at < :endExclusiveUtc
|
||||
group by ccvh.content_id
|
||||
), like_metrics as (
|
||||
select cl.content_id,
|
||||
count(cl.id) as like_count
|
||||
from content_like cl
|
||||
where cl.is_active = true
|
||||
and cl.created_at >= :startInclusiveUtc
|
||||
and cl.created_at < :endExclusiveUtc
|
||||
group by cl.content_id
|
||||
), comment_metrics as (
|
||||
select cc.content_id,
|
||||
count(cc.id) as comment_count
|
||||
from content_comment cc
|
||||
where cc.is_active = true
|
||||
and cc.created_at >= :startInclusiveUtc
|
||||
and cc.created_at < :endExclusiveUtc
|
||||
group by cc.content_id
|
||||
), previous_order_metrics as (
|
||||
select o.content_id,
|
||||
count(o.id) as previous_sales_count
|
||||
from orders o
|
||||
where o.is_active = true
|
||||
and o.created_at >= :previousStartInclusiveUtc
|
||||
and o.created_at < :previousEndExclusiveUtc
|
||||
group by o.content_id
|
||||
), previous_view_metrics as (
|
||||
select ccvh.content_id,
|
||||
count(ccvh.id) as previous_view_count
|
||||
from creator_content_view_history ccvh
|
||||
where ccvh.viewed_at >= :previousStartInclusiveUtc
|
||||
and ccvh.viewed_at < :previousEndExclusiveUtc
|
||||
group by ccvh.content_id
|
||||
), previous_like_metrics as (
|
||||
select cl.content_id,
|
||||
count(cl.id) as previous_like_count
|
||||
from content_like cl
|
||||
where cl.is_active = true
|
||||
and cl.created_at >= :previousStartInclusiveUtc
|
||||
and cl.created_at < :previousEndExclusiveUtc
|
||||
group by cl.content_id
|
||||
), previous_comment_metrics as (
|
||||
select cc.content_id,
|
||||
count(cc.id) as previous_comment_count
|
||||
from content_comment cc
|
||||
where cc.is_active = true
|
||||
and cc.created_at >= :previousStartInclusiveUtc
|
||||
and cc.created_at < :previousEndExclusiveUtc
|
||||
group by cc.content_id
|
||||
)
|
||||
select ec.content_id,
|
||||
ec.title,
|
||||
ec.creator_member_id,
|
||||
ec.creator_nickname,
|
||||
ec.cover_image_url,
|
||||
ec.release_date,
|
||||
ec.is_adult,
|
||||
ec.price,
|
||||
coalesce(om.revenue_can_amount, 0) as revenue_can_amount,
|
||||
coalesce(om.sales_count, 0) as sales_count,
|
||||
coalesce(vm.view_count, 0) as view_count,
|
||||
coalesce(lm.like_count, 0) as like_count,
|
||||
coalesce(cm.comment_count, 0) as comment_count,
|
||||
coalesce(pom.previous_sales_count, 0) as previous_sales_count,
|
||||
coalesce(pvm.previous_view_count, 0) as previous_view_count,
|
||||
coalesce(plm.previous_like_count, 0) as previous_like_count,
|
||||
coalesce(pcm.previous_comment_count, 0) as previous_comment_count
|
||||
from eligible_content ec
|
||||
left join order_metrics om on om.content_id = ec.content_id
|
||||
left join view_metrics vm on vm.content_id = ec.content_id
|
||||
left join like_metrics lm on lm.content_id = ec.content_id
|
||||
left join comment_metrics cm on cm.content_id = ec.content_id
|
||||
left join previous_order_metrics pom on pom.content_id = ec.content_id
|
||||
left join previous_view_metrics pvm on pvm.content_id = ec.content_id
|
||||
left join previous_like_metrics plm on plm.content_id = ec.content_id
|
||||
left join previous_comment_metrics pcm on pcm.content_id = ec.content_id
|
||||
where coalesce(om.revenue_can_amount, 0) <> 0
|
||||
or coalesce(om.sales_count, 0) <> 0
|
||||
or coalesce(vm.view_count, 0) <> 0
|
||||
or coalesce(lm.like_count, 0) <> 0
|
||||
or coalesce(cm.comment_count, 0) <> 0
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package kr.co.vividnext.sodalive.v2.content.ranking.domain
|
||||
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class AudioRankingSnapshotCandidate(
|
||||
val contentId: Long,
|
||||
val title: String,
|
||||
val creatorMemberId: Long,
|
||||
val creatorNickname: String,
|
||||
val coverImageUrl: String?,
|
||||
val releaseDate: LocalDateTime,
|
||||
val isAdult: Boolean,
|
||||
val isPaid: Boolean,
|
||||
val finalScore: Double = 0.0,
|
||||
val revenueCanAmount: Long = 0,
|
||||
val salesCount: Long = 0,
|
||||
val viewCount: Long = 0,
|
||||
val likeCount: Long = 0,
|
||||
val commentCount: Long = 0,
|
||||
val previousSalesCount: Long = 0,
|
||||
val previousViewCount: Long = 0,
|
||||
val previousLikeCount: Long = 0,
|
||||
val previousCommentCount: Long = 0
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
package kr.co.vividnext.sodalive.v2.content.ranking.port.out
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.content.ranking.domain.AudioRankingSnapshotCandidate
|
||||
import java.time.LocalDateTime
|
||||
|
||||
interface AudioRankingAggregationPort {
|
||||
fun aggregateWeeklyPopularCandidates(
|
||||
startInclusiveUtc: LocalDateTime,
|
||||
endExclusiveUtc: LocalDateTime
|
||||
): List<AudioRankingSnapshotCandidate>
|
||||
|
||||
fun aggregateRisingCandidates(
|
||||
startInclusiveUtc: LocalDateTime,
|
||||
endExclusiveUtc: LocalDateTime
|
||||
): List<AudioRankingSnapshotCandidate>
|
||||
|
||||
fun aggregateRevenueCandidates(
|
||||
startInclusiveUtc: LocalDateTime,
|
||||
endExclusiveUtc: LocalDateTime
|
||||
): List<AudioRankingSnapshotCandidate>
|
||||
|
||||
fun aggregateSalesCountCandidates(
|
||||
startInclusiveUtc: LocalDateTime,
|
||||
endExclusiveUtc: LocalDateTime
|
||||
): List<AudioRankingSnapshotCandidate>
|
||||
|
||||
fun aggregateCommentCountCandidates(
|
||||
startInclusiveUtc: LocalDateTime,
|
||||
endExclusiveUtc: LocalDateTime
|
||||
): List<AudioRankingSnapshotCandidate>
|
||||
|
||||
fun aggregateLikeCountCandidates(
|
||||
startInclusiveUtc: LocalDateTime,
|
||||
endExclusiveUtc: LocalDateTime
|
||||
): List<AudioRankingSnapshotCandidate>
|
||||
}
|
||||
Reference in New Issue
Block a user