diff --git a/api/src/main/kotlin/com/oksusu/susu/api/envelope/application/EnvelopeFacade.kt b/api/src/main/kotlin/com/oksusu/susu/api/envelope/application/EnvelopeFacade.kt index 41c060fb..2857dc9a 100644 --- a/api/src/main/kotlin/com/oksusu/susu/api/envelope/application/EnvelopeFacade.kt +++ b/api/src/main/kotlin/com/oksusu/susu/api/envelope/application/EnvelopeFacade.kt @@ -16,6 +16,7 @@ import com.oksusu.susu.api.envelope.model.response.GetFriendStatisticsResponse import com.oksusu.susu.api.envelope.model.response.SearchEnvelopeResponse import com.oksusu.susu.api.event.model.CreateEnvelopeEvent import com.oksusu.susu.api.event.model.DeleteEnvelopeEvent +import com.oksusu.susu.api.event.model.RefreshUserEnvelopeStatisticEvent import com.oksusu.susu.api.event.model.UpdateEnvelopeEvent import com.oksusu.susu.api.friend.application.FriendRelationshipService import com.oksusu.susu.api.friend.application.FriendService @@ -113,6 +114,7 @@ class EnvelopeFacade( ).run { categoryAssignmentService.saveSync(this) } publisher.publishEvent(CreateEnvelopeEvent(createdEnvelope, ledger)) + publisher.publishEvent(RefreshUserEnvelopeStatisticEvent(user.uid)) createdEnvelope } diff --git a/api/src/main/kotlin/com/oksusu/susu/api/event/listener/StatisticEventListener.kt b/api/src/main/kotlin/com/oksusu/susu/api/event/listener/StatisticEventListener.kt index 3c8fa93c..73f465ab 100644 --- a/api/src/main/kotlin/com/oksusu/susu/api/event/listener/StatisticEventListener.kt +++ b/api/src/main/kotlin/com/oksusu/susu/api/event/listener/StatisticEventListener.kt @@ -2,6 +2,7 @@ package com.oksusu.susu.api.event.listener import com.oksusu.susu.api.common.aspect.SusuEventListener import com.oksusu.susu.api.event.model.CacheUserEnvelopeStatisticEvent +import com.oksusu.susu.api.event.model.RefreshUserEnvelopeStatisticEvent import com.oksusu.susu.api.statistic.application.UserEnvelopeStatisticService import com.oksusu.susu.client.common.coroutine.ErrorPublishingCoroutineExceptionHandler import com.oksusu.susu.common.extension.mdcCoroutineScope @@ -10,6 +11,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.springframework.context.event.EventListener +import org.springframework.transaction.event.TransactionalEventListener @SusuEventListener class StatisticEventListener( @@ -28,4 +30,20 @@ class StatisticEventListener( logger.info { "[${event.publishAt}] ${event.uid} 유저 봉투 통계 캐싱 끝" } } } + + @TransactionalEventListener + fun createUserWithdrawService(event: RefreshUserEnvelopeStatisticEvent) { + mdcCoroutineScope(Dispatchers.IO + Job() + coroutineExceptionHandler.handler, event.traceId).launch { + /** 통계 캐싱 안되어있으면 중단 */ + userEnvelopeStatisticService.getStatisticOrNull(event.uid) ?: run { return@launch } + + logger.info { "[${event.publishAt}] ${event.uid} refresh user envelope statistic 시작" } + + val statistic = userEnvelopeStatisticService.createUserEnvelopeStatistic(event.uid) + + userEnvelopeStatisticService.save(event.uid, statistic) + + logger.info { "[${event.publishAt}] ${event.uid} refresh user envelope statistic 끝" } + } + } } diff --git a/api/src/main/kotlin/com/oksusu/susu/api/event/model/Event.kt b/api/src/main/kotlin/com/oksusu/susu/api/event/model/Event.kt index 43151fa4..de7fd6cd 100644 --- a/api/src/main/kotlin/com/oksusu/susu/api/event/model/Event.kt +++ b/api/src/main/kotlin/com/oksusu/susu/api/event/model/Event.kt @@ -121,3 +121,7 @@ data class CreateUserWithdrawEvent( data class CacheAppleOidcPublicKeysEvent( val keys: OidcPublicKeysCacheModel, ) : BaseEvent() + +data class RefreshUserEnvelopeStatisticEvent( + val uid: Long, +) : BaseEvent() diff --git a/api/src/main/kotlin/com/oksusu/susu/api/statistic/application/StatisticFacade.kt b/api/src/main/kotlin/com/oksusu/susu/api/statistic/application/StatisticFacade.kt index ab6a54ff..a5a54e71 100644 --- a/api/src/main/kotlin/com/oksusu/susu/api/statistic/application/StatisticFacade.kt +++ b/api/src/main/kotlin/com/oksusu/susu/api/statistic/application/StatisticFacade.kt @@ -9,11 +9,9 @@ import com.oksusu.susu.api.statistic.model.response.UserEnvelopeStatisticRespons import com.oksusu.susu.api.statistic.model.vo.SusuEnvelopeStatisticRequest import com.oksusu.susu.cache.statistic.domain.UserEnvelopeStatistic import com.oksusu.susu.common.extension.parZipWithMDC -import com.oksusu.susu.common.extension.yearMonth import io.github.oshai.kotlinlogging.KotlinLogging import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service -import java.time.LocalDate @Service class StatisticFacade( @@ -22,7 +20,6 @@ class StatisticFacade( private val susuEnvelopeStatisticService: SusuEnvelopeStatisticService, private val susuSpecificEnvelopeStatisticService: SusuSpecificEnvelopeStatisticService, private val relationshipService: RelationshipService, - private val envelopeStatisticService: EnvelopeStatisticService, private val eventPublisher: ApplicationEventPublisher, ) { private val logger = KotlinLogging.logger { } @@ -35,53 +32,29 @@ class StatisticFacade( return UserEnvelopeStatisticResponse.from(this) } - val userEnvelopeStatistic = parZipWithMDC( - /** 최근 사용 금액 1년 */ - { envelopeStatisticService.getRecentSpentFor1Year(user.uid) }, - /** 최다 수수 관계 */ - { envelopeStatisticService.getMostFrequentRelationship(user.uid) }, - /** 최다 수수 경조사 */ - { envelopeStatisticService.getMostFrequentCategory(user.uid) }, - /** 가장 많이 받은 금액 */ - { envelopeStatisticService.getMaxReceivedEnvelope(user.uid) }, - /** 가장 많이 보낸 금액 */ - { envelopeStatisticService.getMaxSentEnvelope(user.uid) } - ) { - recentSpent, - mostFrequentRelationShip, - mostFrequentCategory, - maxReceivedEnvelope, - maxSentEnvelope, - -> + val userEnvelopeStatistic = createUserEnvelopeStatistic(user.uid) - /** 최근 사용 금액 8달 */ - val before8Month = LocalDate.now().minusMonths(7).yearMonth() - val recentSpentForLast8Months = recentSpent?.filter { spent -> - spent.title >= before8Month - } + return UserEnvelopeStatisticResponse.from(userEnvelopeStatistic) + } - /** 경조사비 가장 많이 쓴 달 */ - val mostSpentMonth = recentSpent?.maxBy { model -> model.value }?.title?.substring(4)?.toLong() + suspend fun refreshUserEnvelopeStatistic(user: AuthUser): UserEnvelopeStatisticResponse { + val userEnvelopeStatistic = createUserEnvelopeStatistic(user.uid) - UserEnvelopeStatistic( - recentSpent = recentSpentForLast8Months, - mostSpentMonth = mostSpentMonth, - mostFrequentRelationShip = mostFrequentRelationShip, - mostFrequentCategory = mostFrequentCategory, - maxReceivedEnvelope = maxReceivedEnvelope, - maxSentEnvelope = maxSentEnvelope - ) - } + return UserEnvelopeStatisticResponse.from(userEnvelopeStatistic) + } + + private suspend fun createUserEnvelopeStatistic(uid: Long): UserEnvelopeStatistic { + val userEnvelopeStatistic = userEnvelopeStatisticService.createUserEnvelopeStatistic(uid) /** 유저 봉투 통계 캐싱 */ eventPublisher.publishEvent( CacheUserEnvelopeStatisticEvent( - uid = user.uid, + uid = uid, statistic = userEnvelopeStatistic ) ) - return UserEnvelopeStatisticResponse.from(userEnvelopeStatistic) + return userEnvelopeStatistic } suspend fun getSusuEnvelopeStatistic(requestParam: SusuEnvelopeStatisticRequest): SusuEnvelopeStatisticResponse { diff --git a/api/src/main/kotlin/com/oksusu/susu/api/statistic/application/UserEnvelopeStatisticService.kt b/api/src/main/kotlin/com/oksusu/susu/api/statistic/application/UserEnvelopeStatisticService.kt index 2de5949f..94430fb7 100644 --- a/api/src/main/kotlin/com/oksusu/susu/api/statistic/application/UserEnvelopeStatisticService.kt +++ b/api/src/main/kotlin/com/oksusu/susu/api/statistic/application/UserEnvelopeStatisticService.kt @@ -2,14 +2,58 @@ package com.oksusu.susu.api.statistic.application import com.oksusu.susu.cache.statistic.domain.UserEnvelopeStatistic import com.oksusu.susu.cache.statistic.infrastructure.UserEnvelopeStatisticRepository +import com.oksusu.susu.common.extension.parZipWithMDC import com.oksusu.susu.common.extension.withMDCContext +import com.oksusu.susu.common.extension.yearMonth import kotlinx.coroutines.Dispatchers import org.springframework.stereotype.Service +import java.time.LocalDate @Service class UserEnvelopeStatisticService( private val userEnvelopeStatisticRepository: UserEnvelopeStatisticRepository, + private val envelopeStatisticService: EnvelopeStatisticService, ) { + suspend fun createUserEnvelopeStatistic(uid: Long): UserEnvelopeStatistic { + return parZipWithMDC( + /** 최근 사용 금액 1년 */ + { envelopeStatisticService.getRecentSpentFor1Year(uid) }, + /** 최다 수수 관계 */ + { envelopeStatisticService.getMostFrequentRelationship(uid) }, + /** 최다 수수 경조사 */ + { envelopeStatisticService.getMostFrequentCategory(uid) }, + /** 가장 많이 받은 금액 */ + { envelopeStatisticService.getMaxReceivedEnvelope(uid) }, + /** 가장 많이 보낸 금액 */ + { envelopeStatisticService.getMaxSentEnvelope(uid) } + ) { + recentSpent, + mostFrequentRelationShip, + mostFrequentCategory, + maxReceivedEnvelope, + maxSentEnvelope, + -> + + /** 최근 사용 금액 8달 */ + val before8Month = LocalDate.now().minusMonths(7).yearMonth() + val recentSpentForLast8Months = recentSpent?.filter { spent -> + spent.title >= before8Month + } + + /** 경조사비 가장 많이 쓴 달 */ + val mostSpentMonth = recentSpent?.maxBy { model -> model.value }?.title?.substring(4)?.toLong() + + UserEnvelopeStatistic( + recentSpent = recentSpentForLast8Months, + mostSpentMonth = mostSpentMonth, + mostFrequentRelationShip = mostFrequentRelationShip, + mostFrequentCategory = mostFrequentCategory, + maxReceivedEnvelope = maxReceivedEnvelope, + maxSentEnvelope = maxSentEnvelope + ) + } + } + suspend fun save(uid: Long, userEnvelopeStatistic: UserEnvelopeStatistic) { return withMDCContext(Dispatchers.IO) { userEnvelopeStatisticRepository.save( diff --git a/api/src/main/kotlin/com/oksusu/susu/api/statistic/presentation/StatisticResource.kt b/api/src/main/kotlin/com/oksusu/susu/api/statistic/presentation/StatisticResource.kt index b6133a01..b5107437 100644 --- a/api/src/main/kotlin/com/oksusu/susu/api/statistic/presentation/StatisticResource.kt +++ b/api/src/main/kotlin/com/oksusu/susu/api/statistic/presentation/StatisticResource.kt @@ -25,6 +25,12 @@ class StatisticResource( user: AuthUser, ) = statisticFacade.getUserEnvelopeStatistic(user).wrapOk() + @Operation(summary = "나의 통계 새로고침") + @GetMapping("/mine/envelope/refresh") + suspend fun refreshUserEnvelopeStatistic( + user: AuthUser, + ) = statisticFacade.refreshUserEnvelopeStatistic(user).wrapOk() + @Operation(summary = "수수 통계") @GetMapping("/susu/envelope") suspend fun getSusuEnvelopeStatistic( diff --git a/batch/src/main/kotlin/com/oksusu/susu/batch/user/scheduler/UserScheduler.kt b/batch/src/main/kotlin/com/oksusu/susu/batch/user/scheduler/UserScheduler.kt index b4457d7a..efcb5110 100644 --- a/batch/src/main/kotlin/com/oksusu/susu/batch/user/scheduler/UserScheduler.kt +++ b/batch/src/main/kotlin/com/oksusu/susu/batch/user/scheduler/UserScheduler.kt @@ -7,7 +7,6 @@ import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component @Component @@ -17,7 +16,7 @@ class UserScheduler( ) { private val logger = KotlinLogging.logger { } - @Scheduled(cron = "0 0 3 * * *") +// @Scheduled(cron = "0 0 3 * * *") fun deleteWithdrawUserData() { CoroutineScope(Dispatchers.IO + coroutineExceptionHandler.handler).launch { runCatching {