-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* feat: Record 모델및 필터 구현 * feat: Browse 서비스 구현 * feat: MailSendRecorder 구현 * feat: MailServiceArgsGenerator 구현 * refactor: WorkBookSubscriberWriter 책임 분리 * chore: 불필요 테스트 삭제 * test: 필터 클래스 테스트 추가
- Loading branch information
1 parent
8a23160
commit 4b11861
Showing
23 changed files
with
454 additions
and
746 deletions.
There are no files selected for viewing
252 changes: 62 additions & 190 deletions
252
batch/src/main/kotlin/com/few/batch/service/article/writer/WorkBookSubscriberWriter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,241 +1,113 @@ | ||
package com.few.batch.service.article.writer | ||
|
||
import com.few.batch.data.common.code.BatchCategoryType | ||
import com.few.batch.data.common.code.BatchMemberType | ||
import com.few.batch.service.article.dto.WorkBookSubscriberItem | ||
import com.few.batch.service.article.dto.toMemberIds | ||
import com.few.batch.service.article.dto.toTargetWorkBookIds | ||
import com.few.batch.service.article.dto.toTargetWorkBookProgress | ||
import com.few.batch.service.article.writer.model.* | ||
import com.few.batch.service.article.writer.service.* | ||
import com.few.batch.service.article.writer.support.MailSendRecorder | ||
import com.few.batch.service.article.writer.support.MailServiceArgsGenerator | ||
import com.few.email.service.article.SendArticleEmailService | ||
import com.few.email.service.article.dto.Content | ||
import com.few.email.service.article.dto.SendArticleEmailArgs | ||
import jooq.jooq_dsl.tables.* | ||
import org.jooq.DSLContext | ||
import org.jooq.UpdateConditionStep | ||
import org.jooq.impl.DSL | ||
import org.springframework.stereotype.Component | ||
import org.springframework.transaction.annotation.Transactional | ||
import java.net.URL | ||
import java.time.LocalDate | ||
import java.time.LocalDateTime | ||
|
||
data class MemberReceiveArticle( | ||
val workbookId: Long, | ||
val articleId: Long, | ||
val dayCol: Long, | ||
) | ||
|
||
data class MemberReceiveArticles( | ||
val articles: List<MemberReceiveArticle>, | ||
) { | ||
fun getByWorkBookIdAndDayCol(workbookId: Long, dayCol: Long): MemberReceiveArticle { | ||
return articles.find { | ||
it.workbookId == workbookId && it.dayCol == dayCol | ||
} ?: throw IllegalArgumentException("Cannot find article by workbookId: $workbookId, dayCol: $dayCol") | ||
} | ||
|
||
fun getArticleIds(): List<Long> { | ||
return articles.map { | ||
it.articleId | ||
} | ||
} | ||
} | ||
|
||
data class ArticleContent( | ||
val id: Long, | ||
val category: String, // todo fix | ||
val articleTitle: String, | ||
val articleContent: String, | ||
val writerName: String, | ||
val writerLink: URL, | ||
) | ||
|
||
data class ReceiveLastDayMember( | ||
val memberId: Long, | ||
val targetWorkBookId: Long, | ||
) | ||
|
||
fun List<ArticleContent>.peek(articleId: Long): ArticleContent { | ||
return this.find { | ||
it.id == articleId | ||
} ?: throw IllegalArgumentException("Cannot find article by articleId: $articleId") | ||
} | ||
|
||
@Component | ||
class WorkBookSubscriberWriter( | ||
private val dslContext: DSLContext, | ||
|
||
private val browseMemberEmailService: BrowseMemberEmailService, | ||
private val browseMemberReceiveArticlesService: BrowseMemberReceiveArticlesService, | ||
private val browseArticleContentsService: BrowseArticleContentsService, | ||
private val browseWorkbookLastDayColService: BrowseWorkbookLastDayColService, | ||
|
||
private val sendArticleEmailService: SendArticleEmailService, | ||
) { | ||
|
||
companion object { | ||
private const val ARTICLE_SUBJECT_TEMPLATE = "Day%d %s" | ||
private const val ARTICLE_TEMPLATE = "article" | ||
} | ||
|
||
/** | ||
* 구독자들에게 이메일을 전송하고 진행률을 업데이트한다. | ||
*/ | ||
@Transactional | ||
fun execute(items: List<WorkBookSubscriberItem>): Map<Any, Any> { | ||
val memberT = Member.MEMBER | ||
val mappingWorkbookArticleT = MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE | ||
val articleIfoT = ArticleIfo.ARTICLE_IFO | ||
val articleMstT = ArticleMst.ARTICLE_MST | ||
val subscriptionT = Subscription.SUBSCRIPTION | ||
|
||
val memberIds = items.toMemberIds() | ||
val targetWorkBookIds = items.toTargetWorkBookIds() | ||
val targetWorkBookProgress = items.toTargetWorkBookProgress() | ||
|
||
/** 회원 ID를 기준으로 이메일을 조회한다.*/ | ||
val memberEmailRecords = dslContext.select( | ||
memberT.ID, | ||
memberT.EMAIL | ||
) | ||
.from(memberT) | ||
.where(memberT.ID.`in`(memberIds)) | ||
.fetch() | ||
.intoMap(memberT.ID, memberT.EMAIL) | ||
|
||
/** 구독자들이 구독한 학습지 ID와 구독자의 학습지 구독 진행률을 기준으로 구독자가 받을 학습지 정보를 조회한다.*/ | ||
val memberReceiveArticles = targetWorkBookProgress.keys.stream().map { workbookId -> | ||
val dayCols = targetWorkBookProgress[workbookId]!!.stream().map { it + 1L }.toList() | ||
// todo check refactoring | ||
dslContext.select( | ||
mappingWorkbookArticleT.WORKBOOK_ID.`as`(MemberReceiveArticle::workbookId.name), | ||
mappingWorkbookArticleT.ARTICLE_ID.`as`(MemberReceiveArticle::articleId.name), | ||
mappingWorkbookArticleT.DAY_COL.`as`(MemberReceiveArticle::dayCol.name) | ||
) | ||
.from(mappingWorkbookArticleT) | ||
.where( | ||
mappingWorkbookArticleT.WORKBOOK_ID.eq(workbookId) | ||
) | ||
.and(mappingWorkbookArticleT.DAY_COL.`in`(dayCols)) | ||
.and(mappingWorkbookArticleT.DELETED_AT.isNull) | ||
.fetchInto(MemberReceiveArticle::class.java) | ||
}.flatMap { it.stream() }.toList().let { | ||
MemberReceiveArticles(it) | ||
} | ||
|
||
/** 구독자들이 받을 학습지 정보를 기준으로 학습지 관련 정보를 조회한다.*/ | ||
val articleContents = dslContext.select( | ||
articleIfoT.ARTICLE_MST_ID.`as`(ArticleContent::id.name), | ||
articleIfoT.CONTENT.`as`(ArticleContent::articleContent.name), | ||
articleMstT.TITLE.`as`(ArticleContent::articleTitle.name), | ||
articleMstT.CATEGORY_CD.`as`(ArticleContent::category.name), | ||
DSL.jsonGetAttributeAsText(memberT.DESCRIPTION, "name").`as`(ArticleContent::writerName.name), | ||
DSL.jsonGetAttribute(memberT.DESCRIPTION, "url").`as`(ArticleContent::writerLink.name) | ||
) | ||
.from(articleIfoT) | ||
.join(articleMstT) | ||
.on(articleIfoT.ARTICLE_MST_ID.eq(articleMstT.ID)) | ||
.join(memberT) | ||
.on( | ||
articleMstT.MEMBER_ID.eq(memberT.ID) | ||
.and(memberT.TYPE_CD.eq(BatchMemberType.WRITER.code)) | ||
) | ||
.where(articleIfoT.ARTICLE_MST_ID.`in`(memberReceiveArticles.getArticleIds())) | ||
.and(articleIfoT.DELETED_AT.isNull) | ||
.fetchInto(ArticleContent::class.java) | ||
|
||
// todo fix | ||
val memberSuccessRecords = memberIds.associateWith { true }.toMutableMap() | ||
val failRecords = mutableMapOf<String, ArrayList<Map<Long, String>>>() | ||
// todo check !! target is not null | ||
val date = LocalDate.now() | ||
val emailServiceArgs = items.map { | ||
val toEmail = memberEmailRecords[it.memberId]!! | ||
val memberArticle = memberReceiveArticles.getByWorkBookIdAndDayCol(it.targetWorkBookId, it.progress + 1) | ||
val articleContent = articleContents.peek(memberArticle.articleId).let { article -> | ||
Content( | ||
articleLink = URL("https://www.fewletter.com/article/${memberArticle.articleId}"), | ||
currentDate = date, | ||
category = BatchCategoryType.convertToDisplayName(article.category.toByte()), | ||
articleDay = memberArticle.dayCol.toInt(), | ||
articleTitle = article.articleTitle, | ||
writerName = article.writerName, | ||
writerLink = article.writerLink, | ||
articleContent = article.articleContent, | ||
problemLink = URL("https://www.fewletter.com/problem?articleId=${memberArticle.articleId}"), | ||
unsubscribeLink = URL("https://www.fewletter.com/unsubscribe?user=${memberEmailRecords[it.memberId]}&workbookId=${it.targetWorkBookId}&articleId=${memberArticle.articleId}") | ||
) | ||
} | ||
return@map it.memberId to | ||
SendArticleEmailArgs( | ||
toEmail, | ||
ARTICLE_SUBJECT_TEMPLATE.format(memberArticle.dayCol, articleContent.articleTitle), | ||
ARTICLE_TEMPLATE, | ||
articleContent | ||
) | ||
} | ||
|
||
// todo refactoring to send email in parallel or batch | ||
/** 이메일 전송을 위한 데이터 조회 */ | ||
val memberEmailRecords = browseMemberEmailService.execute(memberIds) | ||
val workbooksMappedLastDayCol = browseWorkbookLastDayColService.execute(targetWorkBookIds) | ||
val memberReceiveArticles = | ||
browseMemberReceiveArticlesService.execute(targetWorkBookProgress) | ||
val articleContents = browseArticleContentsService.execute(memberReceiveArticles.getArticleIds()) | ||
|
||
/** 조회한 데이터를 이용하여 이메일 전송을 위한 인자 생성 */ | ||
val emailServiceArgs = MailServiceArgsGenerator( | ||
LocalDate.now(), | ||
items, | ||
memberEmailRecords, | ||
memberReceiveArticles, | ||
articleContents | ||
).generate() | ||
|
||
/** 이메일 전송 */ | ||
val mailSendRecorder = MailSendRecorder(emailServiceArgs) | ||
emailServiceArgs.forEach { | ||
try { | ||
sendArticleEmailService.send(it.second) | ||
sendArticleEmailService.send(it.sendArticleEmailArgs) | ||
} catch (e: Exception) { | ||
memberSuccessRecords[it.first] = false | ||
failRecords["EmailSendFail"] = failRecords.getOrDefault("EmailSendFail", arrayListOf()).apply { | ||
val message = e.message ?: "Unknown Error" | ||
add(mapOf(it.first to message)) | ||
} | ||
mailSendRecorder.recordFail( | ||
it.memberId, | ||
it.workbookId, | ||
e.message ?: "Unknown Error" | ||
) | ||
} | ||
} | ||
|
||
/** 워크북 마지막 학습지 DAY_COL을 조회한다 */ | ||
val lastDayCol = dslContext.select( | ||
mappingWorkbookArticleT.WORKBOOK_ID, | ||
DSL.max(mappingWorkbookArticleT.DAY_COL) | ||
) | ||
.from(mappingWorkbookArticleT) | ||
.where(mappingWorkbookArticleT.WORKBOOK_ID.`in`(targetWorkBookIds)) | ||
.and(mappingWorkbookArticleT.DELETED_AT.isNull) | ||
.groupBy(mappingWorkbookArticleT.WORKBOOK_ID) | ||
.fetch() | ||
.intoMap(mappingWorkbookArticleT.WORKBOOK_ID, DSL.max(mappingWorkbookArticleT.DAY_COL)) | ||
|
||
/** 마지막 학습지를 받은 구독자들의 ID를 필터링한다.*/ | ||
val receiveLastDayMembers = items.filter { | ||
it.targetWorkBookId in lastDayCol.keys | ||
}.filter { | ||
(it.progress.toInt() + 1) == lastDayCol[it.targetWorkBookId] | ||
}.map { | ||
ReceiveLastDayMember(it.memberId, it.targetWorkBookId) | ||
} | ||
/** 이메일 전송 결과에 따라 진행률 업데이트 및 구독 해지 처리를 위한 데이터 생성 */ | ||
val receiveLastDayRecords = | ||
ReceiveLastArticleRecordFilter(items, workbooksMappedLastDayCol).filter() | ||
.map { | ||
ReceiveLastArticleRecord(it.memberId, it.targetWorkBookId) | ||
} | ||
|
||
val receiveLastDayMemberIds = receiveLastDayMembers.map { | ||
it.memberId | ||
val updateTargetMemberRecords = UpdateProgressRecordFilter( | ||
items, | ||
mailSendRecorder.getSuccessMemberIds(), | ||
receiveLastDayRecords.getMemberIds() | ||
).filter().map { | ||
UpdateProgressRecord(it.memberId, it.targetWorkBookId, it.progress) | ||
} | ||
|
||
/** 이메일 전송에 성공한 구독자들의 진행률을 업데이트한다.*/ | ||
val successMemberIds = memberSuccessRecords.filter { it.value }.keys | ||
val updateTargetMemberRecords = items.filter { it.memberId in successMemberIds }.filterNot { it.memberId in receiveLastDayMemberIds } | ||
|
||
/** 진행률 업데이트 */ | ||
val updateQueries = mutableListOf<UpdateConditionStep<*>>() | ||
for (updateTargetMemberRecord in updateTargetMemberRecords) { | ||
updateQueries.add( | ||
dslContext.update(subscriptionT) | ||
.set(subscriptionT.PROGRESS, updateTargetMemberRecord.progress + 1) | ||
.where(subscriptionT.MEMBER_ID.eq(updateTargetMemberRecord.memberId)) | ||
.and(subscriptionT.TARGET_WORKBOOK_ID.eq(updateTargetMemberRecord.targetWorkBookId)) | ||
dslContext.update(Subscription.SUBSCRIPTION) | ||
.set(Subscription.SUBSCRIPTION.PROGRESS, updateTargetMemberRecord.updatedProgress) | ||
.where(Subscription.SUBSCRIPTION.MEMBER_ID.eq(updateTargetMemberRecord.memberId)) | ||
.and(Subscription.SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(updateTargetMemberRecord.targetWorkBookId)) | ||
) | ||
} | ||
dslContext.batch(updateQueries).execute() | ||
|
||
/** 마지막 학습지를 받은 구독자들은 구독을 해지한다.*/ | ||
/** 학습지의 마지막 아티클을 받은 구독자 구독 해지 */ | ||
val receiveLastDayQueries = mutableListOf<UpdateConditionStep<*>>() | ||
for (receiveLastDayMember in receiveLastDayMembers) { | ||
for (receiveLastDayMember in receiveLastDayRecords) { | ||
receiveLastDayQueries.add( | ||
dslContext.update(subscriptionT) | ||
.set(subscriptionT.DELETED_AT, LocalDateTime.now()) | ||
.set(subscriptionT.UNSUBS_OPINION, "receive.all") | ||
.where(subscriptionT.MEMBER_ID.eq(receiveLastDayMember.memberId)) | ||
.and(subscriptionT.TARGET_WORKBOOK_ID.eq(receiveLastDayMember.targetWorkBookId)) | ||
dslContext.update(Subscription.SUBSCRIPTION) | ||
.set(Subscription.SUBSCRIPTION.DELETED_AT, LocalDateTime.now()) | ||
.set(Subscription.SUBSCRIPTION.UNSUBS_OPINION, "receive.all") | ||
.where(Subscription.SUBSCRIPTION.MEMBER_ID.eq(receiveLastDayMember.memberId)) | ||
.and(Subscription.SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(receiveLastDayMember.targetWorkBookId)) | ||
) | ||
} | ||
dslContext.batch(receiveLastDayQueries).execute() | ||
|
||
return if (failRecords.isNotEmpty()) { | ||
mapOf("records" to memberSuccessRecords, "fail" to failRecords) | ||
} else { | ||
mapOf("records" to memberSuccessRecords) | ||
} | ||
return mailSendRecorder.getExecutionResult() | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
batch/src/main/kotlin/com/few/batch/service/article/writer/model/ReceiveLastArticleRecord.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.few.batch.service.article.writer.model | ||
|
||
fun List<ReceiveLastArticleRecord>.getMemberIds(): Set<Long> { | ||
return this.map { it.memberId }.toSet() | ||
} | ||
|
||
/** 학습지의 마지막 아티클을 받은 구독자 정보 */ | ||
data class ReceiveLastArticleRecord( | ||
val memberId: Long, | ||
val targetWorkBookId: Long, | ||
) |
20 changes: 20 additions & 0 deletions
20
.../main/kotlin/com/few/batch/service/article/writer/model/ReceiveLastArticleRecordFilter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package com.few.batch.service.article.writer.model | ||
|
||
import com.few.batch.service.article.dto.WorkBookSubscriberItem | ||
|
||
/** | ||
* 학습지의 마지막 아티클을 받은 구독자 정보 필터 | ||
*/ | ||
class ReceiveLastArticleRecordFilter( | ||
private val items: List<WorkBookSubscriberItem>, | ||
private val workbooksMappedLastDayCol: Map<Long, Int>, | ||
) { | ||
|
||
fun filter(): List<WorkBookSubscriberItem> { | ||
return items.filter { | ||
it.targetWorkBookId in workbooksMappedLastDayCol.keys | ||
}.filter { | ||
(it.progress.toInt() + 1) == workbooksMappedLastDayCol[it.targetWorkBookId] | ||
} | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
batch/src/main/kotlin/com/few/batch/service/article/writer/model/UpdateProgressRecord.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.few.batch.service.article.writer.model | ||
|
||
data class UpdateProgressRecord( | ||
val memberId: Long, | ||
val targetWorkBookId: Long, | ||
val progress: Long, | ||
) { | ||
val updatedProgress: Long = progress + 1 | ||
} |
18 changes: 18 additions & 0 deletions
18
.../src/main/kotlin/com/few/batch/service/article/writer/model/UpdateProgressRecordFilter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package com.few.batch.service.article.writer.model | ||
|
||
import com.few.batch.service.article.dto.WorkBookSubscriberItem | ||
|
||
/** | ||
* 진행률을 업데이트할 구독자 정보 필터 | ||
* - 학습지를 성공적으로 받은 구독자만 진행률을 업데이트한다. | ||
* - 학습지의 마지막 아티클을 받은 구독자는 진행률을 업데이트하지 않고 구독을 해지한다. | ||
*/ | ||
class UpdateProgressRecordFilter( | ||
private val items: List<WorkBookSubscriberItem>, | ||
private val successMemberIds: Set<Long>, | ||
private val receiveLastDayArticleRecordMemberIds: Set<Long>, | ||
) { | ||
fun filter(): List<WorkBookSubscriberItem> { | ||
return items.filter { it.memberId in successMemberIds }.filterNot { it.memberId in receiveLastDayArticleRecordMemberIds } | ||
} | ||
} |
Oops, something went wrong.