mirror of https://github.com/halo-dev/halo
refactor: simplify the code of reconciler for comment and optimize performance (#5504)
#### What type of PR is this? /kind improvement /area core /milestone 2.14.x #### What this PR does / why we need it: 优化评论控制器的实现逻辑以优化代码和性能 Resolves #5435 how to test it? - 测试删除评论时能正确连同回复一起删除 - 测试评论下的最新回复的已读功能是否正确 - 删除/审核评论,观察主题端和Console端分别显示的评论数量是否正确 #### Does this PR introduce a user-facing change? ```release-note None ```pull/5516/head
parent
0435e42123
commit
7c3f8b9be2
|
@ -1,8 +1,10 @@
|
|||
package run.halo.app.content.comment;
|
||||
|
||||
import org.springframework.lang.NonNull;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.Ref;
|
||||
|
||||
/**
|
||||
* An application service for {@link Comment}.
|
||||
|
@ -15,4 +17,6 @@ public interface CommentService {
|
|||
Mono<ListResult<ListedComment>> listComment(CommentQuery query);
|
||||
|
||||
Mono<Comment> create(Comment comment);
|
||||
|
||||
Mono<Void> removeBySubject(@NonNull Ref subjectRef);
|
||||
}
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
package run.halo.app.content.comment;
|
||||
|
||||
import static run.halo.app.extension.index.query.QueryFactory.and;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.function.Function;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.Assert;
|
||||
|
@ -12,9 +18,13 @@ import run.halo.app.core.extension.User;
|
|||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.core.extension.service.UserService;
|
||||
import run.halo.app.extension.Extension;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
import run.halo.app.infra.exception.AccessDeniedException;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
|
@ -114,6 +124,31 @@ public class CommentServiceImpl implements CommentService {
|
|||
.flatMap(client::create);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> removeBySubject(@NonNull Ref subjectRef) {
|
||||
Assert.notNull(subjectRef, "The subjectRef must not be null.");
|
||||
// ascending order by creation time and name
|
||||
var pageRequest = PageRequestImpl.of(1, 200,
|
||||
Sort.by("metadata.creationTimestamp", "metadata.name"));
|
||||
return Flux.defer(() -> listCommentsByRef(subjectRef, pageRequest))
|
||||
.expand(page -> page.hasNext()
|
||||
? listCommentsByRef(subjectRef, pageRequest)
|
||||
: Mono.empty()
|
||||
)
|
||||
.flatMap(page -> Flux.fromIterable(page.getItems()))
|
||||
.flatMap(client::delete)
|
||||
.then();
|
||||
}
|
||||
|
||||
Mono<ListResult<Comment>> listCommentsByRef(Ref subjectRef, PageRequest pageRequest) {
|
||||
var listOptions = new ListOptions();
|
||||
listOptions.setFieldSelector(FieldSelector.of(
|
||||
and(equal("spec.subjectRef", Comment.toSubjectRefKey(subjectRef)),
|
||||
isNull("metadata.deletionTimestamp"))
|
||||
));
|
||||
return client.listBy(Comment.class, listOptions, pageRequest);
|
||||
}
|
||||
|
||||
private boolean checkCommentOwner(Comment comment, Boolean onlySystemUser) {
|
||||
Comment.CommentOwner owner = comment.getSpec().getOwner();
|
||||
if (Boolean.TRUE.equals(onlySystemUser)) {
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
package run.halo.app.content.comment;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.function.Function;
|
||||
import org.springframework.util.comparator.Comparators;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Reply;
|
||||
import run.halo.app.extension.ListResult;
|
||||
|
@ -20,19 +16,5 @@ public interface ReplyService {
|
|||
|
||||
Mono<ListResult<ListedReply>> list(ReplyQuery query);
|
||||
|
||||
/**
|
||||
* Ascending order by creation time.
|
||||
*
|
||||
* @return reply comparator
|
||||
*/
|
||||
static Comparator<Reply> creationTimeAscComparator() {
|
||||
Function<Reply, Instant> creationTime = reply -> reply.getSpec().getCreationTime();
|
||||
Function<Reply, Instant> metadataCreationTime =
|
||||
reply -> reply.getMetadata().getCreationTimestamp();
|
||||
// ascending order by creation time
|
||||
// asc nulls high will be placed at the end
|
||||
return Comparator.comparing(creationTime, Comparators.nullsHigh())
|
||||
.thenComparing(metadataCreationTime)
|
||||
.thenComparing(reply -> reply.getMetadata().getName());
|
||||
}
|
||||
Mono<Void> removeAllByComment(String commentName);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
package run.halo.app.content.comment;
|
||||
|
||||
import static run.halo.app.extension.index.query.QueryFactory.and;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
|
||||
|
||||
import java.time.Instant;
|
||||
|
@ -7,6 +10,7 @@ import java.util.function.Function;
|
|||
import java.util.function.Predicate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
@ -17,8 +21,12 @@ 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.Extension;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.metrics.CounterService;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
|
||||
|
@ -89,6 +97,31 @@ public class ReplyServiceImpl implements ReplyService {
|
|||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> removeAllByComment(String commentName) {
|
||||
Assert.notNull(commentName, "The commentName must not be null.");
|
||||
// ascending order by creation time and name
|
||||
var pageRequest = PageRequestImpl.of(1, 200,
|
||||
Sort.by("metadata.creationTimestamp", "metadata.name"));
|
||||
return Flux.defer(() -> listRepliesByComment(commentName, pageRequest))
|
||||
.expand(page -> page.hasNext()
|
||||
? listRepliesByComment(commentName, pageRequest)
|
||||
: Mono.empty()
|
||||
)
|
||||
.flatMap(page -> Flux.fromIterable(page.getItems()))
|
||||
.flatMap(client::delete)
|
||||
.then();
|
||||
}
|
||||
|
||||
Mono<ListResult<Reply>> listRepliesByComment(String commentName, PageRequest pageRequest) {
|
||||
var listOptions = new ListOptions();
|
||||
listOptions.setFieldSelector(FieldSelector.of(
|
||||
and(equal("spec.commentName", commentName),
|
||||
isNull("metadata.deletionTimestamp"))
|
||||
));
|
||||
return client.listBy(Reply.class, listOptions, pageRequest);
|
||||
}
|
||||
|
||||
private Mono<ListedReply> toListedReply(Reply reply) {
|
||||
ListedReply.ListedReplyBuilder builder = ListedReply.builder()
|
||||
.reply(reply);
|
||||
|
|
|
@ -2,33 +2,39 @@ package run.halo.app.core.extension.reconciler;
|
|||
|
||||
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||
import static run.halo.app.extension.ExtensionUtil.addFinalizers;
|
||||
import static run.halo.app.extension.ExtensionUtil.isDeleted;
|
||||
import static run.halo.app.extension.ExtensionUtil.removeFinalizers;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.and;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.content.comment.ReplyNotificationSubscriptionHelper;
|
||||
import run.halo.app.content.comment.ReplyService;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Reply;
|
||||
import run.halo.app.event.post.CommentCreatedEvent;
|
||||
import run.halo.app.event.post.CommentUnreadReplyCountChangedEvent;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.GroupVersionKind;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.SchemeManager;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.extension.index.query.Query;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
|
||||
/**
|
||||
|
@ -43,6 +49,7 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
|
|||
public static final String FINALIZER_NAME = "comment-protection";
|
||||
private final ExtensionClient client;
|
||||
private final SchemeManager schemeManager;
|
||||
private final ReplyService replyService;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
private final ReplyNotificationSubscriptionHelper replyNotificationSubscriptionHelper;
|
||||
|
@ -52,7 +59,10 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
|
|||
client.fetch(Comment.class, request.name())
|
||||
.ifPresent(comment -> {
|
||||
if (isDeleted(comment)) {
|
||||
cleanUpResourcesAndRemoveFinalizer(request.name());
|
||||
if (removeFinalizers(comment.getMetadata(), Set.of(FINALIZER_NAME))) {
|
||||
cleanUpResources(comment);
|
||||
client.update(comment);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (addFinalizers(comment.getMetadata(), Set.of(FINALIZER_NAME))) {
|
||||
|
@ -62,9 +72,14 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
|
|||
|
||||
replyNotificationSubscriptionHelper.subscribeNewReplyReasonForComment(comment);
|
||||
|
||||
compatibleCreationTime(request.name());
|
||||
reconcileStatus(request.name());
|
||||
updateSameSubjectRefCommentCounter(comment.getSpec().getSubjectRef());
|
||||
compatibleCreationTime(comment);
|
||||
Comment.CommentStatus status = comment.getStatusOrDefault();
|
||||
status.setHasNewReply(defaultIfNull(status.getUnreadReplyCount(), 0) > 0);
|
||||
|
||||
updateUnReplyCountIfNecessary(comment);
|
||||
updateSameSubjectRefCommentCounter(comment);
|
||||
|
||||
client.update(comment);
|
||||
});
|
||||
return new Result(false, null);
|
||||
}
|
||||
|
@ -79,39 +94,14 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
|
|||
/**
|
||||
* If the comment creation time is null, set it to the approved time or the current time.
|
||||
* TODO remove this method in the future and fill in attributes in hook mode instead.
|
||||
*
|
||||
* @param name comment name
|
||||
*/
|
||||
void compatibleCreationTime(String name) {
|
||||
client.fetch(Comment.class, name).ifPresent(comment -> {
|
||||
Instant creationTime = comment.getSpec().getCreationTime();
|
||||
Instant oldCreationTime =
|
||||
creationTime == null ? null : Instant.ofEpochMilli(creationTime.toEpochMilli());
|
||||
if (creationTime == null) {
|
||||
creationTime = defaultIfNull(comment.getSpec().getApprovedTime(), Instant.now());
|
||||
comment.getSpec().setCreationTime(creationTime);
|
||||
}
|
||||
|
||||
if (!Objects.equals(oldCreationTime, comment.getSpec().getCreationTime())) {
|
||||
client.update(comment);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isDeleted(Comment comment) {
|
||||
return comment.getMetadata().getDeletionTimestamp() != null;
|
||||
}
|
||||
|
||||
private void reconcileStatus(String name) {
|
||||
client.fetch(Comment.class, name).ifPresent(comment -> {
|
||||
Comment oldComment = JsonUtils.deepCopy(comment);
|
||||
Comment.CommentStatus status = comment.getStatusOrDefault();
|
||||
status.setHasNewReply(defaultIfNull(status.getUnreadReplyCount(), 0) > 0);
|
||||
updateUnReplyCountIfNecessary(comment);
|
||||
if (!oldComment.equals(comment)) {
|
||||
client.update(comment);
|
||||
}
|
||||
});
|
||||
void compatibleCreationTime(Comment comment) {
|
||||
var creationTime = comment.getSpec().getCreationTime();
|
||||
if (creationTime == null) {
|
||||
creationTime = defaultIfNull(comment.getSpec().getApprovedTime(),
|
||||
comment.getMetadata().getCreationTimestamp());
|
||||
}
|
||||
comment.getSpec().setCreationTime(creationTime);
|
||||
}
|
||||
|
||||
private void updateUnReplyCountIfNecessary(Comment comment) {
|
||||
|
@ -121,80 +111,71 @@ public class CommentReconciler implements Reconciler<Reconciler.Request> {
|
|||
if (lastReadTime != null && lastReadTime.toString().equals(lastReadTimeAnno)) {
|
||||
return;
|
||||
}
|
||||
// spec.lastReadTime is null or not equal to annotation.lastReadTime
|
||||
// delegate to other handler though event
|
||||
String commentName = comment.getMetadata().getName();
|
||||
List<Reply> replies = client.list(Reply.class,
|
||||
reply -> commentName.equals(reply.getSpec().getCommentName())
|
||||
&& reply.getMetadata().getDeletionTimestamp() == null,
|
||||
ReplyService.creationTimeAscComparator());
|
||||
|
||||
// calculate unread reply count
|
||||
comment.getStatusOrDefault()
|
||||
.setUnreadReplyCount(Comment.getUnreadReplyCount(replies, lastReadTime));
|
||||
eventPublisher.publishEvent(new CommentUnreadReplyCountChangedEvent(this, commentName));
|
||||
// handled flag
|
||||
if (lastReadTime != null) {
|
||||
annotations.put(Constant.LAST_READ_TIME_ANNO, lastReadTime.toString());
|
||||
} else {
|
||||
annotations.remove(Constant.LAST_READ_TIME_ANNO);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSameSubjectRefCommentCounter(Ref commentSubjectRef) {
|
||||
List<Comment> comments = client.list(Comment.class,
|
||||
comment -> !isDeleted(comment)
|
||||
&& commentSubjectRef.equals(comment.getSpec().getSubjectRef()),
|
||||
null);
|
||||
|
||||
private void updateSameSubjectRefCommentCounter(Comment comment) {
|
||||
var commentSubjectRef = comment.getSpec().getSubjectRef();
|
||||
GroupVersionKind groupVersionKind = groupVersionKind(commentSubjectRef);
|
||||
if (groupVersionKind == null) {
|
||||
return;
|
||||
}
|
||||
// approved total count
|
||||
long approvedTotalCount = comments.stream()
|
||||
.filter(comment -> BooleanUtils.isTrue(comment.getSpec().getApproved()))
|
||||
.count();
|
||||
// total count
|
||||
int totalCount = comments.size();
|
||||
|
||||
var totalCount = countTotalComments(commentSubjectRef);
|
||||
var approvedTotalCount = countApprovedComments(commentSubjectRef);
|
||||
schemeManager.fetch(groupVersionKind).ifPresent(scheme -> {
|
||||
String counterName = MeterUtils.nameOf(commentSubjectRef.getGroup(), scheme.plural(),
|
||||
commentSubjectRef.getName());
|
||||
client.fetch(Counter.class, counterName).ifPresentOrElse(counter -> {
|
||||
counter.setTotalComment(totalCount);
|
||||
counter.setApprovedComment((int) approvedTotalCount);
|
||||
counter.setApprovedComment(approvedTotalCount);
|
||||
client.update(counter);
|
||||
}, () -> {
|
||||
Counter counter = Counter.emptyCounter(counterName);
|
||||
counter.setTotalComment(totalCount);
|
||||
counter.setApprovedComment((int) approvedTotalCount);
|
||||
counter.setApprovedComment(approvedTotalCount);
|
||||
client.create(counter);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void cleanUpResourcesAndRemoveFinalizer(String commentName) {
|
||||
client.fetch(Comment.class, commentName).ifPresent(comment -> {
|
||||
cleanUpResources(comment);
|
||||
if (comment.getMetadata().getFinalizers() != null) {
|
||||
comment.getMetadata().getFinalizers().remove(FINALIZER_NAME);
|
||||
}
|
||||
client.update(comment);
|
||||
});
|
||||
int countTotalComments(Ref commentSubjectRef) {
|
||||
var totalListOptions = new ListOptions();
|
||||
totalListOptions.setFieldSelector(FieldSelector.of(getBaseQuery(commentSubjectRef)));
|
||||
return (int) client.listBy(Comment.class, totalListOptions, PageRequestImpl.ofSize(1))
|
||||
.getTotal();
|
||||
}
|
||||
|
||||
int countApprovedComments(Ref commentSubjectRef) {
|
||||
var approvedListOptions = new ListOptions();
|
||||
approvedListOptions.setFieldSelector(FieldSelector.of(and(
|
||||
getBaseQuery(commentSubjectRef),
|
||||
equal("spec.approved", BooleanUtils.TRUE)
|
||||
)));
|
||||
return (int) client.listBy(Comment.class, approvedListOptions, PageRequestImpl.ofSize(1))
|
||||
.getTotal();
|
||||
}
|
||||
|
||||
private static Query getBaseQuery(Ref commentSubjectRef) {
|
||||
return and(equal("spec.subjectRef", Comment.toSubjectRefKey(commentSubjectRef)),
|
||||
isNull("metadata.deletionTimestamp"));
|
||||
}
|
||||
|
||||
private void cleanUpResources(Comment comment) {
|
||||
// delete all replies under current comment
|
||||
client.list(Reply.class, reply -> comment.getMetadata().getName()
|
||||
.equals(reply.getSpec().getCommentName()),
|
||||
null)
|
||||
.forEach(client::delete);
|
||||
replyService.removeAllByComment(comment.getMetadata().getName()).block();
|
||||
|
||||
// decrement total comment count
|
||||
updateSameSubjectRefCommentCounter(comment.getSpec().getSubjectRef());
|
||||
updateSameSubjectRefCommentCounter(comment);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private GroupVersionKind groupVersionKind(Ref ref) {
|
||||
if (ref == null) {
|
||||
return null;
|
||||
}
|
||||
@NonNull
|
||||
private GroupVersionKind groupVersionKind(@NonNull Ref ref) {
|
||||
return new GroupVersionKind(ref.getGroup(), ref.getVersion(), ref.getKind());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,8 +25,8 @@ import org.springframework.stereotype.Component;
|
|||
import run.halo.app.content.ContentWrapper;
|
||||
import run.halo.app.content.NotificationReasonConst;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.content.comment.CommentService;
|
||||
import run.halo.app.content.permalinks.PostPermalinkPolicy;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.Post.PostPhase;
|
||||
|
@ -75,6 +75,7 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
private final PostService postService;
|
||||
private final PostPermalinkPolicy postPermalinkPolicy;
|
||||
private final CounterService counterService;
|
||||
private final CommentService commentService;
|
||||
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final NotificationCenter notificationCenter;
|
||||
|
@ -329,9 +330,7 @@ public class PostReconciler implements Reconciler<Reconciler.Request> {
|
|||
listSnapshots(ref).forEach(client::delete);
|
||||
|
||||
// clean up comments
|
||||
client.list(Comment.class, comment -> ref.equals(comment.getSpec().getSubjectRef()),
|
||||
null)
|
||||
.forEach(client::delete);
|
||||
commentService.removeBySubject(ref).block();
|
||||
|
||||
// delete counter
|
||||
counterService.deleteByName(MeterUtils.nameOf(Post.class, post.getMetadata().getName()))
|
||||
|
|
|
@ -19,7 +19,7 @@ import org.springframework.stereotype.Component;
|
|||
import org.springframework.util.Assert;
|
||||
import run.halo.app.content.NotificationReasonConst;
|
||||
import run.halo.app.content.SinglePageService;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.content.comment.CommentService;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.core.extension.content.SinglePage;
|
||||
import run.halo.app.core.extension.content.Snapshot;
|
||||
|
@ -64,6 +64,7 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
private final ExtensionClient client;
|
||||
private final SinglePageService singlePageService;
|
||||
private final CounterService counterService;
|
||||
private final CommentService commentService;
|
||||
|
||||
private final ExternalUrlSupplier externalUrlSupplier;
|
||||
|
||||
|
@ -250,9 +251,7 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
|
|||
listSnapshots(ref).forEach(client::delete);
|
||||
|
||||
// clean up comments
|
||||
client.list(Comment.class, comment -> comment.getSpec().getSubjectRef().equals(ref),
|
||||
null)
|
||||
.forEach(client::delete);
|
||||
commentService.removeBySubject(ref).block();
|
||||
|
||||
// delete counter for single page
|
||||
counterService.deleteByName(
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package run.halo.app.event.post;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* <p>This event will be triggered when the unread reply count of the comment is changed.</p>
|
||||
* <p>It is used to update the unread reply count of the comment,such as when the user reads the
|
||||
* reply(lastReadTime changed in comment), the unread reply count will be updated.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.14.0
|
||||
*/
|
||||
@Getter
|
||||
public class CommentUnreadReplyCountChangedEvent extends ApplicationEvent {
|
||||
private final String commentName;
|
||||
|
||||
public CommentUnreadReplyCountChangedEvent(Object source, String commentName) {
|
||||
super(source);
|
||||
this.commentName = commentName;
|
||||
}
|
||||
}
|
|
@ -21,8 +21,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.util.Predicates;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
@ -34,7 +32,6 @@ import reactor.util.retry.Retry;
|
|||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||
import run.halo.app.extension.index.DefaultExtensionIterator;
|
||||
import run.halo.app.extension.index.ExtensionIterator;
|
||||
import run.halo.app.extension.index.ExtensionPaginatedLister;
|
||||
import run.halo.app.extension.index.IndexedQueryEngine;
|
||||
import run.halo.app.extension.index.IndexerFactory;
|
||||
import run.halo.app.extension.store.ReactiveExtensionStoreClient;
|
||||
|
@ -341,18 +338,13 @@ public class ReactiveExtensionClientImpl implements ReactiveExtensionClient {
|
|||
private ExtensionIterator<Extension> createExtensionIterator(Scheme scheme) {
|
||||
var type = scheme.type();
|
||||
var prefix = ExtensionStoreUtil.buildStoreNamePrefix(scheme);
|
||||
var lister = new ExtensionPaginatedLister() {
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <E extends Extension> Page<E> list(Pageable pageable) {
|
||||
return client.listByNamePrefix(prefix, pageable)
|
||||
.map(page -> page.map(
|
||||
store -> (E) converter.convertFrom(type, store))
|
||||
)
|
||||
.block();
|
||||
}
|
||||
};
|
||||
return new DefaultExtensionIterator<>(lister);
|
||||
return new DefaultExtensionIterator<>(pageable ->
|
||||
client.listByNamePrefix(prefix, pageable)
|
||||
.map(page ->
|
||||
page.map(store -> (Extension) converter.convertFrom(type, store))
|
||||
)
|
||||
.block()
|
||||
);
|
||||
}
|
||||
|
||||
@EventListener(ContextRefreshedEvent.class)
|
||||
|
|
|
@ -17,19 +17,23 @@ import run.halo.app.extension.Extension;
|
|||
*/
|
||||
public class DefaultExtensionIterator<E extends Extension> implements ExtensionIterator<E> {
|
||||
static final int DEFAULT_PAGE_SIZE = 500;
|
||||
private final ExtensionPaginatedLister lister;
|
||||
private final ExtensionPaginatedLister<E> lister;
|
||||
private Pageable currentPageable;
|
||||
private List<E> currentData;
|
||||
private int currentIndex;
|
||||
|
||||
public DefaultExtensionIterator(ExtensionPaginatedLister<E> lister) {
|
||||
this(PageRequest.of(0, DEFAULT_PAGE_SIZE, Sort.by("name")), lister);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new DefaultExtensionIterator with the given lister.
|
||||
*
|
||||
* @param lister the lister to use to load data.
|
||||
*/
|
||||
public DefaultExtensionIterator(ExtensionPaginatedLister lister) {
|
||||
public DefaultExtensionIterator(Pageable initPageable, ExtensionPaginatedLister<E> lister) {
|
||||
this.lister = lister;
|
||||
this.currentPageable = PageRequest.of(0, DEFAULT_PAGE_SIZE, Sort.by("name"));
|
||||
this.currentPageable = initPageable;
|
||||
this.currentData = loadData();
|
||||
}
|
||||
|
||||
|
|
|
@ -10,14 +10,14 @@ import run.halo.app.extension.Extension;
|
|||
* @author guqing
|
||||
* @since 2.12.0
|
||||
*/
|
||||
public interface ExtensionPaginatedLister {
|
||||
@FunctionalInterface
|
||||
public interface ExtensionPaginatedLister<E extends Extension> {
|
||||
|
||||
/**
|
||||
* List extensions with pagination.
|
||||
*
|
||||
* @param pageable pageable
|
||||
* @param <E> extension type
|
||||
* @return page of extensions
|
||||
*/
|
||||
<E extends Extension> Page<E> list(Pageable pageable);
|
||||
Page<E> list(Pageable pageable);
|
||||
}
|
||||
|
|
|
@ -14,10 +14,10 @@ import org.apache.commons.lang3.BooleanUtils;
|
|||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.extension.content.Comment;
|
||||
import run.halo.app.core.extension.content.Reply;
|
||||
import run.halo.app.event.post.CommentUnreadReplyCountChangedEvent;
|
||||
import run.halo.app.event.post.ReplyEvent;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
|
@ -39,11 +39,12 @@ import run.halo.app.extension.router.selector.FieldSelector;
|
|||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ReplyEventReconciler implements Reconciler<ReplyEvent>, SmartLifecycle {
|
||||
public class ReplyEventReconciler
|
||||
implements Reconciler<ReplyEventReconciler.CommentName>, SmartLifecycle {
|
||||
private volatile boolean running = false;
|
||||
|
||||
private final ExtensionClient client;
|
||||
private final RequestQueue<ReplyEvent> replyEventQueue;
|
||||
private final RequestQueue<CommentName> replyEventQueue;
|
||||
private final Controller replyEventController;
|
||||
|
||||
public ReplyEventReconciler(ExtensionClient client) {
|
||||
|
@ -53,9 +54,8 @@ public class ReplyEventReconciler implements Reconciler<ReplyEvent>, SmartLifecy
|
|||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(ReplyEvent request) {
|
||||
Reply requestReply = request.getReply();
|
||||
String commentName = requestReply.getSpec().getCommentName();
|
||||
public Result reconcile(CommentName request) {
|
||||
String commentName = request.name();
|
||||
|
||||
client.fetch(Comment.class, commentName)
|
||||
// if the comment has been deleted, then do nothing.
|
||||
|
@ -110,6 +110,12 @@ public class ReplyEventReconciler implements Reconciler<ReplyEvent>, SmartLifecy
|
|||
return new Result(false, null);
|
||||
}
|
||||
|
||||
public record CommentName(String name) {
|
||||
public static CommentName of(String name) {
|
||||
return new CommentName(name);
|
||||
}
|
||||
}
|
||||
|
||||
static ListOptions listOptionsWithFieldQuery(Query query) {
|
||||
var listOptions = new ListOptions();
|
||||
listOptions.setFieldSelector(FieldSelector.of(query));
|
||||
|
@ -144,13 +150,14 @@ public class ReplyEventReconciler implements Reconciler<ReplyEvent>, SmartLifecy
|
|||
return this.running;
|
||||
}
|
||||
|
||||
@Component
|
||||
public class ReplyEventListener {
|
||||
@EventListener(ReplyEvent.class)
|
||||
public void onReplyEvent(ReplyEvent replyEvent) {
|
||||
var commentName = replyEvent.getReply().getSpec().getCommentName();
|
||||
replyEventQueue.addImmediately(CommentName.of(commentName));
|
||||
}
|
||||
|
||||
@Async
|
||||
@EventListener(ReplyEvent.class)
|
||||
public void onReplyEvent(ReplyEvent replyEvent) {
|
||||
replyEventQueue.addImmediately(replyEvent);
|
||||
}
|
||||
@EventListener(CommentUnreadReplyCountChangedEvent.class)
|
||||
public void onUnreadReplyCountChangedEvent(CommentUnreadReplyCountChangedEvent event) {
|
||||
replyEventQueue.addImmediately(CommentName.of(event.getCommentName()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
package run.halo.app.content.comment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import run.halo.app.core.extension.content.Reply;
|
||||
import run.halo.app.extension.Metadata;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReplyService}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class ReplyServiceTest {
|
||||
private final Instant now = Instant.now();
|
||||
|
||||
@Test
|
||||
void creationTimeAscComparator() {
|
||||
// creation time:
|
||||
// 1. now + 5s, name: 1
|
||||
// 2. now + 3s, name: 2
|
||||
// 3. now + 3s, name: 3
|
||||
// 5. now + 1s, name: 4
|
||||
// 6. now - 1s, name: 5
|
||||
// 7. null, name: 6
|
||||
Reply reply1 = createReply("1", now.plusSeconds(5));
|
||||
Reply reply2 = createReply("2", now.plusSeconds(3));
|
||||
Reply reply3 = createReply("3", now.plusSeconds(3));
|
||||
Reply reply4 = createReply("4", now.plusSeconds(1));
|
||||
Reply reply5 = createReply("5", now.minusSeconds(1));
|
||||
Reply reply6 = createReply("6", null);
|
||||
String result = Stream.of(reply1, reply2, reply3, reply4, reply5, reply6)
|
||||
.sorted(ReplyService.creationTimeAscComparator())
|
||||
.map(reply -> reply.getMetadata().getName())
|
||||
.collect(Collectors.joining(", "));
|
||||
assertThat(result).isEqualTo("5, 4, 2, 3, 1, 6");
|
||||
}
|
||||
|
||||
Reply createReply(String name, Instant creationTime) {
|
||||
Reply reply = new Reply();
|
||||
reply.setMetadata(new Metadata());
|
||||
reply.getMetadata().setName(name);
|
||||
reply.getMetadata().setCreationTimestamp(now);
|
||||
reply.setSpec(new Reply.ReplySpec());
|
||||
reply.getSpec().setCreationTime(creationTime);
|
||||
return reply;
|
||||
}
|
||||
}
|
|
@ -3,14 +3,13 @@ package run.halo.app.core.extension.reconciler;
|
|||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -19,10 +18,14 @@ import org.mockito.ArgumentCaptor;
|
|||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import reactor.core.publisher.Mono;
|
||||
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.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.SchemeManager;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
|
@ -42,6 +45,9 @@ class CommentReconcilerTest {
|
|||
@Mock
|
||||
SchemeManager schemeManager;
|
||||
|
||||
@Mock
|
||||
ReplyService replyService;
|
||||
|
||||
@InjectMocks
|
||||
private CommentReconciler commentReconciler;
|
||||
|
||||
|
@ -57,23 +63,23 @@ class CommentReconcilerTest {
|
|||
finalizers.add(CommentReconciler.FINALIZER_NAME);
|
||||
comment.getMetadata().setFinalizers(finalizers);
|
||||
comment.setSpec(new Comment.CommentSpec());
|
||||
comment.getSpec().setSubjectRef(getRef());
|
||||
comment.getSpec().setLastReadTime(now.plusSeconds(5));
|
||||
comment.setStatus(new Comment.CommentStatus());
|
||||
|
||||
lenient().when(client.list(eq(Reply.class), any(), any()))
|
||||
.thenReturn(replyList());
|
||||
lenient().when(client.fetch(eq(Comment.class), eq("test")))
|
||||
when(client.fetch(eq(Comment.class), eq("test")))
|
||||
.thenReturn(Optional.of(comment));
|
||||
|
||||
lenient().when(client.list(eq(Reply.class), any(), any()))
|
||||
.thenReturn(replyList());
|
||||
when(replyService.removeAllByComment(eq(comment.getMetadata().getName())))
|
||||
.thenReturn(Mono.empty());
|
||||
when(client.listBy(eq(Comment.class), any(ListOptions.class), isA(PageRequest.class)))
|
||||
.thenReturn(ListResult.emptyResult());
|
||||
|
||||
Reconciler.Result reconcile = commentReconciler.reconcile(new Reconciler.Request("test"));
|
||||
assertThat(reconcile.reEnqueue()).isFalse();
|
||||
assertThat(reconcile.retryAfter()).isNull();
|
||||
|
||||
verify(client, times(1)).list(eq(Reply.class), any(), any());
|
||||
verify(client, times(3)).delete(any(Reply.class));
|
||||
verify(replyService).removeAllByComment(eq(comment.getMetadata().getName()));
|
||||
|
||||
ArgumentCaptor<Comment> captor = ArgumentCaptor.forClass(Comment.class);
|
||||
verify(client, times(1)).update(captor.capture());
|
||||
|
@ -90,19 +96,11 @@ class CommentReconcilerTest {
|
|||
comment.setSpec(new Comment.CommentSpec());
|
||||
comment.getSpec().setApprovedTime(Instant.now());
|
||||
comment.getSpec().setCreationTime(null);
|
||||
when(client.fetch(eq(Comment.class), eq("fake-comment")))
|
||||
.thenReturn(Optional.of(comment));
|
||||
|
||||
commentReconciler.compatibleCreationTime("fake-comment");
|
||||
commentReconciler.compatibleCreationTime(comment);
|
||||
|
||||
verify(client, times(1)).fetch(eq(Comment.class), eq("fake-comment"));
|
||||
|
||||
ArgumentCaptor<Comment> captor = ArgumentCaptor.forClass(Comment.class);
|
||||
verify(client, times(1)).update(captor.capture());
|
||||
Comment updated = captor.getValue();
|
||||
assertThat(updated.getSpec().getCreationTime()).isNotNull();
|
||||
assertThat(updated.getSpec().getCreationTime())
|
||||
.isEqualTo(updated.getSpec().getApprovedTime());
|
||||
assertThat(comment.getSpec().getCreationTime())
|
||||
.isEqualTo(comment.getSpec().getApprovedTime());
|
||||
}
|
||||
|
||||
private static Ref getRef() {
|
||||
|
@ -113,22 +111,4 @@ class CommentReconcilerTest {
|
|||
ref.setName("fake-post");
|
||||
return ref;
|
||||
}
|
||||
|
||||
List<Reply> replyList() {
|
||||
Reply replyA = new Reply();
|
||||
replyA.setMetadata(new Metadata());
|
||||
replyA.getMetadata().setName("reply-A");
|
||||
replyA.getMetadata().setCreationTimestamp(now.plusSeconds(6));
|
||||
|
||||
Reply replyB = new Reply();
|
||||
replyB.setMetadata(new Metadata());
|
||||
replyB.getMetadata().setName("reply-B");
|
||||
replyB.getMetadata().setCreationTimestamp(now.plusSeconds(5));
|
||||
|
||||
Reply replyC = new Reply();
|
||||
replyC.setMetadata(new Metadata());
|
||||
replyC.getMetadata().setName("reply-C");
|
||||
replyC.getMetadata().setCreationTimestamp(now.plusSeconds(4));
|
||||
return List.of(replyA, replyB, replyC);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import run.halo.app.extension.Extension;
|
|||
class DefaultExtensionIteratorTest {
|
||||
|
||||
@Mock
|
||||
private ExtensionPaginatedLister lister;
|
||||
private ExtensionPaginatedLister<Extension> lister;
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
|
|
Loading…
Reference in New Issue