feat: add filtering conditions to the list of comment components (#3842)

#### What type of PR is this?

/kind improvement
/area core
/milestone 2.5.x

#### What this PR does / why we need it:

为评论接口增加排序条件。当前只增加了根据创建时间进行正/倒序排列,后续持续进行补充。

#### Which issue(s) this PR fixes:

Fixes #3364 

#### Special notes for your reviewer:
调用评论列表接口。增加排序条件 `sort=creationTimestamp,desc` ,查看列表是否正确排序。

#### Does this PR introduce a user-facing change?

```release-note
为评论列表接口增加按照创建时间进行最新/最早排序
```
pull/3870/head
Li 2023-04-27 16:02:23 +08:00 committed by GitHub
parent 8619d96f6a
commit d9f2f77bc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 351 additions and 217 deletions

View File

@ -1,6 +1,7 @@
package run.halo.app.theme.endpoint;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static java.util.Comparator.comparing;
import static org.apache.commons.lang3.BooleanUtils.isFalse;
import static org.apache.commons.lang3.BooleanUtils.isTrue;
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
@ -10,18 +11,23 @@ import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuil
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.fn.builders.schema.Builder;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.data.domain.Sort;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
@ -32,6 +38,8 @@ 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.endpoint.CustomEndpoint;
import run.halo.app.core.extension.endpoint.SortResolver;
import run.halo.app.extension.Comparators;
import run.halo.app.extension.GroupVersion;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Ref;
@ -42,6 +50,7 @@ import run.halo.app.infra.exception.AccessDeniedException;
import run.halo.app.infra.utils.HaloUtils;
import run.halo.app.infra.utils.IpAddressUtils;
import run.halo.app.theme.finders.CommentFinder;
import run.halo.app.theme.finders.CommentPublicQueryService;
import run.halo.app.theme.finders.vo.CommentVo;
import run.halo.app.theme.finders.vo.ReplyVo;
@ -52,7 +61,7 @@ import run.halo.app.theme.finders.vo.ReplyVo;
@RequiredArgsConstructor
public class CommentFinderEndpoint implements CustomEndpoint {
private final CommentFinder commentFinder;
private final CommentPublicQueryService commentPublicQueryService;
private final CommentService commentService;
private final ReplyService replyService;
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
@ -185,15 +194,16 @@ public class CommentFinderEndpoint implements CustomEndpoint {
}
Mono<ServerResponse> listComments(ServerRequest request) {
CommentQuery commentQuery = new CommentQuery(request.queryParams());
return commentFinder.list(commentQuery.toRef(), commentQuery.getPage(),
commentQuery.getSize())
CommentQuery commentQuery = new CommentQuery(request);
var comparator = commentQuery.toComparator();
return commentPublicQueryService.list(commentQuery.toRef(), commentQuery.getPage(),
commentQuery.getSize(), comparator)
.flatMap(list -> ServerResponse.ok().bodyValue(list));
}
Mono<ServerResponse> getComment(ServerRequest request) {
String name = request.pathVariable("name");
return Mono.defer(() -> Mono.justOrEmpty(commentFinder.getByName(name)))
return Mono.defer(() -> Mono.justOrEmpty(commentPublicQueryService.getByName(name)))
.subscribeOn(Schedulers.boundedElastic())
.flatMap(comment -> ServerResponse.ok().bodyValue(comment));
}
@ -202,14 +212,18 @@ public class CommentFinderEndpoint implements CustomEndpoint {
String commentName = request.pathVariable("name");
IListRequest.QueryListRequest queryParams =
new IListRequest.QueryListRequest(request.queryParams());
return commentFinder.listReply(commentName, queryParams.getPage(), queryParams.getSize())
return commentPublicQueryService.listReply(commentName, queryParams.getPage(),
queryParams.getSize())
.flatMap(list -> ServerResponse.ok().bodyValue(list));
}
public static class CommentQuery extends PageableRequest {
public CommentQuery(MultiValueMap<String, String> queryParams) {
super(queryParams);
private final ServerWebExchange exchange;
public CommentQuery(ServerRequest request) {
super(request.queryParams());
this.exchange = request.exchange();
}
@Schema(description = "The comment subject group.")
@ -250,6 +264,17 @@ public class CommentFinderEndpoint implements CustomEndpoint {
return name;
}
@ArraySchema(uniqueItems = true,
arraySchema = @Schema(name = "sort",
description = "Sort property and direction of the list result. Supported fields: "
+ "creationTimestamp"),
schema = @Schema(description = "like field,asc or field,desc",
implementation = String.class,
example = "creationTimestamp,desc"))
public Sort getSort() {
return SortResolver.defaultInstance.resolve(exchange);
}
Ref toRef() {
Ref ref = new Ref();
ref.setGroup(getGroup());
@ -262,6 +287,24 @@ public class CommentFinderEndpoint implements CustomEndpoint {
String emptyToNull(String str) {
return StringUtils.isBlank(str) ? null : str;
}
public Comparator<Comment> toComparator() {
var sort = getSort();
var ctOrder = sort.getOrderFor("creationTimestamp");
List<Comparator<Comment>> comparators = new ArrayList<>();
if (ctOrder != null) {
Comparator<Comment> comparator =
comparing(comment -> comment.getMetadata().getCreationTimestamp());
if (ctOrder.isDescending()) {
comparator = comparator.reversed();
}
comparators.add(comparator);
comparators.add(Comparators.compareName(true));
}
return comparators.stream()
.reduce(Comparator::thenComparing)
.orElse(null);
}
}
public static class PageableRequest extends IListRequest.QueryListRequest {

View File

@ -0,0 +1,32 @@
package run.halo.app.theme.finders;
import java.util.Comparator;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.content.Reply;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Ref;
import run.halo.app.theme.finders.vo.CommentVo;
import run.halo.app.theme.finders.vo.ReplyVo;
/**
* comment finder.
*
* @author LIlGG
*/
public interface CommentPublicQueryService {
Mono<CommentVo> getByName(String name);
Mono<ListResult<CommentVo>> list(Ref ref, @Nullable Integer page,
@Nullable Integer size);
Mono<ListResult<CommentVo>> list(Ref ref, @Nullable Integer page,
@Nullable Integer size, @Nullable Comparator<Comment> comparator);
Mono<ListResult<ReplyVo>> listReply(String commentName, @Nullable Integer page,
@Nullable Integer size);
Mono<ListResult<ReplyVo>> listReply(String commentName, @Nullable Integer page,
@Nullable Integer size, @Nullable Comparator<Reply> comparator);
}

View File

@ -1,37 +1,13 @@
package run.halo.app.theme.finders.impl;
import java.security.Principal;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.util.Assert;
import org.springframework.util.comparator.Comparators;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.content.comment.OwnerInfo;
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.CommentPublicQueryService;
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;
/**
@ -44,185 +20,20 @@ import run.halo.app.theme.finders.vo.ReplyVo;
@RequiredArgsConstructor
public class CommentFinderImpl implements CommentFinder {
private final ReactiveExtensionClient client;
private final UserService userService;
private final CounterService counterService;
private final CommentPublicQueryService commentPublicQueryService;
@Override
public Mono<CommentVo> getByName(String name) {
return client.fetch(Comment.class, name)
.flatMap(this::toCommentVo);
return commentPublicQueryService.getByName(name);
}
@Override
public Mono<ListResult<CommentVo>> list(Ref ref, Integer page, Integer size) {
return fixedCommentPredicate(ref)
.flatMap(fixedPredicate ->
client.list(Comment.class, fixedPredicate,
defaultComparator(),
pageNullSafe(page), sizeNullSafe(size))
.flatMap(list -> Flux.fromStream(list.get().map(this::toCommentVo))
.concatMap(Function.identity())
.collectList()
.map(commentVos -> new ListResult<>(list.getPage(), list.getSize(),
list.getTotal(),
commentVos)
)
)
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()))
);
return commentPublicQueryService.list(ref, page, size);
}
@Override
public Mono<ListResult<ReplyVo>> listReply(String commentName, Integer page, Integer size) {
return fixedReplyPredicate(commentName)
.flatMap(fixedPredicate ->
client.list(Reply.class, fixedPredicate,
ReplyService.creationTimeAscComparator(), pageNullSafe(page),
sizeNullSafe(size))
.flatMap(list -> Flux.fromStream(list.get().map(this::toReplyVo))
.concatMap(Function.identity())
.collectList()
.map(replyVos -> new ListResult<>(list.getPage(), list.getSize(),
list.getTotal(),
replyVos))
)
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()))
);
}
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)
.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())
.doOnNext(replyVo::setOwner)
.thenReturn(replyVo)
);
}
private Mono<OwnerInfo> getOwnerInfo(Comment.CommentOwner owner) {
if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {
return Mono.just(OwnerInfo.from(owner));
}
return userService.getUserOrGhost(owner.getName())
.map(OwnerInfo::from);
}
private Mono<Predicate<Comment>> fixedCommentPredicate(Ref ref) {
Assert.notNull(ref, "Comment subject reference must not be null");
// Ref must be equal to the comment subject
Predicate<Comment> refPredicate = comment -> comment.getSpec().getSubjectRef().equals(ref)
&& comment.getMetadata().getDeletionTimestamp() == null;
// is approved and not hidden
Predicate<Comment> approvedPredicate =
comment -> BooleanUtils.isFalse(comment.getSpec().getHidden())
&& BooleanUtils.isTrue(comment.getSpec().getApproved());
return getCurrentUserWithoutAnonymous()
.map(username -> {
Predicate<Comment> isOwner = comment -> {
Comment.CommentOwner owner = comment.getSpec().getOwner();
return owner != null && StringUtils.equals(username, owner.getName());
};
return approvedPredicate.or(isOwner);
})
.defaultIfEmpty(approvedPredicate)
.map(refPredicate::and);
}
private Mono<Predicate<Reply>> fixedReplyPredicate(String commentName) {
Assert.notNull(commentName, "The commentName must not be null");
// The comment name must be equal to the comment name of the reply
Predicate<Reply> commentNamePredicate =
reply -> reply.getSpec().getCommentName().equals(commentName)
&& reply.getMetadata().getDeletionTimestamp() == null;
// is approved and not hidden
Predicate<Reply> approvedPredicate =
reply -> BooleanUtils.isFalse(reply.getSpec().getHidden())
&& BooleanUtils.isTrue(reply.getSpec().getApproved());
return getCurrentUserWithoutAnonymous()
.map(username -> {
Predicate<Reply> isOwner = reply -> {
Comment.CommentOwner owner = reply.getSpec().getOwner();
return owner != null && StringUtils.equals(username, owner.getName());
};
return approvedPredicate.or(isOwner);
})
.defaultIfEmpty(approvedPredicate)
.map(commentNamePredicate::and);
}
Mono<String> getCurrentUserWithoutAnonymous() {
return ReactiveSecurityContextHolder.getContext()
.mapNotNull(SecurityContext::getAuthentication)
.map(Principal::getName)
.filter(username -> !AnonymousUserConst.PRINCIPAL.equals(username));
}
static Comparator<Comment> defaultComparator() {
return new CommentComparator();
}
static class CommentComparator implements Comparator<Comment> {
@Override
public int compare(Comment c1, Comment c2) {
boolean c1Top = BooleanUtils.isTrue(c1.getSpec().getTop());
boolean c2Top = BooleanUtils.isTrue(c2.getSpec().getTop());
if (c1Top == c2Top) {
int c1Priority = ObjectUtils.defaultIfNull(c1.getSpec().getPriority(), 0);
int c2Priority = ObjectUtils.defaultIfNull(c2.getSpec().getPriority(), 0);
if (c1Top) {
// 都置顶
return Integer.compare(c1Priority, c2Priority);
}
// 两个评论不置顶根据 creationTime 降序排列
return Comparator.comparing(
(Comment comment) -> comment.getSpec().getCreationTime(),
Comparators.nullsLow())
.thenComparing((Comment comment) -> comment.getMetadata().getName())
.compare(c2, c1);
} else if (c1Top) {
// 只有 c1 置顶c1 排前面
return -1;
} else {
// 只有c2置顶, c2排在前面
return 1;
}
}
}
int pageNullSafe(Integer page) {
return ObjectUtils.defaultIfNull(page, 1);
}
int sizeNullSafe(Integer size) {
return ObjectUtils.defaultIfNull(size, 10);
return commentPublicQueryService.listReply(commentName, page, size);
}
}

View File

@ -0,0 +1,244 @@
package run.halo.app.theme.finders.impl;
import java.security.Principal;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.comparator.Comparators;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.content.comment.OwnerInfo;
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.CommentPublicQueryService;
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;
/**
* comment public query service implementation.
*
* @author LIlGG
*/
@Component
@RequiredArgsConstructor
public class CommentPublicQueryServiceImpl implements CommentPublicQueryService {
private final ReactiveExtensionClient client;
private final UserService userService;
private final CounterService counterService;
@Override
public Mono<CommentVo> getByName(String name) {
return client.fetch(Comment.class, name)
.flatMap(this::toCommentVo);
}
@Override
public Mono<ListResult<CommentVo>> list(Ref ref, Integer page, Integer size) {
return list(ref, page, size, defaultComparator());
}
@Override
public Mono<ListResult<CommentVo>> list(Ref ref, Integer page, Integer size,
Comparator<Comment> comparator) {
final Comparator<Comment> commentComparator =
Objects.isNull(comparator) ? defaultComparator()
: comparator.thenComparing(defaultComparator());
return fixedCommentPredicate(ref)
.flatMap(fixedPredicate ->
client.list(Comment.class, fixedPredicate,
commentComparator,
pageNullSafe(page), sizeNullSafe(size))
.flatMap(list -> Flux.fromStream(list.get().map(this::toCommentVo))
.concatMap(Function.identity())
.collectList()
.map(commentVos -> new ListResult<>(list.getPage(), list.getSize(),
list.getTotal(),
commentVos)
)
)
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()))
);
}
@Override
public Mono<ListResult<ReplyVo>> listReply(String commentName, Integer page, Integer size) {
return listReply(commentName, page, size, ReplyService.creationTimeAscComparator());
}
@Override
public Mono<ListResult<ReplyVo>> listReply(String commentName, Integer page, Integer size,
Comparator<Reply> comparator) {
return fixedReplyPredicate(commentName)
.flatMap(fixedPredicate ->
client.list(Reply.class, fixedPredicate,
comparator,
pageNullSafe(page), sizeNullSafe(size))
.flatMap(list -> Flux.fromStream(list.get().map(this::toReplyVo))
.concatMap(Function.identity())
.collectList()
.map(replyVos -> new ListResult<>(list.getPage(), list.getSize(),
list.getTotal(),
replyVos))
)
.defaultIfEmpty(new ListResult<>(page, size, 0L, List.of()))
);
}
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)
.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())
.doOnNext(replyVo::setOwner)
.thenReturn(replyVo)
);
}
private Mono<OwnerInfo> getOwnerInfo(Comment.CommentOwner owner) {
if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {
return Mono.just(OwnerInfo.from(owner));
}
return userService.getUserOrGhost(owner.getName())
.map(OwnerInfo::from);
}
private Mono<Predicate<Comment>> fixedCommentPredicate(Ref ref) {
Assert.notNull(ref, "Comment subject reference must not be null");
// Ref must be equal to the comment subject
Predicate<Comment> refPredicate = comment -> comment.getSpec().getSubjectRef().equals(ref)
&& comment.getMetadata().getDeletionTimestamp() == null;
// is approved and not hidden
Predicate<Comment> approvedPredicate =
comment -> BooleanUtils.isFalse(comment.getSpec().getHidden())
&& BooleanUtils.isTrue(comment.getSpec().getApproved());
return getCurrentUserWithoutAnonymous()
.map(username -> {
Predicate<Comment> isOwner = comment -> {
Comment.CommentOwner owner = comment.getSpec().getOwner();
return owner != null && StringUtils.equals(username, owner.getName());
};
return approvedPredicate.or(isOwner);
})
.defaultIfEmpty(approvedPredicate)
.map(refPredicate::and);
}
private Mono<Predicate<Reply>> fixedReplyPredicate(String commentName) {
Assert.notNull(commentName, "The commentName must not be null");
// The comment name must be equal to the comment name of the reply
Predicate<Reply> commentNamePredicate =
reply -> reply.getSpec().getCommentName().equals(commentName)
&& reply.getMetadata().getDeletionTimestamp() == null;
// is approved and not hidden
Predicate<Reply> approvedPredicate =
reply -> BooleanUtils.isFalse(reply.getSpec().getHidden())
&& BooleanUtils.isTrue(reply.getSpec().getApproved());
return getCurrentUserWithoutAnonymous()
.map(username -> {
Predicate<Reply> isOwner = reply -> {
Comment.CommentOwner owner = reply.getSpec().getOwner();
return owner != null && StringUtils.equals(username, owner.getName());
};
return approvedPredicate.or(isOwner);
})
.defaultIfEmpty(approvedPredicate)
.map(commentNamePredicate::and);
}
Mono<String> getCurrentUserWithoutAnonymous() {
return ReactiveSecurityContextHolder.getContext()
.mapNotNull(SecurityContext::getAuthentication)
.map(Principal::getName)
.filter(username -> !AnonymousUserConst.PRINCIPAL.equals(username));
}
static Comparator<Comment> defaultComparator() {
return new CommentComparator();
}
static class CommentComparator implements Comparator<Comment> {
@Override
public int compare(Comment c1, Comment c2) {
boolean c1Top = BooleanUtils.isTrue(c1.getSpec().getTop());
boolean c2Top = BooleanUtils.isTrue(c2.getSpec().getTop());
if (c1Top == c2Top) {
int c1Priority = ObjectUtils.defaultIfNull(c1.getSpec().getPriority(), 0);
int c2Priority = ObjectUtils.defaultIfNull(c2.getSpec().getPriority(), 0);
if (c1Top) {
// 都置顶
return Integer.compare(c1Priority, c2Priority);
}
// 两个评论不置顶根据 creationTime 降序排列
return Comparator.comparing(
(Comment comment) -> comment.getSpec().getCreationTime(),
Comparators.nullsLow())
.thenComparing((Comment comment) -> comment.getMetadata().getName())
.compare(c2, c1);
} else if (c1Top) {
// 只有 c1 置顶c1 排前面
return -1;
} else {
// 只有c2置顶, c2排在前面
return 1;
}
}
}
int pageNullSafe(Integer page) {
return ObjectUtils.defaultIfNull(page, 1);
}
int sizeNullSafe(Integer size) {
return ObjectUtils.defaultIfNull(size, 10);
}
}

