Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

결석 처리 API 및 배치 개발 #105

Merged
merged 8 commits into from
Jul 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package com.mashup.moit.controller.study

import com.mashup.moit.common.MoitApiResponse
import com.mashup.moit.security.authentication.UserInfo
import com.mashup.moit.security.resolver.GetAuth
import com.mashup.moit.controller.study.dto.StudyAdjustAbsenceRequest
import com.mashup.moit.controller.study.dto.StudyAttendanceKeywordRequest
import com.mashup.moit.controller.study.dto.StudyAttendanceKeywordResponse
import com.mashup.moit.controller.study.dto.StudyDetailsResponse
import com.mashup.moit.controller.study.dto.StudyFirstAttendanceResponse
import com.mashup.moit.controller.study.dto.StudyUserAttendanceStatusResponse
import com.mashup.moit.facade.StudyFacade
import com.mashup.moit.security.authentication.UserInfo
import com.mashup.moit.security.resolver.GetAuth
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
Expand Down Expand Up @@ -78,4 +79,12 @@ class StudyController(
studyFacade.initializeAttendance(studyId)
return MoitApiResponse.success()
}

@Operation(summary = "Adjust Absence Status", description = "Study 결석 처리")
@PostMapping("/attendance/adjust/absence")
fun adjustAbsenceStatus(@RequestBody request: StudyAdjustAbsenceRequest): MoitApiResponse<Unit> {
studyFacade.adjustAbsenceStatus(request.studyIds.toSet())
return MoitApiResponse.success()

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,10 @@ data class StudyUserAttendanceStatusResponse(
)
}
}

