feat(audio-recommendation): 추천 조회 snapshot fallback을 적용한다
This commit is contained in:
@@ -2,33 +2,67 @@ package kr.co.vividnext.sodalive.v2.audio.recommendation.application
|
||||
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.isAdultVisibleByPolicy
|
||||
import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.AudioRecommendationVisibility
|
||||
import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.AudioRecommendations
|
||||
import kr.co.vividnext.sodalive.v2.audio.recommendation.port.out.AudioRecommendationQueryPort
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotPort
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord
|
||||
import org.redisson.api.RedissonClient
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.Duration
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
|
||||
@Service
|
||||
class AudioRecommendationQueryService(
|
||||
private val queryPort: AudioRecommendationQueryPort,
|
||||
private val memberContentPreferenceService: MemberContentPreferenceService
|
||||
private val memberContentPreferenceService: MemberContentPreferenceService,
|
||||
private val snapshotPort: RecommendationSnapshotPort,
|
||||
private val snapshotRefreshService: AudioRecommendationSnapshotRefreshService,
|
||||
private val redissonClient: RedissonClient
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
fun getRecommendations(member: Member?): AudioRecommendations {
|
||||
val now = LocalDateTime.now()
|
||||
val canViewAdultContent = canViewAdultContent(member)
|
||||
val visibility = if (canViewAdultContent) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
|
||||
val memberId = member?.id
|
||||
val newAndHotSectionType = newAndHotSectionType(visibility)
|
||||
val newAndHotSnapshots = snapshotPort.findLatestSnapshots(newAndHotSectionType, limit = NEW_AND_HOT_AUDIO_LIMIT)
|
||||
val mostCommentedSnapshots = snapshotPort.findLatestSnapshots(
|
||||
mostCommentedSectionType(visibility),
|
||||
limit = MOST_COMMENTED_AUDIO_LIMIT
|
||||
)
|
||||
val recommendedSnapshots = snapshotPort.findLatestSnapshots(
|
||||
recommendedAudioSectionType(visibility),
|
||||
limit = RECOMMENDED_AUDIO_LIMIT
|
||||
)
|
||||
val refreshedNewAndHotSnapshots = refreshMissingNewAndHotSnapshots(newAndHotSectionType, newAndHotSnapshots)
|
||||
|
||||
return AudioRecommendations(
|
||||
banners = queryPort.findBanners(BANNER_LIMIT, member?.id, canViewAdultContent),
|
||||
originalSeries = queryPort.findOriginalSeries(ORIGINAL_SERIES_LIMIT, member?.id, canViewAdultContent, now),
|
||||
latestAudios = queryPort.findLatestAudios(LATEST_AUDIO_LIMIT, member?.id, canViewAdultContent, now),
|
||||
newAndHotAudios = emptyList(),
|
||||
freeAudios = queryPort.findFreeAudios(FREE_AUDIO_LIMIT, member?.id, canViewAdultContent, now),
|
||||
pointAudios = queryPort.findPointAudios(POINT_AUDIO_LIMIT, member?.id, canViewAdultContent, now),
|
||||
mostCommentedAudios = emptyList(),
|
||||
recommendedAudios = emptyList()
|
||||
banners = queryPort.findBanners(BANNER_LIMIT, memberId, canViewAdultContent),
|
||||
originalSeries = queryPort.findOriginalSeries(ORIGINAL_SERIES_LIMIT, memberId, canViewAdultContent, now),
|
||||
latestAudios = queryPort.findLatestAudios(LATEST_AUDIO_LIMIT, memberId, canViewAdultContent, now),
|
||||
newAndHotAudios = queryPort.findAudioCardsByIds(
|
||||
refreshedNewAndHotSnapshots.map { it.targetId },
|
||||
memberId,
|
||||
canViewAdultContent,
|
||||
now
|
||||
),
|
||||
freeAudios = queryPort.findFreeAudios(FREE_AUDIO_LIMIT, memberId, canViewAdultContent, now),
|
||||
pointAudios = queryPort.findPointAudios(POINT_AUDIO_LIMIT, memberId, canViewAdultContent, now),
|
||||
mostCommentedAudios = queryPort.findCommentedAudiosByIds(
|
||||
mostCommentedSnapshots.map { it.targetId },
|
||||
memberId,
|
||||
canViewAdultContent
|
||||
),
|
||||
recommendedAudios = queryPort.findAudioCardsByIds(
|
||||
recommendedSnapshots.map { it.targetId },
|
||||
memberId,
|
||||
canViewAdultContent,
|
||||
now
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -57,10 +91,32 @@ class AudioRecommendationQueryService(
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshMissingNewAndHotSnapshots(
|
||||
sectionType: RecommendedSectionType,
|
||||
snapshots: List<RecommendationSnapshotRecord>
|
||||
): List<RecommendationSnapshotRecord> {
|
||||
if (snapshots.isNotEmpty()) return snapshots
|
||||
val today = LocalDate.now(KST_ZONE)
|
||||
val marker = redissonClient.getBucket<String>(newAndHotLazyRefreshMarkerKey(today))
|
||||
if (!marker.setIfAbsent(LAZY_REFRESH_ATTEMPTED_VALUE, LAZY_REFRESH_MARKER_TTL)) {
|
||||
return snapshots
|
||||
}
|
||||
runCatching {
|
||||
snapshotRefreshService.refreshDailySnapshots()
|
||||
}.onFailure { ex ->
|
||||
marker.delete()
|
||||
throw ex
|
||||
}
|
||||
return snapshotPort.findLatestSnapshots(sectionType, limit = NEW_AND_HOT_AUDIO_LIMIT)
|
||||
}
|
||||
|
||||
private fun newAndHotLazyRefreshMarkerKey(date: LocalDate): String {
|
||||
return "$LAZY_REFRESH_MARKER_KEY_PREFIX:$date"
|
||||
}
|
||||
|
||||
private fun canViewAdultContent(member: Member?): Boolean {
|
||||
if (member == null) return false
|
||||
val preference = memberContentPreferenceService.initializeDefaultPreference(member)
|
||||
return isAdultVisibleByPolicy(member, preference.isAdultContentVisible)
|
||||
return memberContentPreferenceService.getStoredPreference(member).isAdult
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -69,5 +125,12 @@ class AudioRecommendationQueryService(
|
||||
const val LATEST_AUDIO_LIMIT = 12
|
||||
const val FREE_AUDIO_LIMIT = 10
|
||||
const val POINT_AUDIO_LIMIT = 10
|
||||
const val NEW_AND_HOT_AUDIO_LIMIT = 12
|
||||
const val MOST_COMMENTED_AUDIO_LIMIT = 5
|
||||
const val RECOMMENDED_AUDIO_LIMIT = 10
|
||||
private const val LAZY_REFRESH_MARKER_KEY_PREFIX = "audio-recommendation:new-and-hot:lazy-refresh-attempted"
|
||||
private const val LAZY_REFRESH_ATTEMPTED_VALUE = "1"
|
||||
private val LAZY_REFRESH_MARKER_TTL: Duration = Duration.ofDays(2)
|
||||
private val KST_ZONE: ZoneId = ZoneId.of("Asia/Seoul")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class AudioRecommendationScorePolicy {
|
||||
}
|
||||
|
||||
private fun daysBetween(from: LocalDateTime, now: LocalDateTime): Long {
|
||||
return ChronoUnit.DAYS.between(from.toLocalDate(), now.toLocalDate()).coerceAtLeast(0)
|
||||
return ChronoUnit.DAYS.between(from, now).coerceAtLeast(0)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
Reference in New Issue
Block a user