Compare commits

...

4 Commits

Author SHA1 Message Date
klaus 24841b9850 Merge pull request 'fix: 코루틴 내 트랜잭션 간 조회 안 되는 문제 해결' (#318) from test into main
Reviewed-on: #318
2025-05-22 04:31:42 +00:00
Klaus e67b798714 fix: actionCount 를 조회할 때 endDate가 마지막 action 저장 이전의 시간이 측정될 수도 있어서 LocalDateTime.now()로 수정 2025-05-22 13:19:52 +09:00
Klaus dc13053825 fix: 구매하지 않은 콘텐츠에 댓글을 써도 ORDER_CONTENT_COMMENT 이벤트가 있으면 유저 행동 데이터에 기록되는 버그 수정 2025-05-22 13:01:39 +09:00
Klaus af352256e9 fix: 코루틴 내 트랜잭션 간 조회 안 되는 문제 해결
- 각 트랜잭션을 TransactionTemplate 블록으로 분리하여 커밋 시점 명확화
- 두 번째 트랜잭션에서 entityManager.clear() 호출로 1차 캐시 무시
- CoroutineExceptionHandler 추가로 비동기 예외 로깅 처리
- @PreDestroy 추가로 서비스 종료 시 CoroutineScope 정리
2025-05-22 12:25:17 +09:00
1 changed files with 138 additions and 90 deletions

View File

@ -1,8 +1,11 @@
package kr.co.vividnext.sodalive.useraction package kr.co.vividnext.sodalive.useraction
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kr.co.vividnext.sodalive.content.order.OrderRepository import kr.co.vividnext.sodalive.content.order.OrderRepository
import kr.co.vividnext.sodalive.fcm.FcmService import kr.co.vividnext.sodalive.fcm.FcmService
import kr.co.vividnext.sodalive.point.MemberPoint import kr.co.vividnext.sodalive.point.MemberPoint
@ -14,6 +17,8 @@ import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.support.TransactionTemplate import org.springframework.transaction.support.TransactionTemplate
import java.time.LocalDateTime import java.time.LocalDateTime
import javax.annotation.PreDestroy
import javax.persistence.EntityManager
@Service @Service
class UserActionService( class UserActionService(
@ -24,10 +29,15 @@ class UserActionService(
private val memberPointRepository: MemberPointRepository, private val memberPointRepository: MemberPointRepository,
private val transactionTemplate: TransactionTemplate, private val transactionTemplate: TransactionTemplate,
private val fcmService: FcmService private val fcmService: FcmService,
private val entityManager: EntityManager
) { ) {
private val coroutineScope = CoroutineScope(Dispatchers.IO) private val coroutineScope = CoroutineScope(
Dispatchers.IO + CoroutineExceptionHandler { _, e ->
logger.error("포인트 지급 또는 알림 실패: ${e.message}")
}
)
fun recordAction( fun recordAction(
memberId: Long, memberId: Long,
@ -39,113 +49,151 @@ class UserActionService(
) { ) {
coroutineScope.launch { coroutineScope.launch {
val now = LocalDateTime.now() val now = LocalDateTime.now()
transactionTemplate.execute { val policy = policyRepository.findByActionTypeAndIsActiveTrue(actionType, now)
repository.save( if (policy != null) {
UserActionLog( val policyType = policy.policyType
val todayAt15 = now.toLocalDate().atTime(15, 0)
val policyTypeDailyStartDate = if (now.toLocalTime().isBefore(todayAt15.toLocalTime())) {
now.toLocalDate().minusDays(1).atTime(15, 0)
} else {
todayAt15
}
val isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate =
policyType == PolicyType.DAILY && policyTypeDailyStartDate >= policy.startDate
val order = if (contentId != null) {
orderRepository.findByMemberIdAndContentId(
memberId = memberId, memberId = memberId,
actionType = actionType, contentId = contentId,
contentCommentId = contentCommentId createdAt = if (isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate) {
policyTypeDailyStartDate
} else {
policy.startDate
}
) )
) } else {
repository.flush() null
}
if (actionType == ActionType.ORDER_CONTENT_COMMENT && order == null) return@launch
} }
if (isAuth) { withContext(Dispatchers.IO) {
try { transactionTemplate.execute {
transactionTemplate.execute { repository.save(
val policy = policyRepository.findByActionTypeAndIsActiveTrue(actionType, now) UserActionLog(
if (policy != null) { memberId = memberId,
val policyType = policy.policyType actionType = actionType,
val todayAt15 = now.toLocalDate().atTime(15, 0) contentCommentId = contentCommentId
val policyTypeDailyStartDate = if (now.toLocalTime().isBefore(todayAt15.toLocalTime())) { )
now.toLocalDate().minusDays(1).atTime(15, 0) )
} else { repository.flush()
todayAt15 }
} }
val isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate = withContext(Dispatchers.IO) {
policyType == PolicyType.DAILY && policyTypeDailyStartDate >= policy.startDate if (isAuth) {
val order = if (contentId != null) { try {
orderRepository.findByMemberIdAndContentId( transactionTemplate.execute {
entityManager.clear()
if (policy != null) {
val policyType = policy.policyType
val todayAt15 = now.toLocalDate().atTime(15, 0)
val policyTypeDailyStartDate =
if (now.toLocalTime().isBefore(todayAt15.toLocalTime())) {
now.toLocalDate().minusDays(1).atTime(15, 0)
} else {
todayAt15
}
val isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate =
policyType == PolicyType.DAILY && policyTypeDailyStartDate >= policy.startDate
val order = if (contentId != null) {
orderRepository.findByMemberIdAndContentId(
memberId = memberId,
contentId = contentId,
createdAt = if (isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate) {
policyTypeDailyStartDate
} else {
policy.startDate
}
)
} else {
null
}
if (actionType == ActionType.ORDER_CONTENT_COMMENT && order == null) return@execute
val actionCount = repository.countByMemberIdAndActionTypeAndCreatedAtBetween(
memberId = memberId, memberId = memberId,
contentId = contentId, actionType = actionType,
createdAt = if (isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate) { startDate = if (isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate) {
policyTypeDailyStartDate policyTypeDailyStartDate
} else { } else {
policy.startDate policy.startDate
},
endDate = policy.endDate ?: LocalDateTime.now()
)
if (actionCount < policy.threshold) return@execute
val grantedCount = grantLogRepository.countByMemberIdAndPolicyIdAndStartDate(
memberId,
policy.id!!,
startDate = if (isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate) {
policyTypeDailyStartDate
} else {
policy.startDate
},
orderId = order?.id
)
if (grantedCount >= policy.availableCount) return@execute
val point = if (actionType == ActionType.ORDER_CONTENT_COMMENT && order != null) {
order.can
} else {
policy.pointAmount
}
if (point > 0) {
grantLogRepository.save(
PointGrantLog(
memberId = memberId,
point = point,
actionType = actionType,
policyId = policy.id!!,
orderId = order?.id
)
)
memberPointRepository.save(
MemberPoint(
memberId = memberId,
point = point,
actionType = actionType,
expiresAt = now.plusDays(3)
)
)
if (pushTokenList.isNotEmpty()) {
fcmService.sendPointGranted(
pushTokenList,
point
)
} }
)
} else {
null
}
if (actionType == ActionType.ORDER_CONTENT_COMMENT && order == null) return@execute
val actionCount = repository.countByMemberIdAndActionTypeAndCreatedAtBetween(
memberId = memberId,
actionType = actionType,
startDate = if (isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate) {
policyTypeDailyStartDate
} else {
policy.startDate
},
endDate = policy.endDate ?: now
)
if (actionCount < policy.threshold) return@execute
val grantedCount = grantLogRepository.countByMemberIdAndPolicyIdAndStartDate(
memberId,
policy.id!!,
startDate = if (isValidPolicyTypeDailyAndDailyStartDateAfterPolicyStartDate) {
policyTypeDailyStartDate
} else {
policy.startDate
},
orderId = order?.id
)
if (grantedCount >= policy.availableCount) return@execute
val point = if (actionType == ActionType.ORDER_CONTENT_COMMENT && order != null) {
order.can
} else {
policy.pointAmount
}
if (point > 0) {
grantLogRepository.save(
PointGrantLog(
memberId = memberId,
point = point,
actionType = actionType,
policyId = policy.id!!,
orderId = order?.id
)
)
memberPointRepository.save(
MemberPoint(
memberId = memberId,
point = point,
actionType = actionType,
expiresAt = now.plusDays(3)
)
)
if (pushTokenList.isNotEmpty()) {
fcmService.sendPointGranted(
pushTokenList,
point
)
} }
} }
} }
} catch (e: Exception) {
logger.warn("포인트 지급 또는 알림 실패: ${e.message}")
} }
} catch (e: Exception) {
logger.warn("포인트 지급 또는 알림 실패: ${e.message}")
} }
} }
} }
} }
@PreDestroy
fun onDestroy() {
coroutineScope.cancel("UserActionService 종료")
}
companion object { companion object {
private val logger = LoggerFactory.getLogger(UserActionService::class.java) private val logger = LoggerFactory.getLogger(UserActionService::class.java)
} }