Skip to content

Commit d99e8a3

Browse files
committed
feat(activity): Track series chapter and question answer activities
- Add `POST_SERIES_CHAPTER` and `POST_QUESTION_ANSWER` in action - Replace UserActivityDTO with UserActivityProjection and native query-based repository - Expose title and href in user activity API responses - Bump version to 0.5.2
1 parent 81784be commit d99e8a3

11 files changed

Lines changed: 116 additions & 52 deletions

File tree

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ plugins {
1010

1111
group = "me.loghub"
1212

13-
version = "0.5.1-SNAPSHOT"
13+
version = "0.5.2-SNAPSHOT"
1414

1515
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }
1616

src/main/kotlin/me/loghub/api/aspect/question/QuestionAnswerAspect.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import io.github.oshai.kotlinlogging.KotlinLogging
44
import me.loghub.api.dto.notification.NotificationDTO
55
import me.loghub.api.entity.question.QuestionAnswer
66
import me.loghub.api.entity.user.User
7+
import me.loghub.api.entity.user.UserActivity
78
import me.loghub.api.lib.redis.key.RedisKeys
9+
import me.loghub.api.repository.user.UserActivityRepository
810
import me.loghub.api.service.notification.NotificationService
911
import org.aspectj.lang.annotation.AfterReturning
1012
import org.aspectj.lang.annotation.Aspect
@@ -17,6 +19,7 @@ import org.springframework.stereotype.Component
1719
class QuestionAnswerAspect(
1820
private val redisTemplate: RedisTemplate<String, String>,
1921
private val notificationService: NotificationService,
22+
private val userActivityRepository: UserActivityRepository,
2023
) {
2124
private object TrendingScoreDelta {
2225
const val ANSWER = 1.toDouble()
@@ -38,6 +41,7 @@ class QuestionAnswerAspect(
3841
val questionId = postedAnswer.question.id!!
3942
updateTrendingScoreAfterPostAnswer(questionId)
4043
sendNotificationsAfterPostAnswer(postedAnswer)
44+
addUserActivityAfterPostAnswer(postedAnswer)
4145
logAfterPostAnswer(postedAnswer)
4246
}
4347

@@ -80,6 +84,16 @@ class QuestionAnswerAspect(
8084
notificationService.addNotification(question.writer.id!!, notification)
8185
}
8286

87+
private fun addUserActivityAfterPostAnswer(postedAnswer: QuestionAnswer) {
88+
val activity = UserActivity(
89+
action = UserActivity.Action.POST_QUESTION_ANSWER,
90+
user = postedAnswer.writer,
91+
question = postedAnswer.question,
92+
questionAnswer = postedAnswer
93+
)
94+
userActivityRepository.save(activity)
95+
}
96+
8397
private fun logAfterPostAnswer(postedAnswer: QuestionAnswer) =
8498
logger.info { "[QuestionAnswer] posted: { questionId=${postedAnswer.question.id}, answerId=${postedAnswer.id}, writerId=${postedAnswer.writer.id}, content=\"${postedAnswer.content}\" }" }
8599

src/main/kotlin/me/loghub/api/aspect/series/SeriesChapterAspect.kt

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ package me.loghub.api.aspect.series
33
import io.github.oshai.kotlinlogging.KotlinLogging
44
import me.loghub.api.entity.series.SeriesChapter
55
import me.loghub.api.entity.user.User
6+
import me.loghub.api.entity.user.UserActivity
7+
import me.loghub.api.repository.user.UserActivityRepository
68
import org.aspectj.lang.annotation.AfterReturning
79
import org.aspectj.lang.annotation.Aspect
810
import org.springframework.stereotype.Component
911

1012
@Aspect
1113
@Component
12-
class SeriesChapterAspect {
14+
class SeriesChapterAspect(
15+
private val userActivityRepository: UserActivityRepository,
16+
) {
1317
private companion object {
1418
private val logger = KotlinLogging.logger { };
1519
}
@@ -18,26 +22,52 @@ class SeriesChapterAspect {
1822
pointcut = "execution(* me.loghub.api.service.series.SeriesChapterService.createChapter(..))",
1923
returning = "postedChapter"
2024
)
21-
fun afterCreateChapter(postedChapter: SeriesChapter) =
22-
logger.info { "[SeriesChapter] created: { seriesId=${postedChapter.series.id}, chapterId=${postedChapter.id}, writerId=${postedChapter.writer.id}, title=\"${postedChapter.title}\" }" }
25+
fun afterCreateChapter(postedChapter: SeriesChapter) {
26+
addUserActivityAfterPostChapter(postedChapter)
27+
logAfterPostChapter(postedChapter)
28+
}
2329

2430
@AfterReturning(
2531
pointcut = "execution(* me.loghub.api.service.series.SeriesChapterService.importChapter(..)) && args(.., articleId, ..)",
2632
returning = "importedChapter"
2733
)
28-
fun afterImportChapter(articleId: Long, importedChapter: SeriesChapter) =
29-
logger.info { "[SeriesChapter] imported: { seriesId=${importedChapter.series.id}, chapterId=${importedChapter.id}, writerId=${importedChapter.writer.id}, articleId=${articleId}, title=\"${importedChapter.title}\" }" }
34+
fun afterImportChapter(articleId: Long, importedChapter: SeriesChapter) {
35+
addUserActivityAfterPostChapter(importedChapter)
36+
logAfterImportChapter(importedChapter, articleId)
37+
}
3038

3139
@AfterReturning(
3240
pointcut = "execution(* me.loghub.api.service.series.SeriesChapterService.editChapter(..)) && args(seriesId, .., writer)",
3341
returning = "chapter"
3442
)
3543
fun afterEditChapter(seriesId: Long, writer: User, chapter: SeriesChapter) =
36-
logger.info { "[SeriesChapter] edited: { seriesId=${seriesId}, chapterId=${chapter.id}, writerId=${writer.id}, title=\"${chapter.title}\" }" }
44+
logAfterEditChapter(chapter)
3745

3846
@AfterReturning(
3947
pointcut = "execution(* me.loghub.api.service.series.SeriesChapterService.deleteChapter(..)) && args(seriesId, sequence, writer)",
4048
)
4149
fun afterDeleteChapter(seriesId: Long, sequence: Long, writer: User) =
50+
logAfterDeleteChapter(seriesId, sequence, writer)
51+
52+
private fun addUserActivityAfterPostChapter(postedChapter: SeriesChapter) {
53+
val activity = UserActivity(
54+
action = UserActivity.Action.POST_SERIES_CHAPTER,
55+
user = postedChapter.writer,
56+
series = postedChapter.series,
57+
seriesChapter = postedChapter
58+
)
59+
userActivityRepository.save(activity)
60+
}
61+
62+
fun logAfterPostChapter(postedChapter: SeriesChapter) =
63+
logger.info { "[SeriesChapter] posted: { seriesId=${postedChapter.series.id}, chapterId=${postedChapter.id}, writerId=${postedChapter.writer.id}, title=\"${postedChapter.title}\" }" }
64+
65+
fun logAfterImportChapter(importedChapter: SeriesChapter, articleId: Long) =
66+
logger.info { "[SeriesChapter] imported: { seriesId=${importedChapter.series.id}, chapterId=${importedChapter.id}, writerId=${importedChapter.writer.id}, articleId=${articleId}, title=\"${importedChapter.title}\" }" }
67+
68+
fun logAfterEditChapter(editedChapter: SeriesChapter) =
69+
logger.info { "[SeriesChapter] edited: { seriesId=${editedChapter.series.id}, chapterId=${editedChapter.id}, writerId=${editedChapter.writer.id}, title=\"${editedChapter.title}\" }" }
70+
71+
fun logAfterDeleteChapter(seriesId: Long, sequence: Long, writer: User) =
4272
logger.info { "[SeriesChapter] deleted: { seriesId=${seriesId}, chapterSequence=${sequence}, writerId=${writer.id} }" }
4373
}

src/main/kotlin/me/loghub/api/controller/user/UserActivityController.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package me.loghub.api.controller.user
22

3-
import me.loghub.api.dto.user.activity.UserActivityDTO
3+
import me.loghub.api.dto.user.activity.UserActivityProjection
44
import me.loghub.api.dto.user.activity.UserActivitySummaryDTO
55
import me.loghub.api.service.user.UserActivityService
66
import org.springframework.http.ResponseEntity
@@ -24,7 +24,7 @@ class UserActivityController(private val userActivityService: UserActivityServic
2424
fun getActivity(
2525
@PathVariable userId: Long,
2626
@PathVariable date: LocalDate
27-
): ResponseEntity<List<UserActivityDTO>> {
27+
): ResponseEntity<List<UserActivityProjection>> {
2828
val activities = userActivityService.getActivities(userId, date)
2929
return ResponseEntity.ok(activities)
3030
}

src/main/kotlin/me/loghub/api/dto/user/activity/UserActivityDTO.kt

Lines changed: 0 additions & 12 deletions
This file was deleted.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package me.loghub.api.dto.user.activity
2+
3+
import me.loghub.api.entity.user.UserActivity
4+
5+
interface UserActivityProjection {
6+
val id: Long;
7+
val title: String;
8+
val href: String;
9+
val action: UserActivity.Action;
10+
}

src/main/kotlin/me/loghub/api/entity/user/UserActivity.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package me.loghub.api.entity.user
33
import jakarta.persistence.*
44
import me.loghub.api.entity.article.Article
55
import me.loghub.api.entity.question.Question
6+
import me.loghub.api.entity.question.QuestionAnswer
67
import me.loghub.api.entity.series.Series
8+
import me.loghub.api.entity.series.SeriesChapter
79
import org.hibernate.annotations.JdbcType
810
import org.hibernate.dialect.PostgreSQLEnumJdbcType
911
import org.springframework.data.annotation.CreatedDate
@@ -36,17 +38,27 @@ class UserActivity(
3638
@JoinColumn(name = "series_id", nullable = true)
3739
val series: Series? = null,
3840

41+
@ManyToOne(fetch = FetchType.EAGER, optional = true)
42+
@JoinColumn(name = "series_chapter_id", nullable = true)
43+
val seriesChapter: SeriesChapter? = null,
44+
3945
@ManyToOne(fetch = FetchType.EAGER, optional = true)
4046
@JoinColumn(name = "question_id", nullable = true)
4147
val question: Question? = null,
4248

49+
@ManyToOne(fetch = FetchType.EAGER, optional = true)
50+
@JoinColumn(name = "question_answer_id", nullable = true)
51+
val questionAnswer: QuestionAnswer? = null,
52+
4353
@ManyToOne(fetch = FetchType.LAZY, optional = false)
4454
@JoinColumn(name = "user_id", nullable = false)
4555
val user: User,
4656
) {
4757
enum class Action {
4858
POST_ARTICLE,
4959
POST_SERIES,
50-
POST_QUESTION
60+
POST_SERIES_CHAPTER,
61+
POST_QUESTION,
62+
POST_QUESTION_ANSWER,
5163
}
5264
}
Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package me.loghub.api.repository.user
22

3-
import me.loghub.api.dto.user.activity.UserActivityDTO
3+
import me.loghub.api.dto.user.activity.UserActivityProjection
44
import me.loghub.api.dto.user.activity.UserActivitySummaryDTO
55
import me.loghub.api.entity.user.User
66
import me.loghub.api.entity.user.UserActivity
@@ -12,33 +12,39 @@ interface UserActivityRepository : JpaRepository<UserActivity, Long> {
1212
private companion object {
1313
const val SELECT_SUMMARY_DTO =
1414
"SELECT new me.loghub.api.dto.user.activity.UserActivitySummaryDTO(ua.createdDate, COUNT(ua)) FROM UserActivity ua"
15-
const val SELECT_DTO = """
16-
SELECT new me.loghub.api.dto.user.activity.UserActivityDTO(
17-
ua.id,
18-
CASE
19-
WHEN ua.action = 'POST_ARTICLE' THEN ua.article.slug
20-
WHEN ua.action = 'POST_SERIES' THEN ua.series.slug
21-
WHEN ua.action = 'POST_QUESTION' THEN ua.question.slug
22-
END,
23-
CASE
24-
WHEN ua.action = 'POST_ARTICLE' THEN ua.article.title
25-
WHEN ua.action = 'POST_SERIES' THEN ua.series.title
26-
WHEN ua.action = 'POST_QUESTION' THEN ua.question.title
27-
END,
28-
ua.action,
29-
ua.createdAt
30-
)
31-
FROM UserActivity ua
32-
LEFT JOIN ua.article
33-
LEFT JOIN ua.series
34-
LEFT JOIN ua.question
35-
"""
3615
const val BY_USER = "ua.user = :user"
3716
const val BY_CREATED_DATE_BETWEEN = "ua.createdDate BETWEEN :from AND :to"
38-
const val BY_CREATED_DATE = "ua.createdDate = :createdDate"
3917
const val GROUP_BY_CREATED_DATE = "GROUP BY ua.createdDate"
4018
const val ORDER_BY_CREATED_DATE_ASC = "ORDER BY ua.createdDate ASC"
41-
const val ORDER_BY_CREATED_AT_DESC = "ORDER BY ua.createdAt DESC"
19+
}
20+
21+
private object NativeQuery {
22+
const val SELECT_PROJECTION_BY_USER_AND_CREATED_DATE = """
23+
SELECT ua.id,
24+
CASE
25+
WHEN ua.action = 'POST_ARTICLE' THEN a.title
26+
WHEN ua.action = 'POST_SERIES' THEN s.title
27+
WHEN ua.action = 'POST_SERIES_CHAPTER' THEN sc.title
28+
WHEN ua.action = 'POST_QUESTION' THEN q.title
29+
WHEN ua.action = 'POST_QUESTION_ANSWER' THEN qa.title
30+
END AS title,
31+
CASE
32+
WHEN ua.action = 'POST_ARTICLE' THEN CONCAT('/articles/', a.writer_username, '/', a.slug)
33+
WHEN ua.action = 'POST_SERIES' THEN CONCAT('/series/', s.writer_username, '/', s.slug)
34+
WHEN ua.action = 'POST_SERIES_CHAPTER' THEN CONCAT('/series/', s.writer_username, '/', s.slug, '/', sc.sequence)
35+
WHEN ua.action = 'POST_QUESTION' THEN CONCAT('/questions/', q.writer_username, '/', q.slug)
36+
WHEN ua.action = 'POST_QUESTION_ANSWER' THEN CONCAT('/questions/', q.writer_username, '/', q.slug, '#answer-', qa.id)
37+
END AS href,
38+
ua.action
39+
FROM user_activities ua
40+
LEFT JOIN articles a ON ua.article_id = a.id
41+
LEFT JOIN series s ON ua.series_id = s.id
42+
LEFT JOIN series_chapters sc ON ua.series_chapter_id = sc.id
43+
LEFT JOIN questions q ON ua.question_id = q.id
44+
LEFT JOIN question_answers qa ON ua.question_answer_id = qa.id
45+
WHERE ua.user_id = :userId AND ua.created_date = :createdDate
46+
ORDER BY ua.id DESC;
47+
"""
4248
}
4349

4450
@Query("$SELECT_SUMMARY_DTO WHERE $BY_USER AND $BY_CREATED_DATE_BETWEEN $GROUP_BY_CREATED_DATE $ORDER_BY_CREATED_DATE_ASC")
@@ -48,6 +54,6 @@ interface UserActivityRepository : JpaRepository<UserActivity, Long> {
4854
to: LocalDate,
4955
): List<UserActivitySummaryDTO>
5056

51-
@Query("$SELECT_DTO WHERE $BY_USER AND $BY_CREATED_DATE $ORDER_BY_CREATED_AT_DESC")
52-
fun findDTOByUserAndCreatedDate(user: User, createdDate: LocalDate): List<UserActivityDTO>
57+
@Query(value = NativeQuery.SELECT_PROJECTION_BY_USER_AND_CREATED_DATE, nativeQuery = true)
58+
fun findProjectionByUserIdAndCreatedDate(userId: Long, createdDate: LocalDate): List<UserActivityProjection>
5359
}

src/main/kotlin/me/loghub/api/service/user/UserActivityService.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package me.loghub.api.service.user
22

33
import me.loghub.api.constant.message.ResponseMessage
4-
import me.loghub.api.dto.user.activity.UserActivityDTO
4+
import me.loghub.api.dto.user.activity.UserActivityProjection
55
import me.loghub.api.dto.user.activity.UserActivitySummaryDTO
66
import me.loghub.api.repository.user.UserActivityRepository
77
import me.loghub.api.repository.user.UserRepository
@@ -24,10 +24,10 @@ class UserActivityService(
2424
}
2525

2626
@Transactional(readOnly = true)
27-
fun getActivities(userId: Long, date: LocalDate): List<UserActivityDTO> {
27+
fun getActivities(userId: Long, date: LocalDate): List<UserActivityProjection> {
2828
checkField("date", !date.isAfter(LocalDate.now())) { ResponseMessage.User.INVALID_DATE_RANGE }
2929

3030
val user = userRepository.getReferenceById(userId)
31-
return userActivityRepository.findDTOByUserAndCreatedDate(user, date)
31+
return userActivityRepository.findProjectionByUserIdAndCreatedDate(userId, date)
3232
}
3333
}

src/main/resources/database/public/tables.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,12 @@ CREATE TABLE IF NOT EXISTS public.user_activities
200200
CONSTRAINT user_activities_article_id_fk REFERENCES public.articles ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
201201
series_id bigint
202202
CONSTRAINT user_activities_series_id_fk REFERENCES public.series ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
203+
series_chapter_id bigint
204+
CONSTRAINT user_activities_series_chapter_id_fk REFERENCES public.series_chapters ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
203205
question_id bigint
204206
CONSTRAINT user_activities_question_id_fk REFERENCES public.questions ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
207+
question_answer_id bigint
208+
CONSTRAINT user_activities_question_answer_id_fk REFERENCES public.question_answers ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED,
205209
user_id bigint NOT NULL
206210
CONSTRAINT user_activities_user_id_fk REFERENCES public.users ON DELETE CASCADE
207211
);

0 commit comments

Comments
 (0)