feat: add stats to the user-sdie comments api (#3366)

#### 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
访客端评论及回复列表支持返回点赞数据
```
pull/3390/head^2
Li 2023-02-28 18:48:17 +08:00 committed by GitHub
parent 848857fbfd
commit 1a9e2f046a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 190 additions and 31 deletions

View File

@ -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<CommentStats> 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<OwnerInfo> getCommentOwnerInfo(Comment.CommentOwner owner) {

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<Reply> 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<CommentStats> 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<OwnerInfo> getOwnerInfo(Reply reply) {

View File

@ -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<CommentVo> getByName(String name) {
@ -88,17 +94,34 @@ public class CommentFinderImpl implements CommentFinder {
private Mono<CommentVo> 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 <E extends AbstractExtension, T extends ExtensionVoOperator> Mono<CommentStatsVo>
populateStats(Class<E> 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<ReplyVo> 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)
);
}

View File

@ -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();
}
}

View File

@ -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}.

View File

@ -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}.

View File

@ -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<ListResult<ListedComment>> 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
}
}
],

View File

@ -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() {