Skip to content

Commit

Permalink
Merge pull request #79 from Weit-2nd/feature/ROFO-184
Browse files Browse the repository at this point in the history
[Feature/ROFO-184] 종합랭킹
  • Loading branch information
hyunjungkimm authored Sep 14, 2024
2 parents aedb3ba + 2934cd7 commit 31e78cd
Show file tree
Hide file tree
Showing 10 changed files with 282 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import kr.weit.roadyfoody.ranking.utils.REPORT_RANKING_KEY
import kr.weit.roadyfoody.ranking.utils.REPORT_RANKING_UPDATE_LOCK
import kr.weit.roadyfoody.ranking.utils.REVIEW_RANKING_KEY
import kr.weit.roadyfoody.ranking.utils.REVIEW_RANKING_UPDATE_LOCK
import kr.weit.roadyfoody.ranking.utils.TOTAL_RANKING_KEY
import kr.weit.roadyfoody.ranking.utils.TOTAL_RANKING_UPDATE_LOCK
import kr.weit.roadyfoody.review.repository.FoodSpotsReviewRepository
import org.redisson.api.RLock
import org.redisson.api.RedissonClient
Expand Down Expand Up @@ -54,6 +56,16 @@ class RankingCommandService(
)
}

@Async("asyncTask")
@Scheduled(cron = "0 0 5 * * *")
fun updateTotalRanking() {
updateRanking(
lockName = TOTAL_RANKING_UPDATE_LOCK,
key = TOTAL_RANKING_KEY,
dataProvider = reviewRepository::findAllUserTotalCount,
)
}

