Skip to content

Commit

Permalink
Merge pull request #121 from mash-up-kr/feature/fcm_fine_remind_batch
Browse files Browse the repository at this point in the history
Implement Remind Fine Batch
  • Loading branch information
mkSpace authored Aug 8, 2023
2 parents a06798c + 825ddd2 commit 90041e7
Show file tree
Hide file tree
Showing 20 changed files with 238 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package com.mashup.moit.controller.user

import com.mashup.moit.common.MoitApiResponse
import com.mashup.moit.controller.user.dto.UserFcmTokenRequest
import com.mashup.moit.facade.UserFacade
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 org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

Expand All @@ -31,4 +34,11 @@ class UserController(
return MoitApiResponse.success()
}

@Operation(summary = "FCM Token 갱신", description = "Update FCM Token API")
@PostMapping("/fcm-token")
fun updateFcmToken(@GetAuth userInfo: UserInfo, @RequestBody request: UserFcmTokenRequest): MoitApiResponse<Unit> {
userFacade.updateFcmToken(userInfo.id, request.fcmToken)
return MoitApiResponse.success()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.mashup.moit.controller.user.dto

data class UserFcmTokenRequest(val fcmToken: String?)
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ data class UserRegisterRequest(
val email: String,
val profileImage: Int,
val moitInvitationCode: String?,
val fcmToken: String?
)
13 changes: 9 additions & 4 deletions moit-api/src/main/kotlin/com/mashup/moit/facade/UserFacade.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ class UserFacade(
throw MoitException.of(MoitExceptionType.ALREADY_EXIST)
}
return userService.createUser(
userRegisterRequest.providerUniqueKey,
userRegisterRequest.nickname,
userRegisterRequest.profileImage,
userRegisterRequest.email
providerUniqueKey = userRegisterRequest.providerUniqueKey,
nickname = userRegisterRequest.nickname,
profileImage = userRegisterRequest.profileImage,
email = userRegisterRequest.email,
fcmToken = userRegisterRequest.fcmToken
).also {
registerMoit(it, userRegisterRequest.moitInvitationCode)
}
Expand All @@ -54,4 +55,8 @@ class UserFacade(
}
}

@Transactional
fun updateFcmToken(userId: Long, fcmToken: String?) {
userService.updateFcmToken(userId, fcmToken)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,9 @@ data class StudyAttendanceStartNotificationPushEvent(val studyIdWithMoitIds: Set
return KafkaEventTopic.STUDY_ATTENDANCE_START_NOTIFICATION
}
}

