From 1a9e2f046a2d80706a38365100b4a2421f610e38 Mon Sep 17 00:00:00 2001 From: Li Date: Tue, 28 Feb 2023 18:48:17 +0800 Subject: [PATCH] feat: add stats to the user-sdie comments api (#3366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /milestone 2.3.x /kind feature #### What this PR does / why we need it: 为访客端的评论和回复接口 聚合点赞数 #### Which issue(s) this PR fixes: Fixes #3347 #### Special notes for your reviewer: 同步修改了 finder API 及 console 位置的接口 如何测试: 1. 调用接口 `/apis/api.halo.run/v1alpha1/trackers/upvote` 增加点赞数。 2. 使用 console 接口 `/apis/api.halo.run/v1alpha1/comments/{name}` 查看目标评论是否增加点赞数。 #### Does this PR introduce a user-facing change? ```release-note 访客端评论及回复列表支持返回点赞数据 ``` --- .../content/comment/CommentServiceImpl.java | 24 ++++++++++++-- .../app/content/comment/CommentStats.java | 23 ++++++++++++++ .../app/content/comment/ListedComment.java | 13 +++++--- .../halo/app/content/comment/ListedReply.java | 11 ++++--- .../app/content/comment/ReplyServiceImpl.java | 20 +++++++++++- .../theme/finders/impl/CommentFinderImpl.java | 31 ++++++++++++++++--- .../app/theme/finders/vo/CommentStatsVo.java | 22 +++++++++++++ .../halo/app/theme/finders/vo/CommentVo.java | 17 +++++----- .../halo/app/theme/finders/vo/ReplyVo.java | 15 ++++----- .../comment/CommentServiceImplTest.java | 30 ++++++++++++++++++ .../finders/impl/CommentFinderImplTest.java | 15 +++++++++ 11 files changed, 190 insertions(+), 31 deletions(-) create mode 100644 src/main/java/run/halo/app/content/comment/CommentStats.java create mode 100644 src/main/java/run/halo/app/theme/finders/vo/CommentStatsVo.java diff --git a/src/main/java/run/halo/app/content/comment/CommentServiceImpl.java b/src/main/java/run/halo/app/content/comment/CommentServiceImpl.java index 464714dc1..cabe88e1e 100644 --- a/src/main/java/run/halo/app/content/comment/CommentServiceImpl.java +++ b/src/main/java/run/halo/app/content/comment/CommentServiceImpl.java @@ -11,6 +11,7 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.stereotype.Component; +import org.springframework.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.core.extension.User; @@ -22,6 +23,8 @@ import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.Ref; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.exception.AccessDeniedException; +import run.halo.app.metrics.CounterService; +import run.halo.app.metrics.MeterUtils; import run.halo.app.plugin.ExtensionComponentsFinder; /** @@ -38,14 +41,17 @@ public class CommentServiceImpl implements CommentService { private final ExtensionComponentsFinder extensionComponentsFinder; private final SystemConfigurableEnvironmentFetcher environmentFetcher; + private final CounterService counterService; public CommentServiceImpl(ReactiveExtensionClient client, UserService userService, ExtensionComponentsFinder extensionComponentsFinder, - SystemConfigurableEnvironmentFetcher environmentFetcher) { + SystemConfigurableEnvironmentFetcher environmentFetcher, + CounterService counterService) { this.client = client; this.userService = userService; this.extensionComponentsFinder = extensionComponentsFinder; this.environmentFetcher = environmentFetcher; + this.counterService = counterService; } @Override @@ -151,7 +157,21 @@ public class CommentServiceImpl implements CommentService { }) .switchIfEmpty(Mono.just(builder)) ) - .map(ListedComment.ListedCommentBuilder::build); + .map(ListedComment.ListedCommentBuilder::build) + .flatMap(lc -> fetchStats(comment) + .doOnNext(lc::setStats) + .thenReturn(lc)); + } + + Mono fetchStats(Comment comment) { + Assert.notNull(comment, "The comment must not be null."); + String name = comment.getMetadata().getName(); + return counterService.getByName(MeterUtils.nameOf(Comment.class, name)) + .map(counter -> CommentStats.builder() + .upvote(counter.getUpvote()) + .build() + ) + .defaultIfEmpty(CommentStats.empty()); } private Mono getCommentOwnerInfo(Comment.CommentOwner owner) { diff --git a/src/main/java/run/halo/app/content/comment/CommentStats.java b/src/main/java/run/halo/app/content/comment/CommentStats.java new file mode 100644 index 000000000..7de0642fc --- /dev/null +++ b/src/main/java/run/halo/app/content/comment/CommentStats.java @@ -0,0 +1,23 @@ +package run.halo.app.content.comment; + +import lombok.Builder; +import lombok.Value; + +/** + * comment stats value object. + * + * @author LIlGG + * @since 2.0.0 + */ +@Value +@Builder +public class CommentStats { + + Integer upvote; + + public static CommentStats empty() { + return CommentStats.builder() + .upvote(0) + .build(); + } +} diff --git a/src/main/java/run/halo/app/content/comment/ListedComment.java b/src/main/java/run/halo/app/content/comment/ListedComment.java index ef2f9402c..d235dc3ab 100644 --- a/src/main/java/run/halo/app/content/comment/ListedComment.java +++ b/src/main/java/run/halo/app/content/comment/ListedComment.java @@ -2,7 +2,7 @@ package run.halo.app.content.comment; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; -import lombok.Value; +import lombok.Data; import run.halo.app.core.extension.content.Comment; import run.halo.app.extension.Extension; @@ -12,15 +12,18 @@ import run.halo.app.extension.Extension; * @author guqing * @since 2.0.0 */ -@Value +@Data @Builder public class ListedComment { @Schema(required = true) - Comment comment; + private Comment comment; @Schema(required = true) - OwnerInfo owner; + private OwnerInfo owner; - Extension subject; + private Extension subject; + + @Schema(required = true) + private CommentStats stats; } diff --git a/src/main/java/run/halo/app/content/comment/ListedReply.java b/src/main/java/run/halo/app/content/comment/ListedReply.java index e4027bdd3..678aa98b7 100644 --- a/src/main/java/run/halo/app/content/comment/ListedReply.java +++ b/src/main/java/run/halo/app/content/comment/ListedReply.java @@ -2,7 +2,7 @@ package run.halo.app.content.comment; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; -import lombok.Value; +import lombok.Data; import run.halo.app.core.extension.content.Reply; /** @@ -11,13 +11,16 @@ import run.halo.app.core.extension.content.Reply; * @author guqing * @since 2.0.0 */ -@Value +@Data @Builder public class ListedReply { @Schema(required = true) - Reply reply; + private Reply reply; @Schema(required = true) - OwnerInfo owner; + private OwnerInfo owner; + + @Schema(required = true) + private CommentStats stats; } diff --git a/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java b/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java index d98025a9e..d1a929405 100644 --- a/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java +++ b/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.BooleanUtils; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.core.extension.User; @@ -18,6 +19,8 @@ import run.halo.app.core.extension.service.UserService; import run.halo.app.extension.Extension; import run.halo.app.extension.ListResult; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.metrics.CounterService; +import run.halo.app.metrics.MeterUtils; /** * A default implementation of {@link ReplyService}. @@ -31,6 +34,7 @@ public class ReplyServiceImpl implements ReplyService { private final ReactiveExtensionClient client; private final UserService userService; + private final CounterService counterService; @Override public Mono create(String commentName, Reply reply) { @@ -95,7 +99,21 @@ public class ReplyServiceImpl implements ReplyService { builder.owner(ownerInfo); return builder; }) - .map(ListedReply.ListedReplyBuilder::build); + .map(ListedReply.ListedReplyBuilder::build) + .flatMap(listedReply -> fetchStats(reply) + .doOnNext(listedReply::setStats) + .thenReturn(listedReply)); + } + + Mono fetchStats(Reply reply) { + Assert.notNull(reply, "The reply must not be null."); + String name = reply.getMetadata().getName(); + return counterService.getByName(MeterUtils.nameOf(Reply.class, name)) + .map(counter -> CommentStats.builder() + .upvote(counter.getUpvote()) + .build() + ) + .defaultIfEmpty(CommentStats.empty()); } private Mono getOwnerInfo(Reply reply) { diff --git a/src/main/java/run/halo/app/theme/finders/impl/CommentFinderImpl.java b/src/main/java/run/halo/app/theme/finders/impl/CommentFinderImpl.java index 4afb78b2a..1fbe160f1 100644 --- a/src/main/java/run/halo/app/theme/finders/impl/CommentFinderImpl.java +++ b/src/main/java/run/halo/app/theme/finders/impl/CommentFinderImpl.java @@ -20,13 +20,18 @@ import run.halo.app.content.comment.ReplyService; import run.halo.app.core.extension.content.Comment; import run.halo.app.core.extension.content.Reply; import run.halo.app.core.extension.service.UserService; +import run.halo.app.extension.AbstractExtension; import run.halo.app.extension.ListResult; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.Ref; import run.halo.app.infra.AnonymousUserConst; +import run.halo.app.metrics.CounterService; +import run.halo.app.metrics.MeterUtils; import run.halo.app.theme.finders.CommentFinder; import run.halo.app.theme.finders.Finder; +import run.halo.app.theme.finders.vo.CommentStatsVo; import run.halo.app.theme.finders.vo.CommentVo; +import run.halo.app.theme.finders.vo.ExtensionVoOperator; import run.halo.app.theme.finders.vo.ReplyVo; /** @@ -41,6 +46,7 @@ public class CommentFinderImpl implements CommentFinder { private final ReactiveExtensionClient client; private final UserService userService; + private final CounterService counterService; @Override public Mono getByName(String name) { @@ -88,17 +94,34 @@ public class CommentFinderImpl implements CommentFinder { private Mono toCommentVo(Comment comment) { Comment.CommentOwner owner = comment.getSpec().getOwner(); return Mono.just(CommentVo.from(comment)) + .flatMap(commentVo -> populateStats(Comment.class, commentVo) + .doOnNext(commentVo::setStats) + .thenReturn(commentVo)) .flatMap(commentVo -> getOwnerInfo(owner) - .map(commentVo::withOwner) - .defaultIfEmpty(commentVo) + .doOnNext(commentVo::setOwner) + .thenReturn(commentVo) ); } + private Mono + populateStats(Class clazz, T vo) { + return counterService.getByName(MeterUtils.nameOf(clazz, vo.getMetadata() + .getName())) + .map(counter -> CommentStatsVo.builder() + .upvote(counter.getUpvote()) + .build() + ) + .defaultIfEmpty(CommentStatsVo.empty()); + } + private Mono toReplyVo(Reply reply) { return Mono.just(ReplyVo.from(reply)) + .flatMap(replyVo -> populateStats(Reply.class, replyVo) + .doOnNext(replyVo::setStats) + .thenReturn(replyVo)) .flatMap(replyVo -> getOwnerInfo(reply.getSpec().getOwner()) - .map(replyVo::withOwner) - .defaultIfEmpty(replyVo) + .doOnNext(replyVo::setOwner) + .thenReturn(replyVo) ); } diff --git a/src/main/java/run/halo/app/theme/finders/vo/CommentStatsVo.java b/src/main/java/run/halo/app/theme/finders/vo/CommentStatsVo.java new file mode 100644 index 000000000..c8ece1913 --- /dev/null +++ b/src/main/java/run/halo/app/theme/finders/vo/CommentStatsVo.java @@ -0,0 +1,22 @@ +package run.halo.app.theme.finders.vo; + +import lombok.Builder; +import lombok.Value; + +/** + * comment stats value object. + * + * @author LIlGG + * @since 2.0.0 + */ +@Value +@Builder +public class CommentStatsVo { + Integer upvote; + + public static CommentStatsVo empty() { + return CommentStatsVo.builder() + .upvote(0) + .build(); + } +} diff --git a/src/main/java/run/halo/app/theme/finders/vo/CommentVo.java b/src/main/java/run/halo/app/theme/finders/vo/CommentVo.java index d71579083..1f2515955 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/CommentVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/CommentVo.java @@ -2,9 +2,8 @@ package run.halo.app.theme.finders.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; +import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.Value; -import lombok.With; import run.halo.app.content.comment.OwnerInfo; import run.halo.app.core.extension.content.Comment; import run.halo.app.extension.MetadataOperator; @@ -15,22 +14,24 @@ import run.halo.app.extension.MetadataOperator; * @author guqing * @since 2.0.0 */ -@Value +@Data @Builder @EqualsAndHashCode public class CommentVo implements ExtensionVoOperator { @Schema(required = true) - MetadataOperator metadata; + private MetadataOperator metadata; @Schema(required = true) - Comment.CommentSpec spec; + private Comment.CommentSpec spec; - Comment.CommentStatus status; + private Comment.CommentStatus status; - @With @Schema(required = true) - OwnerInfo owner; + private OwnerInfo owner; + + @Schema(required = true) + private CommentStatsVo stats; /** * Convert {@link Comment} to {@link CommentVo}. diff --git a/src/main/java/run/halo/app/theme/finders/vo/ReplyVo.java b/src/main/java/run/halo/app/theme/finders/vo/ReplyVo.java index 3931db54a..a67640217 100644 --- a/src/main/java/run/halo/app/theme/finders/vo/ReplyVo.java +++ b/src/main/java/run/halo/app/theme/finders/vo/ReplyVo.java @@ -2,10 +2,9 @@ package run.halo.app.theme.finders.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; +import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; -import lombok.Value; -import lombok.With; import run.halo.app.content.comment.OwnerInfo; import run.halo.app.core.extension.content.Reply; import run.halo.app.extension.MetadataOperator; @@ -16,21 +15,23 @@ import run.halo.app.extension.MetadataOperator; * @author guqing * @since 2.0.0 */ -@Value +@Data @Builder @ToString @EqualsAndHashCode public class ReplyVo implements ExtensionVoOperator { @Schema(required = true) - MetadataOperator metadata; + private MetadataOperator metadata; @Schema(required = true) - Reply.ReplySpec spec; + private Reply.ReplySpec spec; - @With @Schema(required = true) - OwnerInfo owner; + private OwnerInfo owner; + + @Schema(required = true) + private CommentStatsVo stats; /** * Convert {@link Reply} to {@link ReplyVo}. diff --git a/src/test/java/run/halo/app/content/comment/CommentServiceImplTest.java b/src/test/java/run/halo/app/content/comment/CommentServiceImplTest.java index b645cdb43..09a29b74b 100644 --- a/src/test/java/run/halo/app/content/comment/CommentServiceImplTest.java +++ b/src/test/java/run/halo/app/content/comment/CommentServiceImplTest.java @@ -28,6 +28,7 @@ import org.springframework.util.MultiValueMap; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import run.halo.app.content.TestPost; +import run.halo.app.core.extension.Counter; import run.halo.app.core.extension.User; import run.halo.app.core.extension.content.Comment; import run.halo.app.core.extension.content.Post; @@ -39,6 +40,8 @@ import run.halo.app.extension.Ref; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; import run.halo.app.infra.utils.JsonUtils; +import run.halo.app.metrics.CounterService; +import run.halo.app.metrics.MeterUtils; import run.halo.app.plugin.ExtensionComponentsFinder; /** @@ -65,6 +68,9 @@ class CommentServiceImplTest { @InjectMocks private CommentServiceImpl commentService; + @Mock + private CounterService counterService; + @BeforeEach void setUp() { SystemSetting.Comment commentSetting = getCommentSetting(); @@ -111,6 +117,21 @@ class CommentServiceImplTest { Mono> listResultMono = commentService.listComment(new CommentQuery(new LinkedMultiValueMap<>())); + Counter counterA = new Counter(); + counterA.setUpvote(3); + String commentACounter = MeterUtils.nameOf(Comment.class, "A"); + when(counterService.getByName(eq(commentACounter))).thenReturn(Mono.just(counterA)); + + Counter counterB = new Counter(); + counterB.setUpvote(9); + String commentBCounter = MeterUtils.nameOf(Comment.class, "B"); + when(counterService.getByName(eq(commentBCounter))).thenReturn(Mono.just(counterB)); + + Counter counterC = new Counter(); + counterC.setUpvote(0); + String commentCCounter = MeterUtils.nameOf(Comment.class, "C"); + when(counterService.getByName(eq(commentCCounter))).thenReturn(Mono.just(counterC)); + StepVerifier.create(listResultMono) .consumeNextWith(result -> { try { @@ -320,6 +341,9 @@ class CommentServiceImplTest { "metadata": { "name": "fake-post" } + }, + "stats": { + "upvote": 3 } }, { @@ -362,6 +386,9 @@ class CommentServiceImplTest { "metadata": { "name": "fake-post" } + }, + "stats": { + "upvote": 9 } }, { @@ -403,6 +430,9 @@ class CommentServiceImplTest { "metadata": { "name": "fake-post" } + }, + "stats": { + "upvote": 0 } } ], diff --git a/src/test/java/run/halo/app/theme/finders/impl/CommentFinderImplTest.java b/src/test/java/run/halo/app/theme/finders/impl/CommentFinderImplTest.java index e0ee7252c..2cd4308f1 100644 --- a/src/test/java/run/halo/app/theme/finders/impl/CommentFinderImplTest.java +++ b/src/test/java/run/halo/app/theme/finders/impl/CommentFinderImplTest.java @@ -21,6 +21,7 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit.jupiter.SpringExtension; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import run.halo.app.core.extension.Counter; import run.halo.app.core.extension.User; import run.halo.app.core.extension.content.Comment; import run.halo.app.core.extension.content.Post; @@ -33,6 +34,7 @@ import run.halo.app.extension.MetadataOperator; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.Ref; import run.halo.app.infra.AnonymousUserConst; +import run.halo.app.metrics.CounterService; /** * Tests for {@link CommentFinderImpl}. @@ -48,6 +50,9 @@ class CommentFinderImplTest { @Mock private UserService userService; + @Mock + private CounterService counterService; + @InjectMocks private CommentFinderImpl commentFinder; @@ -92,6 +97,7 @@ class CommentFinderImplTest { assertThat(listResult.getItems().size()).isEqualTo(2); assertThat(listResult.getItems().get(0).getMetadata().getName()) .isEqualTo("comment-approved"); + assertThat(listResult.getItems().get(0).getStats().getUpvote()).isEqualTo(9); }) .verifyComplete(); } @@ -225,6 +231,10 @@ class CommentFinderImplTest { extractedUser(); when(client.fetch(eq(User.class), any())).thenReturn(Mono.just(createUser())); + + Counter counter = new Counter(); + counter.setUpvote(9); + when(counterService.getByName(any())).thenReturn(Mono.just(counter)); } Comment createComment() { @@ -262,6 +272,7 @@ class CommentFinderImplTest { assertThat(listResult.getItems().size()).isEqualTo(2); assertThat(listResult.getItems().get(0).getMetadata().getName()) .isEqualTo("reply-approved"); + assertThat(listResult.getItems().get(0).getStats().getUpvote()).isEqualTo(9); }) .verifyComplete(); } @@ -355,6 +366,10 @@ class CommentFinderImplTest { extractedUser(); when(client.fetch(eq(User.class), any())).thenReturn(Mono.just(createUser())); + + Counter counter = new Counter(); + counter.setUpvote(9); + when(counterService.getByName(any())).thenReturn(Mono.just(counter)); } Reply createReply() {