View File

@ -29,6 +29,7 @@ import run.halo.app.extension.ListResult;
import run.halo.app.extension.Ref;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.theme.finders.CommentFinder;
import run.halo.app.theme.finders.CommentPublicQueryService;
/**
* Tests for {@link CommentFinderEndpoint}.
@ -41,6 +42,9 @@ class CommentFinderEndpointTest {
@Mock
private CommentFinder commentFinder;
@Mock
private CommentPublicQueryService commentPublicQueryService;
@Mock
private CommentService commentService;
@ -65,7 +69,7 @@ class CommentFinderEndpointTest {
@Test
void listComments() {
when(commentFinder.list(any(), anyInt(), anyInt()))
when(commentPublicQueryService.list(any(), anyInt(), anyInt(), any()))
.thenReturn(Mono.just(new ListResult<>(1, 10, 0, List.of())));
Ref ref = new Ref();
@ -88,14 +92,14 @@ class CommentFinderEndpointTest {
.expectStatus()
.isOk();
ArgumentCaptor<Ref> refCaptor = ArgumentCaptor.forClass(Ref.class);
verify(commentFinder, times(1)).list(refCaptor.capture(), eq(1), eq(10));
verify(commentPublicQueryService, times(1)).list(refCaptor.capture(), eq(1), eq(10), any());
Ref value = refCaptor.getValue();
assertThat(value).isEqualTo(ref);
}
@Test
void getComment() {
when(commentFinder.getByName(any()))
when(commentPublicQueryService.getByName(any()))
.thenReturn(null);
webTestClient.get()
@ -104,12 +108,12 @@ class CommentFinderEndpointTest {
.expectStatus()
.isOk();
verify(commentFinder, times(1)).getByName(eq("test-comment"));
verify(commentPublicQueryService, times(1)).getByName(eq("test-comment"));
}
@Test
void listCommentReplies() {
when(commentFinder.listReply(any(), anyInt(), anyInt()))
when(commentPublicQueryService.listReply(any(), anyInt(), anyInt()))
.thenReturn(Mono.just(new ListResult<>(2, 20, 0, List.of())));
webTestClient.get()
@ -121,7 +125,7 @@ class CommentFinderEndpointTest {
.expectStatus()
.isOk();
verify(commentFinder, times(1)).listReply(eq("test-comment"), eq(2), eq(20));
verify(commentPublicQueryService, times(1)).listReply(eq("test-comment"), eq(2), eq(20));
}
@Test

View File

@ -43,7 +43,7 @@ import run.halo.app.metrics.CounterService;
* @since 2.0.0
*/
@ExtendWith(SpringExtension.class)
class CommentFinderImplTest {
class CommentPublicQueryServiceImplTest {
@Mock
private ReactiveExtensionClient client;
@ -54,7 +54,7 @@ class CommentFinderImplTest {
private CounterService counterService;
@InjectMocks
private CommentFinderImpl commentFinder;
private CommentPublicQueryServiceImpl commentPublicQueryService;
@BeforeEach
void setUp() {
@ -72,7 +72,7 @@ class CommentFinderImplTest {
mockWhenListComment();
Ref ref = Ref.of("fake-post", GroupVersionKind.fromExtension(Post.class));
commentFinder.list(ref, 1, 10)
commentPublicQueryService.list(ref, 1, 10)
.as(StepVerifier::create)
.consumeNextWith(listResult -> {
assertThat(listResult.getTotal()).isEqualTo(2);
@ -90,7 +90,7 @@ class CommentFinderImplTest {
mockWhenListComment();
Ref ref = Ref.of("fake-post", GroupVersionKind.fromExtension(Post.class));
commentFinder.list(ref, 1, 10)
commentPublicQueryService.list(ref, 1, 10)
.as(StepVerifier::create)
.consumeNextWith(listResult -> {
assertThat(listResult.getTotal()).isEqualTo(2);
@ -108,7 +108,7 @@ class CommentFinderImplTest {
mockWhenListComment();
Ref ref = Ref.of("fake-post", GroupVersionKind.fromExtension(Post.class));
commentFinder.list(ref, 1, 10)
commentPublicQueryService.list(ref, 1, 10)
.as(StepVerifier::create)
.consumeNextWith(listResult -> {
assertThat(listResult.getTotal()).isEqualTo(3);
@ -156,7 +156,7 @@ class CommentFinderImplTest {
var result = Stream.of(comment1, comment2, comment3, comment4, comment5, comment6,
comment7, comment8, comment9, comment10, comment11, comment12, comment13,
comment14)
.sorted(CommentFinderImpl.defaultComparator())
.sorted(CommentPublicQueryServiceImpl.defaultComparator())
.map(Comment::getMetadata)
.map(MetadataOperator::getName)
.collect(Collectors.joining(", "));
@ -265,7 +265,7 @@ class CommentFinderImplTest {
// Mock
mockWhenListRely();
commentFinder.listReply("fake-comment", 1, 10)
commentPublicQueryService.listReply("fake-comment", 1, 10)
.as(StepVerifier::create)
.consumeNextWith(listResult -> {
assertThat(listResult.getTotal()).isEqualTo(2);
@ -283,7 +283,7 @@ class CommentFinderImplTest {
// Mock
mockWhenListRely();
commentFinder.listReply("fake-comment", 1, 10)
commentPublicQueryService.listReply("fake-comment", 1, 10)
.as(StepVerifier::create)
.consumeNextWith(listResult -> {
assertThat(listResult.getTotal()).isEqualTo(2);
@ -299,7 +299,7 @@ class CommentFinderImplTest {
void listWhenUserLoggedIn() {
mockWhenListRely();
commentFinder.listReply("fake-comment", 1, 10)
commentPublicQueryService.listReply("fake-comment", 1, 10)
.as(StepVerifier::create)
.consumeNextWith(listResult -> {
assertThat(listResult.getTotal()).isEqualTo(3);