feat(ranking): 크리에이터 랭킹 집계 저장소를 추가한다
This commit is contained in:
@@ -0,0 +1,183 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence
|
||||||
|
|
||||||
|
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.port.out.CreatorRankingAggregationPort
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import javax.persistence.EntityManager
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class DefaultCreatorRankingAggregationRepository(
|
||||||
|
private val entityManager: EntityManager
|
||||||
|
) : CreatorRankingAggregationPort {
|
||||||
|
private val scorePolicy = CreatorRankingScorePolicy()
|
||||||
|
|
||||||
|
override fun aggregateCandidates(
|
||||||
|
startInclusiveUtc: LocalDateTime,
|
||||||
|
endExclusiveUtc: LocalDateTime
|
||||||
|
): List<CreatorRankingSnapshotCandidate> {
|
||||||
|
val rows = entityManager.createNativeQuery(AGGREGATION_SQL)
|
||||||
|
.setParameter("startInclusiveUtc", startInclusiveUtc)
|
||||||
|
.setParameter("endExclusiveUtc", endExclusiveUtc)
|
||||||
|
.resultList
|
||||||
|
|
||||||
|
return rows
|
||||||
|
.map { row -> (row as Array<*>).toCandidate() }
|
||||||
|
.filter { candidate -> candidate.finalScore >= MINIMUM_FINAL_SCORE }
|
||||||
|
.sortedWith(compareByDescending<CreatorRankingSnapshotCandidate> { it.finalScore }.thenBy { it.creatorId })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Array<*>.toCandidate(): CreatorRankingSnapshotCandidate {
|
||||||
|
val creatorId = this[0].toLong()
|
||||||
|
val nickname = this[1] as String
|
||||||
|
val profileImageUrl = this[2] as String?
|
||||||
|
val liveCanAmount = this[3].toLong()
|
||||||
|
val contentPurchaseCanAmount = this[4].toLong()
|
||||||
|
val contentLikeCount = this[5].toLong()
|
||||||
|
val contentCommentCount = this[6].toLong()
|
||||||
|
val channelDonationCanAmount = this[7].toLong()
|
||||||
|
val channelDonationCount = this[8].toLong()
|
||||||
|
val fanTalkCount = this[9].toLong()
|
||||||
|
val finalFollowerCount = this[10].toLong()
|
||||||
|
val followIncrease = this[11].toLong()
|
||||||
|
val contentLiveScore = scorePolicy.calculateContentLiveScore(liveCanAmount, contentPurchaseCanAmount)
|
||||||
|
val engagementScore = scorePolicy.calculateEngagementScore(contentLikeCount, contentCommentCount)
|
||||||
|
val supportScore = scorePolicy.calculateSupportScore(channelDonationCanAmount, channelDonationCount, fanTalkCount)
|
||||||
|
val fanLoyaltyScore = scorePolicy.calculateFanLoyaltyScore(finalFollowerCount, followIncrease)
|
||||||
|
val finalScore = scorePolicy.calculateFinalScore(contentLiveScore, engagementScore, supportScore, fanLoyaltyScore)
|
||||||
|
|
||||||
|
return CreatorRankingSnapshotCandidate(
|
||||||
|
creatorId = creatorId,
|
||||||
|
nickname = nickname,
|
||||||
|
profileImageUrl = profileImageUrl,
|
||||||
|
finalScore = finalScore,
|
||||||
|
contentLiveScore = contentLiveScore,
|
||||||
|
engagementScore = engagementScore,
|
||||||
|
supportScore = supportScore,
|
||||||
|
fanLoyaltyScore = fanLoyaltyScore,
|
||||||
|
liveCanAmount = liveCanAmount,
|
||||||
|
contentPurchaseCanAmount = contentPurchaseCanAmount,
|
||||||
|
contentLikeCount = contentLikeCount,
|
||||||
|
contentCommentCount = contentCommentCount,
|
||||||
|
channelDonationCanAmount = channelDonationCanAmount,
|
||||||
|
channelDonationCount = channelDonationCount,
|
||||||
|
fanTalkCount = fanTalkCount,
|
||||||
|
finalFollowerCount = finalFollowerCount,
|
||||||
|
followIncrease = followIncrease
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Any?.toLong(): Long {
|
||||||
|
return (this as Number?)?.toLong() ?: 0L
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MINIMUM_FINAL_SCORE = 1.0
|
||||||
|
|
||||||
|
private val AGGREGATION_SQL = """
|
||||||
|
with active_creators as (
|
||||||
|
select id, nickname, profile_image
|
||||||
|
from member
|
||||||
|
where role = 'CREATOR'
|
||||||
|
and is_active = true
|
||||||
|
), can_metrics as (
|
||||||
|
select ucc.recipient_creator_id as creator_id,
|
||||||
|
sum(case when uc.can_usage in ('DONATION', 'LIVE', 'SPIN_ROULETTE') then ucc.can else 0 end) as live_can_amount,
|
||||||
|
sum(case when uc.can_usage = 'ORDER_CONTENT' then ucc.can else 0 end) as content_purchase_can_amount,
|
||||||
|
sum(case when uc.can_usage = 'CHANNEL_DONATION' then ucc.can else 0 end) as channel_donation_can_amount,
|
||||||
|
sum(case when uc.can_usage = 'CHANNEL_DONATION' then 1 else 0 end) as channel_donation_count
|
||||||
|
from use_can_calculate ucc
|
||||||
|
join use_can uc on uc.id = ucc.use_can_id
|
||||||
|
where ucc.recipient_creator_id is not null
|
||||||
|
and ucc.status = 'RECEIVED'
|
||||||
|
and uc.is_refund = false
|
||||||
|
and ucc.created_at >= :startInclusiveUtc
|
||||||
|
and ucc.created_at < :endExclusiveUtc
|
||||||
|
group by ucc.recipient_creator_id
|
||||||
|
), like_metrics as (
|
||||||
|
select c.member_id as creator_id,
|
||||||
|
count(cl.id) as content_like_count
|
||||||
|
from content_like cl
|
||||||
|
join content c on c.id = cl.content_id
|
||||||
|
where c.is_active = true
|
||||||
|
and cl.is_active = true
|
||||||
|
and cl.created_at >= :startInclusiveUtc
|
||||||
|
and cl.created_at < :endExclusiveUtc
|
||||||
|
group by c.member_id
|
||||||
|
), comment_metrics as (
|
||||||
|
select c.member_id as creator_id,
|
||||||
|
count(cc.id) as content_comment_count
|
||||||
|
from content_comment cc
|
||||||
|
join content c on c.id = cc.content_id
|
||||||
|
where c.is_active = true
|
||||||
|
and cc.is_active = true
|
||||||
|
and cc.member_id <> c.member_id
|
||||||
|
and cc.created_at >= :startInclusiveUtc
|
||||||
|
and cc.created_at < :endExclusiveUtc
|
||||||
|
group by c.member_id
|
||||||
|
), fan_talk_metrics as (
|
||||||
|
select creator_id,
|
||||||
|
count(id) as fan_talk_count
|
||||||
|
from creator_cheers
|
||||||
|
where is_active = true
|
||||||
|
and parent_id is null
|
||||||
|
and created_at >= :startInclusiveUtc
|
||||||
|
and created_at < :endExclusiveUtc
|
||||||
|
group by creator_id
|
||||||
|
), final_follower_metrics as (
|
||||||
|
select creator_id,
|
||||||
|
count(id) as final_follower_count
|
||||||
|
from creator_following
|
||||||
|
where is_active = true
|
||||||
|
and created_at < :endExclusiveUtc
|
||||||
|
group by creator_id
|
||||||
|
), new_follow_metrics as (
|
||||||
|
select creator_id,
|
||||||
|
count(id) as new_follow_count
|
||||||
|
from creator_following
|
||||||
|
where created_at >= :startInclusiveUtc
|
||||||
|
and created_at < :endExclusiveUtc
|
||||||
|
group by creator_id
|
||||||
|
), unfollow_metrics as (
|
||||||
|
select creator_id,
|
||||||
|
count(id) as unfollow_count
|
||||||
|
from creator_following
|
||||||
|
where is_active = false
|
||||||
|
and updated_at >= :startInclusiveUtc
|
||||||
|
and updated_at < :endExclusiveUtc
|
||||||
|
group by creator_id
|
||||||
|
)
|
||||||
|
select ac.id as creator_id,
|
||||||
|
ac.nickname as nickname,
|
||||||
|
ac.profile_image as profile_image_url,
|
||||||
|
coalesce(cm.live_can_amount, 0) as live_can_amount,
|
||||||
|
coalesce(cm.content_purchase_can_amount, 0) as content_purchase_can_amount,
|
||||||
|
coalesce(lm.content_like_count, 0) as content_like_count,
|
||||||
|
coalesce(com.content_comment_count, 0) as content_comment_count,
|
||||||
|
coalesce(cm.channel_donation_can_amount, 0) as channel_donation_can_amount,
|
||||||
|
coalesce(cm.channel_donation_count, 0) as channel_donation_count,
|
||||||
|
coalesce(ftm.fan_talk_count, 0) as fan_talk_count,
|
||||||
|
coalesce(ffm.final_follower_count, 0) as final_follower_count,
|
||||||
|
coalesce(nfm.new_follow_count, 0) - coalesce(um.unfollow_count, 0) as follow_increase
|
||||||
|
from active_creators ac
|
||||||
|
left join can_metrics cm on cm.creator_id = ac.id
|
||||||
|
left join like_metrics lm on lm.creator_id = ac.id
|
||||||
|
left join comment_metrics com on com.creator_id = ac.id
|
||||||
|
left join fan_talk_metrics ftm on ftm.creator_id = ac.id
|
||||||
|
left join final_follower_metrics ffm on ffm.creator_id = ac.id
|
||||||
|
left join new_follow_metrics nfm on nfm.creator_id = ac.id
|
||||||
|
left join unfollow_metrics um on um.creator_id = ac.id
|
||||||
|
where coalesce(cm.live_can_amount, 0) <> 0
|
||||||
|
or coalesce(cm.content_purchase_can_amount, 0) <> 0
|
||||||
|
or coalesce(lm.content_like_count, 0) <> 0
|
||||||
|
or coalesce(com.content_comment_count, 0) <> 0
|
||||||
|
or coalesce(cm.channel_donation_can_amount, 0) <> 0
|
||||||
|
or coalesce(cm.channel_donation_count, 0) <> 0
|
||||||
|
or coalesce(ftm.fan_talk_count, 0) <> 0
|
||||||
|
or coalesce(ffm.final_follower_count, 0) <> 0
|
||||||
|
or coalesce(nfm.new_follow_count, 0) <> 0
|
||||||
|
or coalesce(um.unfollow_count, 0) <> 0
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.ranking.port.out
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingSnapshotCandidate
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
interface CreatorRankingAggregationPort {
|
||||||
|
fun aggregateCandidates(
|
||||||
|
startInclusiveUtc: LocalDateTime,
|
||||||
|
endExclusiveUtc: LocalDateTime
|
||||||
|
): List<CreatorRankingSnapshotCandidate>
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
||||||
|
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||||
|
import kr.co.vividnext.sodalive.can.use.UseCan
|
||||||
|
import kr.co.vividnext.sodalive.can.use.UseCanCalculate
|
||||||
|
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
|
||||||
|
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||||
|
import kr.co.vividnext.sodalive.content.AudioContent
|
||||||
|
import kr.co.vividnext.sodalive.content.comment.AudioContentComment
|
||||||
|
import kr.co.vividnext.sodalive.content.like.AudioContentLike
|
||||||
|
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||||
|
import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||||
|
import org.springframework.context.annotation.Import
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import javax.persistence.EntityManager
|
||||||
|
|
||||||
|
@DataJpaTest(
|
||||||
|
properties = [
|
||||||
|
"spring.cache.type=none",
|
||||||
|
"spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;NON_KEYWORDS=VALUE"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@Import(QueryDslConfig::class)
|
||||||
|
class DefaultCreatorRankingAggregationRepositoryTest @Autowired constructor(
|
||||||
|
private val entityManager: EntityManager
|
||||||
|
) {
|
||||||
|
private val adapter = DefaultCreatorRankingAggregationRepository(entityManager)
|
||||||
|
private val startAt = LocalDateTime.of(2026, 5, 31, 15, 0)
|
||||||
|
private val endAt = LocalDateTime.of(2026, 6, 7, 15, 0)
|
||||||
|
private val inPeriod = LocalDateTime.of(2026, 6, 1, 0, 0)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("콘텐츠/라이브 캔은 사용 구분, 정산 상태, 환불 여부, UTC 기간으로 집계한다")
|
||||||
|
fun shouldAggregateLiveAndContentCanAmountsByUsageStatusRefundAndUtcPeriod() {
|
||||||
|
val creator = saveCreator("can-creator")
|
||||||
|
val user = saveUser("can-user")
|
||||||
|
saveUseCanCalculate(user, creator, CanUsage.DONATION, 10, UseCanCalculateStatus.RECEIVED, false, startAt)
|
||||||
|
saveUseCanCalculate(user, creator, CanUsage.LIVE, 20, UseCanCalculateStatus.RECEIVED, false, inPeriod)
|
||||||
|
saveUseCanCalculate(
|
||||||
|
user,
|
||||||
|
creator,
|
||||||
|
CanUsage.SPIN_ROULETTE,
|
||||||
|
30,
|
||||||
|
UseCanCalculateStatus.RECEIVED,
|
||||||
|
false,
|
||||||
|
endAt.minusSeconds(1)
|
||||||
|
)
|
||||||
|
saveUseCanCalculate(user, creator, CanUsage.ORDER_CONTENT, 40, UseCanCalculateStatus.RECEIVED, false, inPeriod)
|
||||||
|
saveUseCanCalculate(user, creator, CanUsage.DONATION, 100, UseCanCalculateStatus.RECEIVED, true, inPeriod)
|
||||||
|
saveUseCanCalculate(user, creator, CanUsage.LIVE, 200, UseCanCalculateStatus.CALCULATE_COMPLETE, false, inPeriod)
|
||||||
|
saveUseCanCalculate(
|
||||||
|
user,
|
||||||
|
creator,
|
||||||
|
CanUsage.ORDER_CONTENT,
|
||||||
|
300,
|
||||||
|
UseCanCalculateStatus.RECEIVED,
|
||||||
|
false,
|
||||||
|
startAt.minusSeconds(1)
|
||||||
|
)
|
||||||
|
saveUseCanCalculate(user, creator, CanUsage.ORDER_CONTENT, 400, UseCanCalculateStatus.RECEIVED, false, endAt)
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val candidate = aggregate().single()
|
||||||
|
|
||||||
|
assertEquals(60, candidate.liveCanAmount)
|
||||||
|
assertEquals(40, candidate.contentPurchaseCanAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("활성 콘텐츠의 활성 좋아요와 작성자 본인이 아닌 활성 댓글/대댓글만 집계한다")
|
||||||
|
fun shouldAggregateActiveContentLikesAndCommentsExcludingCreatorSelfResponses() {
|
||||||
|
val creator = saveCreator("engagement-creator")
|
||||||
|
val otherCreator = saveCreator("inactive-content-creator")
|
||||||
|
val user = saveUser("engagement-user")
|
||||||
|
val content = saveAudioContent(creator, isActive = true)
|
||||||
|
val inactiveContent = saveAudioContent(otherCreator, isActive = false)
|
||||||
|
saveUseCanCalculate(user, creator, CanUsage.DONATION, 10, UseCanCalculateStatus.RECEIVED, false, inPeriod)
|
||||||
|
saveContentLike(content, user, isActive = true, createdAt = inPeriod)
|
||||||
|
saveContentLike(content, user, isActive = false, createdAt = inPeriod)
|
||||||
|
saveContentLike(content, user, isActive = true, createdAt = startAt.minusSeconds(1))
|
||||||
|
saveContentLike(inactiveContent, user, isActive = true, createdAt = inPeriod)
|
||||||
|
val parent = saveContentComment(content, user, isActive = true, createdAt = inPeriod)
|
||||||
|
saveContentComment(content, user, parent = parent, isActive = true, createdAt = inPeriod)
|
||||||
|
saveContentComment(content, creator, isActive = true, createdAt = inPeriod)
|
||||||
|
saveContentComment(content, user, isActive = false, createdAt = inPeriod)
|
||||||
|
saveContentComment(content, user, isActive = true, createdAt = endAt)
|
||||||
|
saveContentComment(inactiveContent, user, isActive = true, createdAt = inPeriod)
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val candidate = aggregate().single { it.creatorId == creator.id }
|
||||||
|
|
||||||
|
assertEquals(1, candidate.contentLikeCount)
|
||||||
|
assertEquals(2, candidate.contentCommentCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("채널 후원 캔/건수와 최상위 활성 팬 Talk만 집계한다")
|
||||||
|
fun shouldAggregateChannelDonationAndTopLevelActiveFanTalks() {
|
||||||
|
val creator = saveCreator("support-creator")
|
||||||
|
val user = saveUser("support-user")
|
||||||
|
saveUseCanCalculate(user, creator, CanUsage.CHANNEL_DONATION, 100, UseCanCalculateStatus.RECEIVED, false, inPeriod)
|
||||||
|
saveUseCanCalculate(user, creator, CanUsage.CHANNEL_DONATION, 200, UseCanCalculateStatus.RECEIVED, false, inPeriod)
|
||||||
|
saveUseCanCalculate(user, creator, CanUsage.CHANNEL_DONATION, 300, UseCanCalculateStatus.RECEIVED, true, inPeriod)
|
||||||
|
saveUseCanCalculate(user, creator, CanUsage.CHANNEL_DONATION, 400, UseCanCalculateStatus.REFUND, false, inPeriod)
|
||||||
|
val topLevel = saveCreatorCheers(creator, user, isActive = true, createdAt = inPeriod)
|
||||||
|
saveCreatorCheers(creator, user, parent = topLevel, isActive = true, createdAt = inPeriod)
|
||||||
|
saveCreatorCheers(creator, user, isActive = false, createdAt = inPeriod)
|
||||||
|
saveCreatorCheers(creator, user, isActive = true, createdAt = endAt)
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val candidate = aggregate().single()
|
||||||
|
|
||||||
|
assertEquals(300, candidate.channelDonationCanAmount)
|
||||||
|
assertEquals(2, candidate.channelDonationCount)
|
||||||
|
assertEquals(1, candidate.fanTalkCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("팔로우 최종 활성 수와 현재 row 기준 생성/비활성 변경 증가 수를 집계한다")
|
||||||
|
fun shouldAggregateFinalFollowerCountAndFollowIncreaseFromCurrentRows() {
|
||||||
|
val creator = saveCreator("follow-creator")
|
||||||
|
val activeFollower = saveUser("active-follower")
|
||||||
|
val newFollower = saveUser("new-follower")
|
||||||
|
val unfollower = saveUser("unfollower")
|
||||||
|
val oldInactiveFollower = saveUser("old-inactive-follower")
|
||||||
|
saveUseCanCalculate(activeFollower, creator, CanUsage.DONATION, 10, UseCanCalculateStatus.RECEIVED, false, inPeriod)
|
||||||
|
saveFollowing(
|
||||||
|
creator,
|
||||||
|
activeFollower,
|
||||||
|
isActive = true,
|
||||||
|
createdAt = startAt.minusDays(5),
|
||||||
|
updatedAt = startAt.minusDays(5)
|
||||||
|
)
|
||||||
|
saveFollowing(creator, newFollower, isActive = true, createdAt = inPeriod, updatedAt = inPeriod)
|
||||||
|
saveFollowing(creator, unfollower, isActive = false, createdAt = startAt.minusDays(10), updatedAt = inPeriod)
|
||||||
|
saveFollowing(
|
||||||
|
creator,
|
||||||
|
oldInactiveFollower,
|
||||||
|
isActive = false,
|
||||||
|
createdAt = startAt.minusDays(10),
|
||||||
|
updatedAt = startAt.minusSeconds(1)
|
||||||
|
)
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val candidate = aggregate().single()
|
||||||
|
|
||||||
|
assertEquals(2, candidate.finalFollowerCount)
|
||||||
|
// 현재 CreatorFollowing row만으로 집계하므로 기간 내 재팔로우 이력은 별도 이벤트로 복원하지 않는다.
|
||||||
|
assertEquals(0, candidate.followIncrease)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("활성 크리에이터별 원천 지표를 합쳐 1점 이상 후보만 점수와 함께 반환한다")
|
||||||
|
fun shouldMergeMetricsForActiveCreatorsAndExcludeInactiveNonCreatorAndLowScoreCandidates() {
|
||||||
|
val creator = saveCreator("merged-creator", profileImage = "merged.png")
|
||||||
|
val lowScoreCreator = saveCreator("low-score-creator")
|
||||||
|
val inactiveCreator = saveCreator("inactive-creator", isActive = false)
|
||||||
|
val nonCreator = saveUser("non-creator")
|
||||||
|
val user = saveUser("merged-user")
|
||||||
|
saveUseCanCalculate(user, creator, CanUsage.DONATION, 10, UseCanCalculateStatus.RECEIVED, false, inPeriod)
|
||||||
|
saveContentLike(saveAudioContent(creator, isActive = true), user, isActive = true, createdAt = inPeriod)
|
||||||
|
saveUseCanCalculate(user, inactiveCreator, CanUsage.DONATION, 1000, UseCanCalculateStatus.RECEIVED, false, inPeriod)
|
||||||
|
saveUseCanCalculate(user, nonCreator, CanUsage.DONATION, 1000, UseCanCalculateStatus.RECEIVED, false, inPeriod)
|
||||||
|
saveFollowing(lowScoreCreator, user, isActive = true, createdAt = startAt.minusDays(1), updatedAt = startAt.minusDays(1))
|
||||||
|
flushAndClear()
|
||||||
|
|
||||||
|
val candidates = aggregate()
|
||||||
|
|
||||||
|
assertEquals(listOf(creator.id), candidates.map { it.creatorId })
|
||||||
|
val candidate = candidates.single()
|
||||||
|
assertEquals("merged-creator", candidate.nickname)
|
||||||
|
assertEquals("merged.png", candidate.profileImageUrl)
|
||||||
|
assertEquals(10, candidate.liveCanAmount)
|
||||||
|
assertEquals(0, candidate.contentPurchaseCanAmount)
|
||||||
|
assertEquals(1, candidate.contentLikeCount)
|
||||||
|
assertEquals(7.0, candidate.contentLiveScore, 0.0001)
|
||||||
|
assertEquals(0.5, candidate.engagementScore, 0.0001)
|
||||||
|
assertEquals(0.0, candidate.supportScore, 0.0001)
|
||||||
|
assertEquals(0.0, candidate.fanLoyaltyScore, 0.0001)
|
||||||
|
assertEquals(2.6, candidate.finalScore, 0.0001)
|
||||||
|
assertTrue(candidate.finalScore >= 1.0)
|
||||||
|
assertFalse(candidates.any { it.creatorId == lowScoreCreator.id })
|
||||||
|
assertFalse(candidates.any { it.creatorId == inactiveCreator.id })
|
||||||
|
assertFalse(candidates.any { it.creatorId == nonCreator.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aggregate() = adapter.aggregateCandidates(startAt, endAt)
|
||||||
|
|
||||||
|
private fun saveCreator(nickname: String, profileImage: String? = null, isActive: Boolean = true): Member {
|
||||||
|
return saveMember(nickname, MemberRole.CREATOR, profileImage, isActive)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveUser(nickname: String): Member {
|
||||||
|
return saveMember(nickname, MemberRole.USER, null, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveMember(nickname: String, role: MemberRole, profileImage: String?, isActive: Boolean): Member {
|
||||||
|
val member = Member(
|
||||||
|
email = "$nickname@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = nickname,
|
||||||
|
profileImage = profileImage,
|
||||||
|
role = role,
|
||||||
|
isActive = isActive
|
||||||
|
)
|
||||||
|
entityManager.persist(member)
|
||||||
|
entityManager.flush()
|
||||||
|
return member
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveAudioContent(creator: Member, isActive: Boolean): AudioContent {
|
||||||
|
val theme = AudioContentTheme(theme = "theme-${creator.nickname}", image = "theme.png")
|
||||||
|
entityManager.persist(theme)
|
||||||
|
val content = AudioContent(
|
||||||
|
title = "content-${creator.nickname}",
|
||||||
|
detail = "detail",
|
||||||
|
languageCode = "ko",
|
||||||
|
releaseDate = inPeriod
|
||||||
|
)
|
||||||
|
content.member = creator
|
||||||
|
content.theme = theme
|
||||||
|
content.isActive = isActive
|
||||||
|
entityManager.persist(content)
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveUseCanCalculate(
|
||||||
|
member: Member,
|
||||||
|
creator: Member,
|
||||||
|
usage: CanUsage,
|
||||||
|
can: Int,
|
||||||
|
status: UseCanCalculateStatus,
|
||||||
|
isRefund: Boolean,
|
||||||
|
createdAt: LocalDateTime
|
||||||
|
) {
|
||||||
|
val useCan = UseCan(canUsage = usage, can = can, rewardCan = 0, isRefund = isRefund)
|
||||||
|
useCan.member = member
|
||||||
|
entityManager.persist(useCan)
|
||||||
|
val calculate = UseCanCalculate(can = can, paymentGateway = PaymentGateway.PG, status = status)
|
||||||
|
calculate.useCan = useCan
|
||||||
|
calculate.recipientCreatorId = creator.id
|
||||||
|
entityManager.persist(calculate)
|
||||||
|
entityManager.flush()
|
||||||
|
updateTimestamps("use_can_calculate", calculate.id!!, createdAt, createdAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveContentLike(content: AudioContent, member: Member, isActive: Boolean, createdAt: LocalDateTime) {
|
||||||
|
val like = AudioContentLike(memberId = member.id!!)
|
||||||
|
like.audioContent = content
|
||||||
|
like.isActive = isActive
|
||||||
|
entityManager.persist(like)
|
||||||
|
entityManager.flush()
|
||||||
|
updateTimestamps("content_like", like.id!!, createdAt, createdAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveContentComment(
|
||||||
|
content: AudioContent,
|
||||||
|
member: Member,
|
||||||
|
parent: AudioContentComment? = null,
|
||||||
|
isActive: Boolean,
|
||||||
|
createdAt: LocalDateTime
|
||||||
|
): AudioContentComment {
|
||||||
|
val comment = AudioContentComment(comment = "comment", languageCode = "ko", isActive = isActive)
|
||||||
|
comment.audioContent = content
|
||||||
|
comment.member = member
|
||||||
|
comment.parent = parent
|
||||||
|
entityManager.persist(comment)
|
||||||
|
entityManager.flush()
|
||||||
|
updateTimestamps("content_comment", comment.id!!, createdAt, createdAt)
|
||||||
|
return comment
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveCreatorCheers(
|
||||||
|
creator: Member,
|
||||||
|
member: Member,
|
||||||
|
parent: CreatorCheers? = null,
|
||||||
|
isActive: Boolean,
|
||||||
|
createdAt: LocalDateTime
|
||||||
|
): CreatorCheers {
|
||||||
|
val cheers = CreatorCheers(cheers = "cheers", languageCode = "ko", isActive = isActive)
|
||||||
|
cheers.creator = creator
|
||||||
|
cheers.member = member
|
||||||
|
cheers.parent = parent
|
||||||
|
entityManager.persist(cheers)
|
||||||
|
entityManager.flush()
|
||||||
|
updateTimestamps("creator_cheers", cheers.id!!, createdAt, createdAt)
|
||||||
|
return cheers
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveFollowing(
|
||||||
|
creator: Member,
|
||||||
|
member: Member,
|
||||||
|
isActive: Boolean,
|
||||||
|
createdAt: LocalDateTime,
|
||||||
|
updatedAt: LocalDateTime
|
||||||
|
) {
|
||||||
|
val following = CreatorFollowing(isActive = isActive)
|
||||||
|
following.creator = creator
|
||||||
|
following.member = member
|
||||||
|
entityManager.persist(following)
|
||||||
|
entityManager.flush()
|
||||||
|
updateTimestamps("creator_following", following.id!!, createdAt, updatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateTimestamps(tableName: String, id: Long, createdAt: LocalDateTime, updatedAt: LocalDateTime) {
|
||||||
|
entityManager.createNativeQuery(
|
||||||
|
"update $tableName set created_at = :createdAt, updated_at = :updatedAt where id = :id"
|
||||||
|
)
|
||||||
|
.setParameter("createdAt", createdAt)
|
||||||
|
.setParameter("updatedAt", updatedAt)
|
||||||
|
.setParameter("id", id)
|
||||||
|
.executeUpdate()
|
||||||
|
entityManager.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flushAndClear() {
|
||||||
|
entityManager.flush()
|
||||||
|
entityManager.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user