diff --git a/api/src/main/kotlin/com/oksusu/susu/api/dev/DevBatchResource.kt b/api/src/main/kotlin/com/oksusu/susu/api/dev/DevBatchResource.kt index 034d434d..f596d51f 100644 --- a/api/src/main/kotlin/com/oksusu/susu/api/dev/DevBatchResource.kt +++ b/api/src/main/kotlin/com/oksusu/susu/api/dev/DevBatchResource.kt @@ -3,8 +3,10 @@ package com.oksusu.susu.api.dev import com.oksusu.susu.api.auth.model.AdminUser import com.oksusu.susu.api.config.web.SwaggerTag import com.oksusu.susu.batch.envelope.job.RefreshSusuEnvelopeStatisticJob +import com.oksusu.susu.batch.report.job.ImposeSanctionsAboutReportJob import com.oksusu.susu.batch.summary.job.SusuStatisticsDailySummaryJob import com.oksusu.susu.batch.summary.job.SusuStatisticsHourSummaryJob +import com.oksusu.susu.batch.user.job.DeleteWithdrawUserDataJob import io.swagger.v3.oas.annotations.Operation import io.swagger.v3.oas.annotations.tags.Tag import kotlinx.coroutines.CoroutineScope @@ -22,6 +24,8 @@ class DevBatchResource( private val susuStatisticsHourSummaryJob: SusuStatisticsHourSummaryJob, private val susuStatisticsDailySummaryJob: SusuStatisticsDailySummaryJob, private val susuEnvelopeStatisticJob: RefreshSusuEnvelopeStatisticJob, + private val deleteWithdrawUserDataJob: DeleteWithdrawUserDataJob, + private val imposeSanctionsAboutReportJob: ImposeSanctionsAboutReportJob, ) { @Operation(tags = [SwaggerTag.DEV_SWAGGER_TAG], summary = "hour summary 호출") @GetMapping("/hour-summaries") @@ -53,6 +57,48 @@ class DevBatchResource( } } + /** 서비스 시작부터 지금까지 모든 탈퇴 유저의 데이터를 삭제한다. */ + @Operation(tags = [SwaggerTag.DEV_SWAGGER_TAG], summary = "delete withdraw user data 호출") + @GetMapping("/delete-withdraw-user-data") + suspend fun deleteWithdrawUserData( + adminUser: AdminUser, + ) { + CoroutineScope(Dispatchers.IO).launch { + deleteWithdrawUserDataJob.deleteWithdrawUserData() + } + } + + @Operation(tags = [SwaggerTag.DEV_SWAGGER_TAG], summary = "delete withdraw user data for week 호출") + @GetMapping("/delete-withdraw-user-data-for-week") + suspend fun deleteWithdrawUserDataForWeek( + adminUser: AdminUser, + ) { + CoroutineScope(Dispatchers.IO).launch { + deleteWithdrawUserDataJob.deleteWithdrawUserDataForWeek() + } + } + + @Operation(tags = [SwaggerTag.DEV_SWAGGER_TAG], summary = "impose sanctions about report for day 호출") + @GetMapping("/impose-sanction-about-report-for-day") + suspend fun imposeSanctionsAboutReportForDay( + adminUser: AdminUser, + ) { + CoroutineScope(Dispatchers.IO).launch { + imposeSanctionsAboutReportJob.imposeSanctionsAboutReportForDay() + } + } + + /** 서비스 시작부터 현 시점까지 기록된 report의 수를 캐싱한다. */ + @Operation(tags = [SwaggerTag.DEV_SWAGGER_TAG], summary = "update report count 호출") + @GetMapping("/update-report-count") + suspend fun updateReportCount( + adminUser: AdminUser, + ) { + CoroutineScope(Dispatchers.IO).launch { + imposeSanctionsAboutReportJob.updateReportCount() + } + } + /** 수수 통계 캐싱 데이터를 초기화합니다. */ @Operation(tags = [SwaggerTag.DEV_SWAGGER_TAG], summary = "refresh susu envelope statistic amount 호출") @GetMapping("/refresh-susu-envelope-statistic-amount") diff --git a/batch/src/main/kotlin/com/oksusu/susu/batch/report/job/ImposeSanctionsAboutReportJob.kt b/batch/src/main/kotlin/com/oksusu/susu/batch/report/job/ImposeSanctionsAboutReportJob.kt new file mode 100644 index 00000000..4a341711 --- /dev/null +++ b/batch/src/main/kotlin/com/oksusu/susu/batch/report/job/ImposeSanctionsAboutReportJob.kt @@ -0,0 +1,224 @@ +package com.oksusu.susu.batch.report.job + +import arrow.fx.coroutines.parZip +import com.oksusu.susu.cache.key.Cache +import com.oksusu.susu.cache.service.CacheService +import com.oksusu.susu.common.extension.merge +import com.oksusu.susu.domain.post.infrastructure.repository.PostRepository +import com.oksusu.susu.domain.report.domain.ReportResult +import com.oksusu.susu.domain.report.domain.vo.ReportResultStatus +import com.oksusu.susu.domain.report.domain.vo.ReportTargetType +import com.oksusu.susu.domain.report.infrastructure.ReportHistoryRepository +import com.oksusu.susu.domain.report.infrastructure.ReportResultRepository +import com.oksusu.susu.domain.user.domain.UserStatusHistory +import com.oksusu.susu.domain.user.domain.vo.UserStatusAssignmentType +import com.oksusu.susu.domain.user.domain.vo.UserStatusTypeInfo +import com.oksusu.susu.domain.user.infrastructure.UserStatusHistoryRepository +import com.oksusu.susu.domain.user.infrastructure.UserStatusRepository +import com.oksusu.susu.domain.user.infrastructure.UserStatusTypeRepository +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.* +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +@Component +class ImposeSanctionsAboutReportJob( + private val reportHistoryRepository: ReportHistoryRepository, + private val reportResultRepository: ReportResultRepository, + private val userStatusRepository: UserStatusRepository, + private val userStatusHistoryRepository: UserStatusHistoryRepository, + private val userStatusTypeRepository: UserStatusTypeRepository, + private val cacheService: CacheService, + private val postRepository: PostRepository, +) { + private val logger = KotlinLogging.logger {} + + companion object { + private const val REPORT_BEFORE_DAYS = 1L + } + + /** 서비스 시작 시점부터 지금까지의 report 카운트를 처리합니다. */ + suspend fun updateReportCount() { + val reports = withContext(Dispatchers.IO) { reportHistoryRepository.findAll() } + + val userReports = reports.filter { report -> report.targetType == ReportTargetType.USER } + .groupBy { it.targetId } + .mapValues { it.value.size.toLong() } + val postReports = reports.filter { report -> report.targetType == ReportTargetType.POST } + .groupBy { it.targetId } + .mapValues { it.value.size.toLong() } + + /** 기록을 캐싱한다. */ + coroutineScope { + val cacheUserReportCountDeferred = async { cacheService.set(Cache.getUserReportCountCache(), userReports) } + val cachePostReportCountDeferred = async { cacheService.set(Cache.getPostReportCountCache(), postReports) } + + awaitAll(cacheUserReportCountDeferred, cachePostReportCountDeferred) + } + } + + suspend fun imposeSanctionsAboutReportForDay() { + logger.info { "start impose sanction about report" } + + /** 제재 완료 유저 석방 */ + freePunishedUsers() + + /** 제재 대상 식별 */ + val (punishUids, punishPostIds) = getPunishTargetIds() + + logger.info { "$punishUids 유저 제재 및 $punishPostIds 게시글 삭제" } + + /** 제재 */ + punish(punishUids, punishPostIds) + + logger.info { "finish impose sanction about report" } + } + + suspend fun punish(punishUids: List, punishPostIds: List) { + val reportResults = mutableListOf() + val histories = mutableListOf() + + /** 게시물 제재 */ + punishPostIds.forEach { id -> + reportResults.add( + ReportResult( + targetId = id, + targetType = ReportTargetType.POST, + status = ReportResultStatus.DELETED + ) + ) + } + + /** 제재 게시물 글쓴이 조회 */ + val punishPostUids = withContext(Dispatchers.IO) { postRepository.findAllByIdIn(punishPostIds) } + .map { post -> post.uid } + + val targetUids = punishUids.toSet().plus(punishPostUids) + + /** 유저 제재 */ + val updatedStatuses = parZip( + { withContext(Dispatchers.IO) { userStatusTypeRepository.findAllByIsActive(true) } }, + { withContext(Dispatchers.IO) { userStatusRepository.findAllByUidIn(targetUids) } } + ) { userStatuses, statuses -> + val restrict7DaysUserStatusId = + userStatuses.first { status -> status.statusTypeInfo == UserStatusTypeInfo.RESTRICTED_7_DAYS }.id + + statuses.map { status -> + histories.add( + UserStatusHistory( + uid = status.uid, + statusAssignmentType = UserStatusAssignmentType.COMMUNITY, + fromStatusId = status.communityStatusId, + toStatusId = restrict7DaysUserStatusId, + isForced = true + ) + ) + + reportResults.add( + ReportResult( + targetId = status.uid, + targetType = ReportTargetType.USER, + status = ReportResultStatus.RESTRICTED_7_DAYS + ) + ) + + status.apply { + this.communityStatusId = restrict7DaysUserStatusId + } + } + } + + coroutineScope { + val updatePostDeferred = async { postRepository.updateIsActiveById(punishPostIds) } + val reportResultDeferred = async { reportResultRepository.saveAll(reportResults) } + val statusHistoryDeferred = async { userStatusHistoryRepository.saveAll(histories) } + val statusDeferred = async { userStatusRepository.saveAll(updatedStatuses) } + + awaitAll(updatePostDeferred, reportResultDeferred, statusDeferred, statusHistoryDeferred) + } + } + + suspend fun freePunishedUsers() { + /** RESTRICTED_7_DAYS 대응 */ + val from = LocalDateTime.now().minusDays(7).minusHours(1) + val to = LocalDateTime.now().minusDays(7).plusHours(1) + val freeUid = withContext(Dispatchers.IO) { reportResultRepository.findAllByCreatedAtBetween(from, to) } + .filter { report -> report.status == ReportResultStatus.RESTRICTED_7_DAYS && report.targetType == ReportTargetType.USER } + .map { result -> result.targetId } + + logger.info { "$freeUid 유저 석방" } + + /** RESTRICTED_7_DAYS 제재 해제 */ + val histories = mutableListOf() + + val updatedStatuses = parZip( + { withContext(Dispatchers.IO) { userStatusTypeRepository.findAllByIsActive(true) } }, + { withContext(Dispatchers.IO) { userStatusRepository.findAllByUidIn(freeUid.toSet()) } } + ) { userStatus, statuses -> + val activeUserStatusId = + userStatus.first { status -> status.statusTypeInfo == UserStatusTypeInfo.ACTIVE }.id + + statuses.map { status -> + histories.add( + UserStatusHistory( + uid = status.uid, + statusAssignmentType = UserStatusAssignmentType.COMMUNITY, + fromStatusId = status.communityStatusId, + toStatusId = activeUserStatusId, + isForced = true + ) + ) + + status.apply { + this.communityStatusId = activeUserStatusId + } + } + } + + coroutineScope { + val statusHistoryDeferred = async { userStatusHistoryRepository.saveAll(histories) } + val statusDeferred = async { userStatusRepository.saveAll(updatedStatuses) } + + awaitAll(statusDeferred, statusHistoryDeferred) + } + } + + suspend fun getPunishTargetIds(): Pair, List> { + val targetDate = LocalDateTime.now().minusDays(REPORT_BEFORE_DAYS) + + return parZip( + { withContext(Dispatchers.IO) { reportHistoryRepository.findAllByCreatedAtAfter(targetDate) } }, + { withContext(Dispatchers.IO) { cacheService.getOrNull(Cache.getUserReportCountCache()) } }, + { withContext(Dispatchers.IO) { cacheService.getOrNull(Cache.getPostReportCountCache()) } } + ) { reports, userReportHistory, postReportHistory -> + /** 일주일간 기록과 7일 전까지의 기록을 병합한다 */ + val userReports = reports.filter { report -> report.targetType == ReportTargetType.USER } + .groupBy { it.targetId } + .mapValues { it.value.size.toLong() } + .merge(userReportHistory!!) + .toMutableMap() + + val postReports = reports.filter { report -> report.targetType == ReportTargetType.POST } + .groupBy { it.targetId } + .mapValues { it.value.size.toLong() } + .merge(postReportHistory!!) + + /** 5회 이상인 유저 게시물을 찾는다. */ + val punishUids = userReports.filter { it.value / 5 > 0 }.map { report -> report.key } + val punishPostIds = postReports.filter { it.value / 5 > 0 }.map { report -> report.key } + + /** 유저 기록의 경우 초기화한다. */ + for (uid in punishUids) { + userReports[uid] = userReports[uid]!! % 5 + } + + /** 기록을 캐싱한다. */ + val cacheUserReportCountDeferred = async { cacheService.set(Cache.getUserReportCountCache(), userReports) } + val cachePostReportCountDeferred = async { cacheService.set(Cache.getPostReportCountCache(), postReports) } + + awaitAll(cacheUserReportCountDeferred, cachePostReportCountDeferred) + + punishUids to punishPostIds + } + } +} diff --git a/batch/src/main/kotlin/com/oksusu/susu/batch/report/scheduler/ReportScheduler.kt b/batch/src/main/kotlin/com/oksusu/susu/batch/report/scheduler/ReportScheduler.kt new file mode 100644 index 00000000..132a50b4 --- /dev/null +++ b/batch/src/main/kotlin/com/oksusu/susu/batch/report/scheduler/ReportScheduler.kt @@ -0,0 +1,20 @@ +package com.oksusu.susu.batch.report.scheduler + +import com.oksusu.susu.batch.report.job.ImposeSanctionsAboutReportJob +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component + +@Component +class ReportScheduler( + private val imposeSanctionsAboutReportJob: ImposeSanctionsAboutReportJob, +) { + @Scheduled(cron = "0 0 0 * * *") + fun imposeSanctionsAboutReportForDay() { + CoroutineScope(Dispatchers.IO).launch { + imposeSanctionsAboutReportJob.imposeSanctionsAboutReportForDay() + } + } +} diff --git a/batch/src/main/kotlin/com/oksusu/susu/batch/user/job/DeleteWithdrawUserDataJob.kt b/batch/src/main/kotlin/com/oksusu/susu/batch/user/job/DeleteWithdrawUserDataJob.kt new file mode 100644 index 00000000..b266ef87 --- /dev/null +++ b/batch/src/main/kotlin/com/oksusu/susu/batch/user/job/DeleteWithdrawUserDataJob.kt @@ -0,0 +1,152 @@ +package com.oksusu.susu.batch.user.job + +import com.oksusu.susu.domain.envelope.infrastructure.EnvelopeRepository +import com.oksusu.susu.domain.envelope.infrastructure.LedgerRepository +import com.oksusu.susu.domain.friend.infrastructure.FriendRelationshipRepository +import com.oksusu.susu.domain.friend.infrastructure.FriendRepository +import com.oksusu.susu.domain.post.infrastructure.repository.PostRepository +import com.oksusu.susu.domain.user.domain.vo.UserStatusTypeInfo +import com.oksusu.susu.domain.user.infrastructure.UserStatusHistoryRepository +import com.oksusu.susu.domain.user.infrastructure.UserStatusTypeRepository +import io.github.oshai.kotlinlogging.KotlinLogging +import kotlinx.coroutines.* +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +@Component +class DeleteWithdrawUserDataJob( + val userStatusTypeRepository: UserStatusTypeRepository, + val userStatusHistoryRepository: UserStatusHistoryRepository, + val envelopeRepository: EnvelopeRepository, + val ledgerRepository: LedgerRepository, + val friendRepository: FriendRepository, + val friendRelationshipRepository: FriendRelationshipRepository, + val postRepository: PostRepository, +) { + private val logger = KotlinLogging.logger {} + + companion object { + private const val DELETE_BEFORE_DAYS = 1L + private const val DELETE_CHUNK = 1000 + } + + /** 기간 지정 없이 생성부터 지금까지 모든 데이터에 대해 삭제합니다. */ + suspend fun deleteWithdrawUserData() { + logger.info { "start delete withdraw user data" } + + val targetUserStatusId = getTargetUserStatusId() + + /** 삭제된 uid */ + val targetUids = getDeletedUids(targetUserStatusId) + + deleteData(targetUids) + + logger.info { "finish delete withdraw user data" } + } + + suspend fun deleteWithdrawUserDataForWeek() { + logger.info { "start delete withdraw user data for week" } + + val targetUserStatusId = getTargetUserStatusId() + val targetDate = LocalDateTime.now().minusDays(DELETE_BEFORE_DAYS) + + /** 삭제 대상 uid */ + val targetUids = getDeletedUidsAfter(targetUserStatusId, targetDate) + + deleteData(targetUids) + + logger.info { "finish delete withdraw user data for week" } + } + + suspend fun deleteData(targetUids: List) { + coroutineScope { + /** 삭제 유저의 봉투 삭제 */ + val deleteEnvelopesDeferred = async { deleteEnvelopes(targetUids) } + + /** 삭제 유저의 장부 삭제 */ + val deleteLedgersDeferred = async { deleteLedgers(targetUids) } + + /** 삭제 유저의 친구 및 친구 관계 삭제 */ + val deleteFriendsDeferred = async { deleteFriends(targetUids) } + + /** 삭제 유저의 게시물 삭제 */ + val deletePostsDeferred = async { deletePosts(targetUids) } + + awaitAll(deleteEnvelopesDeferred, deleteLedgersDeferred, deleteFriendsDeferred, deletePostsDeferred) + } + } + + suspend fun getTargetUserStatusId(): Long { + return withContext(Dispatchers.IO) { + userStatusTypeRepository.findAllByIsActive(true) + }.first { type -> type.statusTypeInfo == UserStatusTypeInfo.DELETED }.id + } + + suspend fun getDeletedUidsAfter(targetUserStatusId: Long, targetDate: LocalDateTime): List { + return withContext(Dispatchers.IO) { + userStatusHistoryRepository.getUidByToStatusIdAfter(targetUserStatusId, targetDate) + } + } + + suspend fun getDeletedUids(targetUserStatusId: Long): List { + return withContext(Dispatchers.IO) { + userStatusHistoryRepository.getUidByToStatusId(targetUserStatusId) + } + } + + suspend fun deleteEnvelopes(uid: List) { + val envelopes = withContext(Dispatchers.IO) { + envelopeRepository.findAllByUidIn(uid) + }.takeIf { envelopes -> envelopes.isNotEmpty() } ?: return + + coroutineScope { + envelopes.chunked(DELETE_CHUNK) + .map { chunk -> async(Dispatchers.IO) { envelopeRepository.deleteAllInBatch(chunk) } } + } + } + + suspend fun deleteLedgers(uid: List) { + val ledgers = withContext(Dispatchers.IO) { + ledgerRepository.findAllByUidIn(uid) + }.takeIf { ledgers -> ledgers.isNotEmpty() } ?: return + + coroutineScope { + ledgers.chunked(DELETE_CHUNK) + .map { chunk -> async(Dispatchers.IO) { ledgerRepository.deleteAllInBatch(chunk) } } + } + } + + suspend fun deleteFriends(uid: List) { + val friends = withContext(Dispatchers.IO) { + friendRepository.findAllByUidIn(uid) + }.takeIf { friends -> friends.isNotEmpty() } ?: return + + val friendIds = friends.map { friend -> friend.id } + + withContext(Dispatchers.IO) { + friendRelationshipRepository.findAllByFriendIdIn(friendIds) + }.takeIf { relationships -> relationships.isNotEmpty() } + ?.let { relationships -> + coroutineScope { + relationships.chunked(DELETE_CHUNK) + .map { chunk -> async(Dispatchers.IO) { friendRelationshipRepository.deleteAllInBatch(chunk) } } + } + } + + coroutineScope { + friends.chunked(DELETE_CHUNK) + .map { chunk -> async(Dispatchers.IO) { friendRepository.deleteAllInBatch(chunk) } } + } + } + + suspend fun deletePosts(uid: List) { + val posts = withContext(Dispatchers.IO) { + postRepository.findAllByUidIn(uid) + }.takeIf { posts -> posts.isNotEmpty() } ?: return + + coroutineScope { + posts.chunked(DELETE_CHUNK) + .map { chunk -> async(Dispatchers.IO) { postRepository.deleteAllInBatch(chunk) } } + } + } +} 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 new file mode 100644 index 00000000..49730b7d --- /dev/null +++ b/batch/src/main/kotlin/com/oksusu/susu/batch/user/scheduler/UserScheduler.kt @@ -0,0 +1,20 @@ +package com.oksusu.susu.batch.user.scheduler + +import com.oksusu.susu.batch.user.job.DeleteWithdrawUserDataJob +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component + +@Component +class UserScheduler( + val deleteWithdrawUserDataJob: DeleteWithdrawUserDataJob, +) { + @Scheduled(cron = "0 0 3 * * *") + fun deleteWithdrawUserData() { + CoroutineScope(Dispatchers.IO).launch { + deleteWithdrawUserDataJob.deleteWithdrawUserDataForWeek() + } + } +} diff --git a/cache/src/main/kotlin/com/oksusu/susu/cache/key/Cache.kt b/cache/src/main/kotlin/com/oksusu/susu/cache/key/Cache.kt index 82b1aa00..fe52b01c 100644 --- a/cache/src/main/kotlin/com/oksusu/susu/cache/key/Cache.kt +++ b/cache/src/main/kotlin/com/oksusu/susu/cache/key/Cache.kt @@ -57,6 +57,22 @@ data class Cache( ) } + fun getUserReportCountCache(): Cache> { + return Cache( + key = USER_REPORT_COUNT_KEY, + type = toTypeReference(), + duration = Duration.ofDays(2) + ) + } + + fun getPostReportCountCache(): Cache> { + return Cache( + key = POST_REPORT_COUNT_KEY, + type = toTypeReference(), + duration = Duration.ofDays(2) + ) + } + fun getSusuEnvelopeStatisticAmountCache(): Cache> { return Cache( key = SUSU_ENVELOPE_STATISTIC_AMOUNT_KEY, diff --git a/common/src/main/kotlin/com/oksusu/susu/common/consts/SusuConsts.kt b/common/src/main/kotlin/com/oksusu/susu/common/consts/SusuConsts.kt index 065839a8..dc0f84b5 100644 --- a/common/src/main/kotlin/com/oksusu/susu/common/consts/SusuConsts.kt +++ b/common/src/main/kotlin/com/oksusu/susu/common/consts/SusuConsts.kt @@ -20,6 +20,9 @@ const val FAIL_TO_VALIDATE_MESSAGE = "fail to validate" const val APPLE_OIDC_PUBLIC_KEY_KEY = "apple_oidc_public_key" const val APPLE_OIDC_PUBLIC_KEY_TTL = 7 +const val USER_REPORT_COUNT_KEY = "user_report_count_key" +const val POST_REPORT_COUNT_KEY = "post_report_count_key" + const val SUSU_ENVELOPE_STATISTIC_AMOUNT_KEY = "susu_envelope_statistic_amount" const val KID = "kid" diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/envelope/infrastructure/EnvelopeRepository.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/envelope/infrastructure/EnvelopeRepository.kt index 85c6339b..a12770d8 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/envelope/infrastructure/EnvelopeRepository.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/envelope/infrastructure/EnvelopeRepository.kt @@ -50,6 +50,9 @@ interface EnvelopeRepository : JpaRepository, EnvelopeCustomRepo @Transactional(readOnly = true) fun countByUidAndFriendId(uid: Long, friendId: Long): Long + + @Transactional(readOnly = true) + fun findAllByUidIn(uid: List): List } interface EnvelopeCustomRepository { diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/envelope/infrastructure/LedgerRepository.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/envelope/infrastructure/LedgerRepository.kt index 399844c2..d1b481dd 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/envelope/infrastructure/LedgerRepository.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/envelope/infrastructure/LedgerRepository.kt @@ -28,6 +28,9 @@ interface LedgerRepository : JpaRepository, LedgerCustomRepository @Transactional(readOnly = true) fun countByCreatedAtBetween(startAt: LocalDateTime, endAt: LocalDateTime): Long + + @Transactional(readOnly = true) + fun findAllByUidIn(uid: List): List } interface LedgerCustomRepository { diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/friend/infrastructure/FriendRepository.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/friend/infrastructure/FriendRepository.kt index 05163841..eed2534b 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/friend/infrastructure/FriendRepository.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/friend/infrastructure/FriendRepository.kt @@ -37,6 +37,9 @@ interface FriendRepository : JpaRepository, FriendCustomRepository @Transactional(readOnly = true) fun countByCreatedAtBetween(startAt: LocalDateTime, endAt: LocalDateTime): Long + + @Transactional(readOnly = true) + fun findAllByUidIn(uid: List): List } interface FriendCustomRepository { diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/post/infrastructure/repository/PostRepository.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/post/infrastructure/repository/PostRepository.kt index 9fa8a826..e58db32d 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/post/infrastructure/repository/PostRepository.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/post/infrastructure/repository/PostRepository.kt @@ -13,6 +13,7 @@ import com.oksusu.susu.domain.post.domain.vo.PostType import com.oksusu.susu.domain.post.infrastructure.repository.model.* import com.oksusu.susu.domain.user.domain.QUser import com.querydsl.jpa.impl.JPAQuery +import com.querydsl.jpa.impl.JPAQueryFactory import jakarta.persistence.EntityManager import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Qualifier @@ -29,6 +30,12 @@ interface PostRepository : JpaRepository, PostCustomRepository { @Transactional(readOnly = true) fun findAllByUid(uid: Long): List + + @Transactional(readOnly = true) + fun findAllByUidIn(uid: List): List + + @Transactional(readOnly = true) + fun findAllByIdIn(punishPostIds: List) : List } interface PostCustomRepository { @@ -40,6 +47,9 @@ interface PostCustomRepository { @Transactional(readOnly = true) fun getPostAndCreator(id: Long, type: PostType): PostAndUserModel? + + @Transactional + fun updateIsActiveById(ids: List): Long } class PostCustomRepositoryImpl : PostCustomRepository, QuerydslRepositorySupport(Post::class.java) { @@ -111,4 +121,12 @@ class PostCustomRepositoryImpl : PostCustomRepository, QuerydslRepositorySupport qPost.type.eq(type) ).fetchFirst() } + + override fun updateIsActiveById(ids: List): Long { + return JPAQueryFactory(entityManager) + .update(qPost) + .where(qPost.id.`in`(ids)) + .set(qPost.isActive, false) + .execute() + } } diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/report/domain/ReportResult.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/report/domain/ReportResult.kt index edb0f8f0..c5f7e61c 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/report/domain/ReportResult.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/report/domain/ReportResult.kt @@ -2,6 +2,7 @@ package com.oksusu.susu.domain.report.domain import com.oksusu.susu.domain.common.BaseEntity import com.oksusu.susu.domain.report.domain.vo.ReportResultStatus +import com.oksusu.susu.domain.report.domain.vo.ReportTargetType import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.EnumType @@ -19,8 +20,14 @@ class ReportResult( @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = -1, - /** 신고 대상 uid */ - val uid: Long, + /** 신고 대상 id */ + @Column(name = "target_id") + val targetId: Long, + + /** 신고 대상 타입 */ + @Enumerated(EnumType.STRING) + @Column(name = "target_type") + val targetType: ReportTargetType, /** 신고 결과 상태 */ @Enumerated(EnumType.STRING) diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/report/domain/vo/ReportResultStatus.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/report/domain/vo/ReportResultStatus.kt index a39fc809..30446d45 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/report/domain/vo/ReportResultStatus.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/report/domain/vo/ReportResultStatus.kt @@ -1,3 +1,13 @@ package com.oksusu.susu.domain.report.domain.vo -enum class ReportResultStatus +enum class ReportResultStatus { + /** 일시 정지 7일 */ + RESTRICTED_7_DAYS, + + /** 영구 정지 */ + BANISHED, + + /** 삭제 */ + DELETED, + ; +} diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportHistoryRepository.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportHistoryRepository.kt index 9bd789d8..59cfccda 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportHistoryRepository.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportHistoryRepository.kt @@ -5,9 +5,13 @@ import com.oksusu.susu.domain.report.domain.vo.ReportTargetType import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime @Repository interface ReportHistoryRepository : JpaRepository { @Transactional(readOnly = true) fun existsByUidAndTargetIdAndTargetType(uid: Long, targetId: Long, targetType: ReportTargetType): Boolean + + @Transactional(readOnly = true) + fun findAllByCreatedAtAfter(createdAt: LocalDateTime): List } diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportResultRepository.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportResultRepository.kt index f6673a35..80d10ac8 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportResultRepository.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/report/infrastructure/ReportResultRepository.kt @@ -3,6 +3,11 @@ package com.oksusu.susu.domain.report.infrastructure import com.oksusu.susu.domain.report.domain.ReportResult import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime @Repository -interface ReportResultRepository : JpaRepository +interface ReportResultRepository : JpaRepository { + @Transactional(readOnly = true) + fun findAllByCreatedAtBetween(from: LocalDateTime, to: LocalDateTime): List +} diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/user/domain/UserStatusHistory.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/user/domain/UserStatusHistory.kt index d57711e2..ef6cc2c9 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/user/domain/UserStatusHistory.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/user/domain/UserStatusHistory.kt @@ -27,4 +27,8 @@ class UserStatusHistory( /** 변경 후 상태 id */ @Column(name = "to_status_id") val toStatusId: Long, + + /** 관리자 실행 여부, 1 : 관리자, 0 : 유저 */ + @Column(name = "is_forced") + val isForced: Boolean = false, ) : BaseEntity() diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusHistoryRepository.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusHistoryRepository.kt index 368ba7fd..d0d055ed 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusHistoryRepository.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusHistoryRepository.kt @@ -1,8 +1,57 @@ package com.oksusu.susu.domain.user.infrastructure +import com.oksusu.susu.domain.user.domain.QUserStatusHistory import com.oksusu.susu.domain.user.domain.UserStatusHistory +import com.querydsl.jpa.impl.JPAQuery +import jakarta.persistence.EntityManager +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime @Repository -interface UserStatusHistoryRepository : JpaRepository +interface UserStatusHistoryRepository : JpaRepository, UserStatusHistoryCustomRepository + +interface UserStatusHistoryCustomRepository { + @Transactional(readOnly = true) + fun getUidByToStatusIdAfter(toStatusId: Long, targetDate: LocalDateTime): List + + @Transactional(readOnly = true) + fun getUidByToStatusId(toStatusId: Long): List +} + +class UserStatusHistoryCustomRepositoryImpl : UserStatusHistoryCustomRepository, + QuerydslRepositorySupport(UserStatusHistory::class.java) { + + @Autowired + @Qualifier("susuEntityManager") + override fun setEntityManager(entityManager: EntityManager) { + super.setEntityManager(entityManager) + } + + private val qUserStatusHistory = QUserStatusHistory.userStatusHistory + + override fun getUidByToStatusIdAfter(toStatusId: Long, targetDate: LocalDateTime): List { + return JPAQuery(entityManager) + .select(qUserStatusHistory.uid) + .from(qUserStatusHistory) + .where( + qUserStatusHistory.toStatusId.eq(toStatusId), + qUserStatusHistory.createdAt.after(targetDate) + ) + .fetch() + } + + override fun getUidByToStatusId(toStatusId: Long): List { + return JPAQuery(entityManager) + .select(qUserStatusHistory.uid) + .from(qUserStatusHistory) + .where( + qUserStatusHistory.toStatusId.eq(toStatusId) + ) + .fetch() + } +} diff --git a/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusRepository.kt b/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusRepository.kt index d5df1f9a..e2513e22 100644 --- a/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusRepository.kt +++ b/domain/src/main/kotlin/com/oksusu/susu/domain/user/infrastructure/UserStatusRepository.kt @@ -3,6 +3,10 @@ package com.oksusu.susu.domain.user.infrastructure import com.oksusu.susu.domain.user.domain.UserStatus import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository +import org.springframework.transaction.annotation.Transactional @Repository -interface UserStatusRepository : JpaRepository +interface UserStatusRepository : JpaRepository { + @Transactional(readOnly = true) + fun findAllByUidIn(freeUid: Set): List +} diff --git a/scripts/DDL.sql b/scripts/DDL.sql index 0cd5b5fa..27048e14 100644 --- a/scripts/DDL.sql +++ b/scripts/DDL.sql @@ -56,6 +56,7 @@ CREATE TABLE `user_status_history` PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='유저 상태 변경 기록'; CREATE INDEX idx__uid ON user_status_history (uid); +ALTER TABLE user_status_history ADD (`is_forced` tinyint DEFAULT 0 NOT NULL COMMENT '관리자 실행 여부, 1 : 관리자, 0 : 유저'); -- 탈퇴 유저 기록 CREATE TABLE `user_withdraw` @@ -318,6 +319,11 @@ CREATE TABLE `report_result` PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE =utf8mb4_general_ci COMMENT '신고 결과'; CREATE INDEX idx__uid ON report_result (uid); +ALTER TABLE report_result ADD (`target_type` varchar(128) NOT NULL COMMENT '신고 대상'); +ALTER TABLE report_result CHANGE COLUMN `uid` `target_id` bigint NOT NULL COMMENT '신고 대상 id'; +ALTER TABLE report_result CHANGE COLUMN `status` `status` varchar (128) NOT NULL COMMENT '신고 결과 상태'; +DROP INDEX idx__uid ON report_result; +CREATE INDEX idx__target_id__target_type ON report_result (target_id, target_type); -- 카운트 CREATE TABLE `count`