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..87251d59 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 @@ -112,7 +112,7 @@ class EnvelopeFacade( customCategory = customCategory ).run { categoryAssignmentService.saveSync(this) } - publisher.publishEvent(CreateEnvelopeEvent(createdEnvelope, ledger)) + publisher.publishEvent(CreateEnvelopeEvent(createdEnvelope, ledger, user)) createdEnvelope } @@ -183,7 +183,7 @@ class EnvelopeFacade( this.customCategory = customCategory }.run { categoryAssignmentService.saveSync(this) } - publisher.publishEvent(UpdateEnvelopeEvent(updatedEnvelope)) + publisher.publishEvent(UpdateEnvelopeEvent(updatedEnvelope, user)) updatedEnvelope } @@ -226,7 +226,7 @@ class EnvelopeFacade( targetType = CategoryAssignmentType.ENVELOPE ) - DeleteEnvelopeEvent(envelope, user.uid) + DeleteEnvelopeEvent(envelope, user) .run { publisher.publishEvent(this) } } } diff --git a/api/src/main/kotlin/com/oksusu/susu/api/event/listener/EnvelopeEventListener.kt b/api/src/main/kotlin/com/oksusu/susu/api/event/listener/EnvelopeEventListener.kt index 24c8cf9c..763211e3 100644 --- a/api/src/main/kotlin/com/oksusu/susu/api/event/listener/EnvelopeEventListener.kt +++ b/api/src/main/kotlin/com/oksusu/susu/api/event/listener/EnvelopeEventListener.kt @@ -8,11 +8,13 @@ import com.oksusu.susu.api.event.model.DeleteEnvelopeEvent import com.oksusu.susu.api.event.model.UpdateEnvelopeEvent import com.oksusu.susu.api.friend.application.FriendRelationshipService import com.oksusu.susu.api.friend.application.FriendService +import com.oksusu.susu.api.statistic.application.UserEnvelopeStatisticService import com.oksusu.susu.client.common.coroutine.ErrorPublishingCoroutineExceptionHandler import com.oksusu.susu.common.extension.mdcCoroutineScope import com.oksusu.susu.common.extension.parZipWithMDC import com.oksusu.susu.domain.envelope.domain.vo.EnvelopeType import com.oksusu.susu.domain.envelope.infrastructure.LedgerRepository +import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -27,9 +29,13 @@ class EnvelopeEventListener( private val ledgerRepository: LedgerRepository, private val friendRelationshipService: FriendRelationshipService, private val coroutineExceptionHandler: ErrorPublishingCoroutineExceptionHandler, + private val userEnvelopeStatisticService: UserEnvelopeStatisticService, ) { + private val logger = KotlinLogging.logger { } + @TransactionalEventListener fun handle(event: CreateEnvelopeEvent) { + /** 장부 처리 집계 변경 */ event.ledger?.let { ledger -> when (event.envelope.type) { EnvelopeType.RECEIVED -> { @@ -43,6 +49,20 @@ class EnvelopeEventListener( ledgerService.saveSync(ledger) } + + /** 통계 캐싱 처리 */ + mdcCoroutineScope(Dispatchers.IO + Job() + coroutineExceptionHandler.handler, event.traceId).launch { + /** 통계 캐싱 안되어있으면 중단 */ + userEnvelopeStatisticService.getStatisticOrNull(event.user.uid) ?: run { return@launch } + + logger.info { "[${event.publishAt}] ${event.user.uid} refresh user envelope statistic 시작" } + + val statistic = userEnvelopeStatisticService.createUserEnvelopeStatistic(event.user.uid) + + userEnvelopeStatisticService.save(event.user.uid, statistic) + + logger.info { "[${event.publishAt}] ${event.user.uid} refresh user envelope statistic 끝" } + } } @TransactionalEventListener @@ -62,13 +82,27 @@ class EnvelopeEventListener( } } } + + /** 통계 캐싱 처리 */ + mdcCoroutineScope(Dispatchers.IO + Job() + coroutineExceptionHandler.handler, event.traceId).launch { + /** 통계 캐싱 안되어있으면 중단 */ + userEnvelopeStatisticService.getStatisticOrNull(event.user.uid) ?: run { return@launch } + + logger.info { "[${event.publishAt}] ${event.user.uid} refresh user envelope statistic 시작" } + + val statistic = userEnvelopeStatisticService.createUserEnvelopeStatistic(event.user.uid) + + userEnvelopeStatisticService.save(event.user.uid, statistic) + + logger.info { "[${event.publishAt}] ${event.user.uid} refresh user envelope statistic 끝" } + } } @TransactionalEventListener fun handel(event: DeleteEnvelopeEvent) { mdcCoroutineScope(Dispatchers.IO + Job() + coroutineExceptionHandler.handler, event.traceId).launch { val count = envelopeService.countByUidAndFriendId( - uid = event.uid, + uid = event.user.uid, friendId = event.envelope.friendId ) @@ -92,5 +126,19 @@ class EnvelopeEventListener( friendRelationshipService.deleteByFriendIdSync(event.envelope.friendId) } } + + /** 통계 캐싱 처리 */ + mdcCoroutineScope(Dispatchers.IO + Job() + coroutineExceptionHandler.handler, event.traceId).launch { + /** 통계 캐싱 안되어있으면 중단 */ + userEnvelopeStatisticService.getStatisticOrNull(event.user.uid) ?: run { return@launch } + + logger.info { "[${event.publishAt}] ${event.user.uid} refresh user envelope statistic 시작" } + + val statistic = userEnvelopeStatisticService.createUserEnvelopeStatistic(event.user.uid) + + userEnvelopeStatisticService.save(event.user.uid, statistic) + + logger.info { "[${event.publishAt}] ${event.user.uid} refresh user envelope statistic 끝" } + } } } diff --git a/api/src/main/kotlin/com/oksusu/susu/api/event/listener/SystemActionLogEventListener.kt b/api/src/main/kotlin/com/oksusu/susu/api/event/listener/SystemActionLogEventListener.kt index e0db9b85..641a8f77 100644 --- a/api/src/main/kotlin/com/oksusu/susu/api/event/listener/SystemActionLogEventListener.kt +++ b/api/src/main/kotlin/com/oksusu/susu/api/event/listener/SystemActionLogEventListener.kt @@ -19,15 +19,28 @@ class SystemActionLogEventListener( @EventListener fun subscribe(event: SystemActionLogEvent) { mdcCoroutineScope(Dispatchers.IO + Job() + coroutineExceptionHandler.handler, event.traceId).launch { - SystemActionLog( - ipAddress = event.ipAddress, - path = event.path, - httpMethod = event.method, - userAgent = event.userAgent, - host = event.host, - referer = event.referer, - extra = event.extra - ).run { systemActionLogService.record(this) } + if (check(event)) { + SystemActionLog( + ipAddress = event.ipAddress, + path = event.path, + httpMethod = event.method, + userAgent = event.userAgent, + host = event.host, + referer = event.referer, + extra = event.extra + ).run { systemActionLogService.record(this) } + } } } + + private fun check(event: SystemActionLogEvent): Boolean { + return !NON_TARGET_PATH.contains(event.path) + } + + companion object { + private val NON_TARGET_PATH = setOf( + "/api/v1/health", + "/health" + ) + } } 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..903443e8 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 @@ -1,5 +1,6 @@ package com.oksusu.susu.api.event.model +import com.oksusu.susu.api.auth.model.AuthUser import com.oksusu.susu.api.extension.remoteIp import com.oksusu.susu.cache.model.OidcPublicKeysCacheModel import com.oksusu.susu.cache.statistic.domain.UserEnvelopeStatistic @@ -103,15 +104,17 @@ data class CacheUserEnvelopeStatisticEvent( data class CreateEnvelopeEvent( val envelope: Envelope, val ledger: Ledger?, + val user: AuthUser, ) : BaseEvent() data class UpdateEnvelopeEvent( val envelope: Envelope, + val user: AuthUser, ) : BaseEvent() data class DeleteEnvelopeEvent( val envelope: Envelope, - val uid: Long, + val user: AuthUser, ) : BaseEvent() data class CreateUserWithdrawEvent( diff --git a/api/src/main/kotlin/com/oksusu/susu/api/health/presentation/HealthResource.kt b/api/src/main/kotlin/com/oksusu/susu/api/health/presentation/HealthResource.kt index bbf78e78..bee7d2ca 100644 --- a/api/src/main/kotlin/com/oksusu/susu/api/health/presentation/HealthResource.kt +++ b/api/src/main/kotlin/com/oksusu/susu/api/health/presentation/HealthResource.kt @@ -22,4 +22,10 @@ class HealthResource( fun healthCheckV1() = environment.activeProfiles.first() .run { HealthResponse.from(this) } .wrapOk() + + @Operation(summary = "health-check") + @GetMapping("/health") + fun healthCheck() = environment.activeProfiles.first() + .run { HealthResponse.from(this) } + .wrapOk() } 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 { diff --git a/gradle.properties b/gradle.properties index eed77adc..234be870 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=1.1.9 +version=1.1.10 kotlin.code.style=official