mirror of https://github.com/halo-dev/halo
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
parent
8619d96f6a
commit
d9f2f77bc2
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
Loading…
Reference in New Issue