data class RemindFineNotificationPushEvent(val fineIds: Set<Long>, val flushAt: LocalDateTime) : MoitEvent {
override fun getTopic(): String {
return KafkaEventTopic.REMIND_FINE_NOTIFICATION
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ object KafkaEventTopic {
const val FINE_CREATE = "fine_create"
const val FINE_CREATE_BULK = "fine_create_bulk"
const val FINE_APPROVE = "fine_approve"
const val REMIND_FINE_NOTIFICATION = "remind_fine_notification"
}

object KafkaConsumerGroup {
Expand All @@ -20,4 +21,5 @@ object KafkaConsumerGroup {
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"
const val REMIND_FINE_NOTIFICATION_CREATE = "remind_fine_notification_create"
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.mashup.moit.domain.banner.update.StudyAttendanceStartBannerUpdateRequ
import com.mashup.moit.domain.fine.FineService
import com.mashup.moit.domain.notification.AttendanceStartNotificationEvent
import com.mashup.moit.domain.notification.NotificationService
import com.mashup.moit.domain.notification.RemindFineNotificationEvent
import com.mashup.moit.domain.study.StudyService
import com.mashup.moit.infra.event.EventProducer
import com.mashup.moit.infra.event.FineApproveEvent
Expand All @@ -14,6 +15,7 @@ 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.RemindFineNotificationPushEvent
import com.mashup.moit.infra.event.StudyAttendanceEvent
import com.mashup.moit.infra.event.StudyAttendanceEventBulk
import com.mashup.moit.infra.event.StudyAttendanceStartNotificationPushEvent
Expand Down Expand Up @@ -112,4 +114,13 @@ class KafkaConsumer(
log.debug("consumeStudyAttendanceStartNotificationPushEvent called: {}", event)
notificationService.save(AttendanceStartNotificationEvent(event.studyIdWithMoitIds, event.flushAt))
}

@KafkaListener(
topics = [KafkaEventTopic.REMIND_FINE_NOTIFICATION],
groupId = KafkaConsumerGroup.REMIND_FINE_NOTIFICATION_CREATE,
)
fun consumeRemindFineNotificationPushEvent(event: RemindFineNotificationPushEvent) {
log.debug("consumeRemindFineNotificationPushEvent called: {}", event)
notificationService.save(RemindFineNotificationEvent(event.fineIds, event.flushAt))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package com.mashup.moit.infra.fcm

import com.mashup.moit.domain.moit.Moit
import com.mashup.moit.domain.moit.NotificationRemindOption
import com.mashup.moit.domain.notification.RemindFinePushNotificationGenerator
import com.mashup.moit.domain.notification.StartAttendancePushNotificationGenerator
import com.mashup.moit.domain.study.Study
import com.mashup.moit.domain.user.User

data class SampleNotificationRequest(
val studyId: Long,
Expand Down Expand Up @@ -33,3 +35,23 @@ data class StudyAttendanceStartNotification(
)
}
}

data class FineRemindNotification(
val userFcmToken: String,
val title: String,
val body: String
) {
companion object {
fun of(
user: User,
moit: Moit,
study: Study
): FineRemindNotification? = user.fcmToken?.let { token ->
FineRemindNotification(
userFcmToken = token,
title = RemindFinePushNotificationGenerator.TITLE_TEMPLATE,
body = RemindFinePushNotificationGenerator.bodyTemplate(user.nickname, moit.name, study.order)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,27 @@ class FCMNotificationService(
}
}

fun pushRemindFineNotification(fineNotification: FineRemindNotification) {
val userFcmToken = fineNotification.userFcmToken
runCatching {
val notification = Notification.builder()
.setTitle(fineNotification.title)
.setBody(fineNotification.body)
.build()

val message = Message.builder()
.setToken(userFcmToken)
.setNotification(notification)
.build()

firebaseMessaging.send(message)
}.onSuccess { response ->
logger.info("success to send notification : {}", response)
}.onFailure { e ->
logger.error("Fail to send Remind Fine Noti. userFcmToken : [{}], error: [{}]", userFcmToken, e.toString())
}
}

fun getMoitTopic(moitId: Long): String {
return TOPIC_MOIT_PREFIX + moitId
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.mashup.moit.scheduler

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
import com.mashup.moit.infra.event.EventProducer
import com.mashup.moit.infra.event.RemindFineNotificationPushEvent
import com.mashup.moit.infra.fcm.FCMNotificationService
import com.mashup.moit.infra.fcm.FineRemindNotification
import org.joda.time.LocalDateTime
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

@Component
class FineRemindNotiPushScheduler(
private val userService: UserService,
private val moitService: MoitService,
private val studyService: StudyService,
private val fineService: FineService,
private val fcmNotificationService: FCMNotificationService,
private val eventProducer: EventProducer
) {

val logger: Logger = LoggerFactory.getLogger(FineRemindNotiPushScheduler::class.java)

@Scheduled(cron = "0 30 10 * * *")
@Async("pushSchedulerExecutor")
fun pushRemindFineNotification() {
val scheduleContext = LocalDateTime.now()

val unrequestedFines = fineService.getUnrequestedFines()
logger.info("Remind {} fines! Start Push Notification at {}.", unrequestedFines.size, scheduleContext)

val unrequestedFineUserMap = userService
.findUsersById(
unrequestedFines.map { fine -> fine.userId }.distinct()
)
.associateBy { user -> user.id }
val unrequestedFineMoitMap = moitService
.getMoitsByIds(
unrequestedFines.map { fine -> fine.moitId }.toSet()
)
.associateBy { moit -> moit.id }
val unrequestedFineStudyMap = studyService
.findByStudyIds(
unrequestedFines.map { fine -> fine.studyId }.distinct()
)
.associateBy { study -> study.id }

for (fine in unrequestedFines) {
val user = unrequestedFineUserMap[fine.userId] ?: continue
val moit = unrequestedFineMoitMap[fine.moitId] ?: continue
val study = unrequestedFineStudyMap[fine.studyId] ?: continue
FineRemindNotification.of(
user = user,
moit = moit,
study = study
)?.let { notification -> fcmNotificationService.pushRemindFineNotification(notification) }
}
eventProducer.produce(
RemindFineNotificationPushEvent(
fineIds = unrequestedFines.map { fine -> fine.id }.toSet(),
flushAt = java.time.LocalDateTime.now())
)
logger.info("Done Push notification for {} fines, at {}", unrequestedFines.size, LocalDateTime.now())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class JwtTokenSupporterTest : BehaviorSpec() {

Given("User 정보가 주어진 상황에서") {
// define test user
val user = User(1L, LocalDateTime.now(), LocalDateTime.now(), false, "key", "nickname", 1, "email", setOf(UserRole.USER))
val user = User(1L, LocalDateTime.now(), LocalDateTime.now(), false, "key", "nickname", 1, "email", setOf(UserRole.USER), null)
val userInfo = UserInfo.from(user)
val tokenSubject = "jwt-user-${userInfo.nickname}"
val tokenAudience = "${userInfo.providerUniqueKey}|${userInfo.id}|${userInfo.nickname}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository
interface FineRepository : JpaRepository<FineEntity, Long> {
fun findByMoitId(moitId: Long): List<FineEntity>
fun findAllByMoitIdAndUserIdAndApproveStatusIn(moitId: Long, userId: Long, approveStatuses: Collection<FineApproveStatus>): List<FineEntity>
fun findAllByApproveStatusIn(approveStatuses: Collection<FineApproveStatus>): List<FineEntity>
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ class FineService(
.map { it.toDomain() }
}

fun getUnrequestedFines(): List<Fine> {
return fineRepository.findAllByApproveStatusIn(
listOf(FineApproveStatus.NEW, FineApproveStatus.REJECTED)
).map { it.toDomain() }
}

@Transactional
fun updateFineApproveStatus(fineId: Long, confirmFine: Boolean) {
val approveStatus = if (confirmFine) FineApproveStatus.APPROVED else FineApproveStatus.REJECTED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import org.springframework.transaction.annotation.Transactional
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalTime
import java.util.Locale
import java.util.*

@Service
@Transactional(readOnly = true)
Expand Down Expand Up @@ -84,6 +84,11 @@ class MoitService(
.toDomain()
}

fun getMoitsByIds(moitIds: Set<Long>): List<Moit> {
return moitRepository.findAllById(moitIds)
.map { it.toDomain() }
}

fun getMoitsByUserId(userId: Long): List<Moit> {
val moitIds = userMoitRepository.findAllByUserId(userId).map { it.moitId }
return moitRepository.findAllById(moitIds).map { it.toDomain() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ data class AttendanceStartNotificationEvent(
val studyIdWithMoitIds: Set<Pair<Long, Long>>,
val flushAt: LocalDateTime
) : NotificationEvent

data class RemindFineNotificationEvent(
val fineIds: Set<Long>,
val flushAt: LocalDateTime
) : NotificationEvent
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package com.mashup.moit.domain.notification

import com.mashup.moit.common.exception.MoitException
import com.mashup.moit.common.exception.MoitExceptionType
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
import com.mashup.moit.domain.usermoit.UserMoitService
import org.springframework.stereotype.Component

Expand Down Expand Up @@ -57,3 +59,51 @@ class StartAttendancePushNotificationGenerator(
}
}
}

@Component
class RemindFinePushNotificationGenerator(
private val userService: UserService,
private val moitService: MoitService,
private val studyService: StudyService,
private val fineService: FineService,
private val urlSchemeProperties: UrlSchemeProperties,
) : NotificationGenerator {
companion object {
const val TITLE_TEMPLATE = "밀린 벌금이 있어요!"
fun bodyTemplate(userName: String, moitName: String, studyOrder: Int): String {
return "$moitName ${userName}님, 아직 ${studyOrder + 1}회차 스터디의 벌금을 납부하지 않았습니다.\n얼른 납부하고 인증받으세요!"
}
}

override fun support(event: NotificationEvent): Boolean {
return event is RemindFineNotificationEvent
}

override fun generate(event: NotificationEvent): List<NotificationEntity> {
if (event !is RemindFineNotificationEvent) {
throw MoitException.of(MoitExceptionType.SYSTEM_FAIL)
}

return event.fineIds
.map { fineId ->
val fine = fineService.getFine(fineId)
val user = userService.findUserById(fine.userId)
val (userId, userName) = user.id to user.nickname
val moit = moitService.getMoitById(fine.moitId)
val (moitId, moitName) = moit.id to moit.name
val studyOrder = studyService.findById(fine.studyId).order

NotificationEntity(
type = NotificationType.CHECK_FINE_NOTIFICATION,
userId = userId,
title = TITLE_TEMPLATE,
body = bodyTemplate(userName, moitName, studyOrder),
urlScheme = UrlSchemeUtils.generate(
urlSchemeProperties.checkFine,
UrlSchemeParameter("moitId", moitId.toString()),
UrlSchemeParameter("fineId", fineId.toString())
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ data class User(
val profileImage: Int,
val email: String,
val roles: Set<UserRole>,
val fcmToken: String?
)
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class UserEntity(
@Convert(converter = UserRoleConverter::class)
@Column(name = "roles")
val roles: Set<UserRole>,

@Column(name = "fcm_token", nullable = true)
var fcmToken: String?
) : BaseEntity() {
fun toDomain(): User {
return User(
Expand All @@ -41,6 +44,7 @@ class UserEntity(
profileImage = profileImage,
email = email,
roles = roles,
fcmToken = fcmToken
)
}

Expand Down
Loading

0 comments on commit 90041e7

Please sign in to comment.