@Schema(description = "Study 결석 처리 요청")
data class StudyAdjustAbsenceRequest(
@Schema(description = "Study Ids")
@field:NotBlank
val studyIds: List<Long>
)
10 changes: 10 additions & 0 deletions moit-api/src/main/kotlin/com/mashup/moit/facade/StudyFacade.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.mashup.moit.controller.study.dto.StudyDetailsResponse
import com.mashup.moit.controller.study.dto.StudyFirstAttendanceResponse
import com.mashup.moit.controller.study.dto.StudyUserAttendanceStatusResponse
import com.mashup.moit.domain.attendance.AttendanceService
import com.mashup.moit.domain.fine.FineService
import com.mashup.moit.domain.moit.MoitService
import com.mashup.moit.domain.study.StudyService
import com.mashup.moit.domain.user.UserService
Expand All @@ -20,6 +21,7 @@ class StudyFacade(
private val moitService: MoitService,
private val studyService: StudyService,
private val attendanceService: AttendanceService,
private val fineService: FineService,
private val userService: UserService,
private val eventProducer: EventProducer
) {
Expand Down Expand Up @@ -69,4 +71,12 @@ class StudyFacade(
fun checkFirstAttendance(studyId: Long): StudyFirstAttendanceResponse {
return StudyFirstAttendanceResponse.of(attendanceService.existFirstAttendanceByStudyId(studyId))
}

@Transactional
fun adjustAbsenceStatus(studyIds: Set<Long>) {
studyService.findByStudyIds(studyIds.toList()).forEach { study ->
attendanceService.adjustUndecidedAttendancesByStudyId(study.id)
.forEach { attendanceId -> fineService.create(attendanceId, study.moitId) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ data class StudyAttendanceEvent(val attendanceId: Long, val moitId: Long) : Moit
}
}

data class StudyAttendanceEventBulk(val attendanceIdWithMoitIds: List<Pair<Long, Long>>) : MoitEvent {
override fun getTopic(): String {
return KafkaEventTopic.STUDY_ATTENDANCE_BULK
}
}

data class StudyInitializeEvent(val studyId: Long) : MoitEvent {
override fun getTopic(): String {
return KafkaEventTopic.STUDY_INITIALIZE
Expand All @@ -28,6 +34,12 @@ data class FineCreateEvent(val fineId: Long) : MoitEvent {
}
}

data class FineCreateEventBulk(val fineIds: Set<Long>) : MoitEvent {
override fun getTopic(): String {
return KafkaEventTopic.FINE_CREATE_BULK
}
}

data class FineApproveEvent(val fineId: Long) : MoitEvent {
override fun getTopic(): String {
return KafkaEventTopic.FINE_APPROVE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ object KafkaEventTopic {
const val MOIT_CREATE = "moit_create"
const val STUDY_INITIALIZE = "study_initialize"
const val STUDY_ATTENDANCE = "study_attendance"
const val STUDY_ATTENDANCE_BULK = "study_attendance_bulk"
const val FINE_CREATE = "fine_create"
const val FINE_CREATE_BULK = "fine_create_bulk"
const val FINE_APPROVE = "fine_approve"
}

object KafkaConsumerGroup {
const val MOIT_CREATE_STUDY_CREATE = "moit_create_study_create"
const val STUDY_INITIALIZE_BANNER_UPDATE = "study_initialize_banner_update"
const val STUDY_ATTENDANCE_FINE_CREATE = "study_attendance_fine_create"
const val STUDY_ATTENDANCE_FINE_CREATE_BULK = "study_attendance_fine_create_bulk"
const val FINE_CREATE_BANNER_UPDATE = "fine_create_banner_update"
const val FINE_CREATE_BANNER_UPDATE_BULK = "fine_create_banner_update_bulk"
const val FINE_APPROVE_BANNER_UPDATE = "fine_approve_banner_update"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ import com.mashup.moit.domain.study.StudyService
import com.mashup.moit.infra.event.EventProducer
import com.mashup.moit.infra.event.FineApproveEvent
import com.mashup.moit.infra.event.FineCreateEvent
import com.mashup.moit.infra.event.StudyAttendanceEvent
import com.mashup.moit.infra.event.FineCreateEventBulk
import com.mashup.moit.infra.event.KafkaConsumerGroup
import com.mashup.moit.infra.event.KafkaEventTopic
import com.mashup.moit.infra.event.MoitCreateEvent
import com.mashup.moit.infra.event.StudyAttendanceEvent
import com.mashup.moit.infra.event.StudyAttendanceEventBulk
import com.mashup.moit.infra.event.StudyInitializeEvent
import org.slf4j.Logger
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -56,6 +58,19 @@ class KafkaConsumer(
}
}

@KafkaListener(
topics = [KafkaEventTopic.STUDY_ATTENDANCE_BULK],
groupId = KafkaConsumerGroup.STUDY_ATTENDANCE_FINE_CREATE_BULK,
)
fun consumeStudyAttendancesEvent(event: StudyAttendanceEventBulk) {
log.debug("consumeStudyAttendancesEventBulk called: {}", event)
val fineIds = mutableSetOf<Long>()
event.attendanceIdWithMoitIds.forEach { (attendanceId, moitId) ->
fineService.create(attendanceId, moitId)?.let { fine -> fineIds.add(fine.id) }
KimDoubleB marked this conversation as resolved.
Show resolved Hide resolved
}
eventProducer.produce(FineCreateEventBulk(fineIds))
}

@KafkaListener(
topics = [KafkaEventTopic.FINE_CREATE],
groupId = KafkaConsumerGroup.FINE_CREATE_BANNER_UPDATE,
Expand All @@ -65,6 +80,17 @@ class KafkaConsumer(
bannerService.update(MoitUnapprovedFineExistBannerUpdateRequest(event.fineId))
}

@KafkaListener(
topics = [KafkaEventTopic.FINE_CREATE_BULK],
groupId = KafkaConsumerGroup.FINE_CREATE_BANNER_UPDATE_BULK,
)
fun consumeFineCreateEventBulk(event: FineCreateEventBulk) {
log.debug("consumeFineCreateEventBulk called: {}", event)
event.fineIds.forEach { fineId ->
bannerService.update(MoitUnapprovedFineExistBannerUpdateRequest(fineId))
}
Comment on lines +89 to +91
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하나의 트랜잭션에서 돌아가도록 서비스나 파사드로 빼서 구성하는 것도 좋아보이는데, 나중에 한 번에 진행해도 좋을 것 같습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! 배너 부분 숙지하고 제가 다음에 PR 올리도록 하겠습니다!

}

@KafkaListener(
topics = [KafkaEventTopic.FINE_APPROVE],
groupId = KafkaConsumerGroup.FINE_APPROVE_BANNER_UPDATE,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.mashup.moit.scheduler

import com.mashup.moit.domain.attendance.AttendanceService
import com.mashup.moit.domain.study.StudyService
import com.mashup.moit.infra.event.EventProducer
import com.mashup.moit.infra.event.StudyAttendanceEventBulk
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Async
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime

@Component
class StudyAdjustAbsenceScheduler(
private val studyService: StudyService,
private val attendanceService: AttendanceService,
private val eventProducer: EventProducer
) {
val logger: Logger = LoggerFactory.getLogger(StudyAdjustAbsenceScheduler::class.java)

@Scheduled(cron = "0 */5 * * * *")
@Async("asyncSchedulerExecutor")
@Transactional
fun adjustAbsenceStatusFromNow() {
// 결석 확정을 지을 때 endAt을 현재 시간보다 15초 정도 유예기간을 줌. 5분마다 배치가 돌기 때문에 95% 시간 내에 끝난 스터디를 반환함
xonmin marked this conversation as resolved.
Show resolved Hide resolved
val scheduleContext = LocalDateTime.now()
val undecided = studyService
.findUnfinalizedStudiesByEndAtBefore(scheduleContext.minusSeconds(DECIDE_ABSENCE_RANGE_SECONDS))
logger.info("{} undecided studies start! Start adjusting absence status at {}.", undecided.size, scheduleContext)

val attendanceIdWithMoitIds = mutableListOf<Pair<Long, Long>>()
undecided.forEach { study ->
attendanceIdWithMoitIds.addAll(
attendanceService.adjustUndecidedAttendancesByStudyId(study.id)
.map { attendanceId -> attendanceId to study.moitId }
)
}
eventProducer.produce(StudyAttendanceEventBulk(attendanceIdWithMoitIds))

logger.info("Done adjusting absence status for {} studies, at {}", undecided.size, LocalDateTime.now())
}

companion object {
private const val DECIDE_ABSENCE_RANGE_SECONDS = 15L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ import org.springframework.stereotype.Repository
interface AttendanceRepository : JpaRepository<AttendanceEntity, Long> {
fun findByUserIdAndStudyId(userId: Long, studyId: Long): AttendanceEntity?
fun findAllByStudyIdOrderByAttendanceAtAsc(studyId: Long): List<AttendanceEntity>
fun findAllByStudyIdAndStatus(studyId: Long, status: AttendanceStatus): List<AttendanceEntity>
fun existsByStudyIdAndStatus(studyId: Long, status: AttendanceStatus): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,11 @@ class AttendanceService(
fun existFirstAttendanceByStudyId(studyId: Long): Boolean {
return attendanceRepository.existsByStudyIdAndStatus(studyId, AttendanceStatus.ATTENDANCE)
}

@Transactional
fun adjustUndecidedAttendancesByStudyId(studyId: Long, status: AttendanceStatus = AttendanceStatus.ABSENCE): List<Long> {
val attendances = attendanceRepository.findAllByStudyIdAndStatus(studyId, AttendanceStatus.UNDECIDED)
attendances.forEach { attendance -> attendance.status = status }
return attendances.map { it.id }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class StudyEntity(
@Column(name = "is_initialized", nullable = false)
var isInitialized: Boolean = false

@Column(name = "is_finalized", nullable = false)
var isFinalized: Boolean = false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스터디가 끝나고 지각/결석처리가 다 되었을 때 확인 필드용


fun toDomain() = Study(
id = id,
moitId = moitId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface StudyRepository : JpaRepository<StudyEntity, Long> {
// findAll By StartAtBefore And IsInitializedFalse
fun findAllByStartAtBeforeAndIsInitializedFalse(startAt: LocalDateTime): List<StudyEntity>
fun findAllByMoitIdAndStartAtBeforeOrderByOrderDesc(moitId: Long, startAt: LocalDateTime): List<StudyEntity>
fun findAllByEndAtBeforeAndIsFinalizedFalse(endAt: LocalDateTime): List<StudyEntity>

// findAll By minDt <= StartAt <= maxDt (cause: Between Index performance degradation)
fun findAllByStartAtGreaterThanEqualAndStartAtLessThanEqual(minStartAt: LocalDateTime, maxStartAt: LocalDateTime): List<StudyEntity>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,9 @@ class StudyService(
.filter { !it.isInitialized } // 모종의 initialize 실패 인입 방지
.map { it.toDomain() }
}

fun findUnfinalizedStudiesByEndAtBefore(endAt: LocalDateTime): List<Study> {
return studyRepository.findAllByEndAtBeforeAndIsFinalizedFalse(endAt)
.map { it.toDomain() }
}
}