fun updateRanking(
lockName: String,
key: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,21 @@ import kr.weit.roadyfoody.ranking.utils.REPORT_RANKING_KEY
import kr.weit.roadyfoody.ranking.utils.REPORT_RANKING_UPDATE_LOCK
import kr.weit.roadyfoody.ranking.utils.REVIEW_RANKING_KEY
import kr.weit.roadyfoody.ranking.utils.REVIEW_RANKING_UPDATE_LOCK
import kr.weit.roadyfoody.ranking.utils.TOTAL_RANKING_KEY
import kr.weit.roadyfoody.ranking.utils.TOTAL_RANKING_UPDATE_LOCK
import kr.weit.roadyfoody.review.repository.FoodSpotsReviewRepository
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Service
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutorService

@Service
class RankingQueryService(
private val redisTemplate: RedisTemplate<String, String>,
private val foodSpotsHistoryRepository: FoodSpotsHistoryRepository,
private val reviewRepository: FoodSpotsReviewRepository,
private val rankingCommandService: RankingCommandService,
private val executor: ExecutorService,
) {
fun getReportRanking(size: Long): List<UserRanking> =
getRanking(
Expand All @@ -45,6 +49,14 @@ class RankingQueryService(
dataProvider = reviewRepository::findAllUserLikeCount,
)

fun getTotalRanking(size: Long): List<UserRanking> =
getRanking(
lockName = TOTAL_RANKING_UPDATE_LOCK,
size = size,
key = TOTAL_RANKING_KEY,
dataProvider = reviewRepository::findAllUserTotalCount,
)

private fun getRanking(
lockName: String,
size: Long,
Expand All @@ -57,13 +69,13 @@ class RankingQueryService(
.range(key, 0, size - 1)

if (ranking.isNullOrEmpty()) {
CompletableFuture.runAsync {
CompletableFuture.runAsync({
rankingCommandService.updateRanking(
lockName = lockName,
key = key,
dataProvider = dataProvider,
)
}
}, executor)
throw RankingNotFoundException()
}
return ranking.map { score ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,11 @@ class RankingController(
@RequestParam(defaultValue = "10")
size: Long,
): List<UserRanking> = rankingQueryService.getLikeRanking(size)

@GetMapping("total")
override fun getTotalRanking(
@Positive(message = "size는 양수여야 합니다")
@RequestParam(defaultValue = "10")
size: Long,
): List<UserRanking> = rankingQueryService.getTotalRanking(size)
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,33 @@ interface RankingControllerSpec {
@RequestParam(defaultValue = "10")
size: Long,
): List<UserRanking>

@ApiErrorCodeExamples(
[
ErrorCode.SIZE_NON_POSITIVE,
],
)
@Operation(
description = "종합 랭킹 조회 API",
responses = [
ApiResponse(
responseCode = "200",
description = "종합 랭킹 조회 성공",
content = [
Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema =
Schema(
implementation = UserRanking::class,
),
),
],
),
],
)
fun getTotalRanking(
@Positive(message = "size는 양수여야 합니다")
@RequestParam(defaultValue = "10")
size: Long,
): List<UserRanking>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package kr.weit.roadyfoody.ranking.utils
const val REPORT_RANKING_KEY = "rofo:user-report-ranking"
const val REVIEW_RANKING_KEY = "rofo:user-review-ranking"
const val LIKE_RANKING_KEY = "rofo:user-like-ranking"
const val TOTAL_RANKING_KEY = "rofo:user-total-ranking"
const val REPORT_RANKING_UPDATE_LOCK = "updateReportRankingLock"
const val REVIEW_RANKING_UPDATE_LOCK = "updateReviewRankingLock"
const val LIKE_RANKING_UPDATE_LOCK = "updateLikeRankingLock"
const val TOTAL_RANKING_UPDATE_LOCK = "totalLikeRankingLock"
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package kr.weit.roadyfoody.review.repository

import com.linecorp.kotlinjdsl.dsl.jpql.Jpql
import com.linecorp.kotlinjdsl.querymodel.jpql.expression.Expressions
import com.linecorp.kotlinjdsl.querymodel.jpql.predicate.Predicate
import com.linecorp.kotlinjdsl.querymodel.jpql.sort.Sortable
import com.linecorp.kotlinjdsl.support.spring.data.jpa.repository.KotlinJdslJpqlExecutor
import kr.weit.roadyfoody.badge.domain.Badge
import kr.weit.roadyfoody.foodSpots.application.dto.CountRate
import kr.weit.roadyfoody.foodSpots.application.dto.ReviewAggregatedInfoResponse
import kr.weit.roadyfoody.foodSpots.domain.FoodSpots
import kr.weit.roadyfoody.foodSpots.domain.FoodSpotsHistory
import kr.weit.roadyfoody.global.utils.findList
import kr.weit.roadyfoody.global.utils.findMutableList
import kr.weit.roadyfoody.global.utils.getSlice
Expand Down Expand Up @@ -55,6 +57,8 @@ interface CustomFoodSpotsReviewRepository {
fun findAllUserLikeCount(): List<UserRanking>

fun getRatingCount(foodSpotsId: Long): List<CountRate>

fun findAllUserTotalCount(): List<UserRanking>
}

class CustomFoodSpotsReviewRepositoryImpl(
Expand Down Expand Up @@ -184,6 +188,72 @@ class CustomFoodSpotsReviewRepositoryImpl(
)
}

override fun findAllUserTotalCount(): List<UserRanking> =
kotlinJdslJpqlExecutor
.findList {
val foodSpotsReview = entity(FoodSpotsReview::class, "foodSpotsReview")
val foodSpotsHistory = entity(FoodSpotsHistory::class, "foodSpotsHistory")
val reviewLike = entity(ReviewLike::class, "reviewLike")

val subquery =
select<Long>(
coalesce(
count(
foodSpotsReview(FoodSpotsReview::id),
).plus(sum(foodSpotsReview(FoodSpotsReview::likeTotal))),
0,
),
).from(
foodSpotsReview,
).where(foodSpotsReview(FoodSpotsReview::user)(User::id).eq(entity(User::class)(User::id)))
.asSubquery()

val subquery2 =
select<Long>(
coalesce(count(foodSpotsHistory(FoodSpotsHistory::id)), 0),
).from(
foodSpotsHistory,
).where(foodSpotsHistory(FoodSpotsHistory::user)(User::id).eq(entity(User::class)(User::id)))
.asSubquery()

val defaultDate = LocalDateTime.parse("1970-12-31T00:00:00")

val maxReviewDate = coalesce(max(foodSpotsReview(FoodSpotsReview::createdDateTime)), defaultDate)
val maxHistoryDate = coalesce(max(foodSpotsHistory(FoodSpotsHistory::createdDateTime)), defaultDate)
val maxLikeDate = coalesce(max(reviewLike(ReviewLike::createdDateTime)), defaultDate)

val greatestDateExpression =
Expressions.customExpression(
LocalDateTime::class,
"GREATEST({0}, {1}, {2})",
listOf(
maxReviewDate,
maxHistoryDate,
maxLikeDate,
),
)
val total = expression(Long::class, "total")
selectNew<UserRanking>(
path(User::profile)(Profile::nickname),
subquery2.plus(subquery).`as`(total),
).from(
entity(User::class),
leftJoin(foodSpotsHistory).on(foodSpotsHistory(FoodSpotsHistory::user)(User::id).eq(path(User::id))),
leftJoin(
foodSpotsReview,
).on(foodSpotsReview(FoodSpotsReview::user)(User::id).eq(path(User::id))),
leftJoin(
reviewLike,
).on(reviewLike(ReviewLike::review)(FoodSpotsReview::id).eq(foodSpotsReview(FoodSpotsReview::id))),
).groupBy(
path(User::id),
path(User::profile)(Profile::nickname),
).orderBy(
total.desc(),
greatestDateExpression.asc(),
)
}

private fun Jpql.dynamicOrder(sortType: ReviewSortType): Array<Sortable> =
when (sortType) {
ReviewSortType.LATEST -> arrayOf(path(FoodSpotsReview::id).desc())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import io.mockk.mockk
import io.mockk.verify
import kr.weit.roadyfoody.foodSpots.repository.FoodSpotsHistoryRepository
import kr.weit.roadyfoody.ranking.fixture.createUserRankingResponse
import kr.weit.roadyfoody.ranking.utils.LIKE_RANKING_KEY
import kr.weit.roadyfoody.ranking.utils.REPORT_RANKING_KEY
import kr.weit.roadyfoody.ranking.utils.REVIEW_RANKING_KEY
import kr.weit.roadyfoody.ranking.utils.TOTAL_RANKING_KEY
import kr.weit.roadyfoody.review.repository.FoodSpotsReviewRepository
import org.redisson.api.RLock
import org.redisson.api.RedissonClient
Expand Down Expand Up @@ -39,7 +43,7 @@ class RankingCommandServiceTest :
`when`("Lock을 획득한 경우") {
every { lock.tryLock(0, 10, TimeUnit.MINUTES) } returns true

every { redisTemplate.delete("rofo:user-report-ranking") } returns true
every { redisTemplate.delete(REPORT_RANKING_KEY) } returns true
every { foodSpotsHistoryRepository.findAllUserReportCount() } returns createUserRankingResponse()
every { redisTemplate.opsForList() } returns list
every { list.rightPushAll(any(), any<List<String>>()) } returns 1L
Expand Down Expand Up @@ -67,7 +71,7 @@ class RankingCommandServiceTest :
`when`("Lock을 획득한 경우") {
every { lock.tryLock(0, 10, TimeUnit.MINUTES) } returns true

every { redisTemplate.delete("rofo:user-review-ranking") } returns true
every { redisTemplate.delete(REVIEW_RANKING_KEY) } returns true
every { reviewRepository.findAllUserReviewCount() } returns createUserRankingResponse()
every { redisTemplate.opsForList() } returns list
every { list.rightPushAll(any(), any<List<String>>()) } returns 1L
Expand Down Expand Up @@ -95,7 +99,7 @@ class RankingCommandServiceTest :
`when`("Lock을 획득한 경우") {
every { lock.tryLock(0, 10, TimeUnit.MINUTES) } returns true

every { redisTemplate.delete("rofo:user-like-ranking") } returns true
every { redisTemplate.delete(LIKE_RANKING_KEY) } returns true
every { reviewRepository.findAllUserLikeCount() } returns createUserRankingResponse()
every { redisTemplate.opsForList() } returns list
every { list.rightPushAll(any(), any<List<String>>()) } returns 1L
Expand All @@ -115,5 +119,33 @@ class RankingCommandServiceTest :
}
}
}

given("updateTotalRanking 테스트") {
every { redissonClient.getLock(any<String>()) } returns lock
afterEach { clearMocks(reviewRepository) }

`when`("Lock을 획득한 경우") {
every { lock.tryLock(0, 10, TimeUnit.MINUTES) } returns true

every { redisTemplate.delete(TOTAL_RANKING_KEY) } returns true
every { reviewRepository.findAllUserTotalCount() } returns createUserRankingResponse()
every { redisTemplate.opsForList() } returns list
every { list.rightPushAll(any(), any<List<String>>()) } returns 1L

then("레디스의 데이터가 정상적으로 업데이트된다.") {
rankingCommandService.updateTotalRanking()
verify(exactly = 1) { reviewRepository.findAllUserTotalCount() }
}
}

`when`("Lock을 획득하지 못한 경우") {
every { lock.tryLock(0, 10, TimeUnit.MINUTES) } returns false

then("레디스의 데이터가 업데이트되지 않는다.") {
rankingCommandService.updateTotalRanking()
verify(exactly = 0) { reviewRepository.findAllUserTotalCount() }
}
}
}
},
)
Loading

0 comments on commit 31e78cd

Please sign in to comment.