refactor: optimize comment and reply logic to reduce duplicate code (#6542)

#### What type of PR is this?
/kind improvement
/area core
/milestone 2.19.x

#### What this PR does / why we need it:
优化评论和回复的逻辑并减少重复代码

#### Does this PR introduce a user-facing change?
```release-note
None
```
pull/6548/head
guqing 2024-08-29 12:07:25 +08:00 committed by GitHub
parent 0a13981c0d
commit ad267ebed7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 130 additions and 151 deletions

View File

@ -0,0 +1,77 @@
package run.halo.app.content.comment;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.User;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.content.Reply;
import run.halo.app.core.extension.service.RoleService;
import run.halo.app.core.extension.service.UserService;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.metrics.CounterService;
import run.halo.app.metrics.MeterUtils;
import run.halo.app.security.authorization.AuthorityUtils;
@RequiredArgsConstructor
public abstract class AbstractCommentService {
protected final RoleService roleService;
protected final ReactiveExtensionClient client;
protected final UserService userService;
protected final CounterService counterService;
protected Mono<User> fetchCurrentUser() {
return ReactiveSecurityContextHolder.getContext()
.map(securityContext -> securityContext.getAuthentication().getName())
.flatMap(username -> client.fetch(User.class, username));
}
Mono<Boolean> hasCommentManagePermission() {
return ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
var authentication = securityContext.getAuthentication();
var roles = AuthorityUtils.authoritiesToRoles(authentication.getAuthorities());
return roleService.contains(roles,
Set.of(AuthorityUtils.COMMENT_MANAGEMENT_ROLE_NAME));
});
}
protected Comment.CommentOwner toCommentOwner(User user) {
Comment.CommentOwner owner = new Comment.CommentOwner();
owner.setKind(User.KIND);
owner.setName(user.getMetadata().getName());
owner.setDisplayName(user.getSpec().getDisplayName());
return owner;
}
protected Mono<OwnerInfo> getOwnerInfo(Comment.CommentOwner owner) {
if (User.KIND.equals(owner.getKind())) {
return userService.getUserOrGhost(owner.getName())
.map(OwnerInfo::from);
}
if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {
return Mono.just(OwnerInfo.from(owner));
}
return Mono.error(new IllegalStateException("Unsupported owner kind: " + owner.getKind()));
}
protected Mono<CommentStats> fetchCommentStats(String commentName) {
return this.fetchStats(MeterUtils.nameOf(Comment.class, commentName));
}
protected Mono<CommentStats> fetchReplyStats(String replyName) {
return this.fetchStats(MeterUtils.nameOf(Reply.class, replyName));
}
private Mono<CommentStats> fetchStats(String meterName) {
Assert.notNull(meterName, "The reply must not be null.");
return counterService.getByName(meterName)
.map(counter -> CommentStats.builder()
.upvote(counter.getUpvote())
.build()
)
.switchIfEmpty(Mono.fromSupplier(CommentStats::empty));
}
}

View File

@ -6,19 +6,16 @@ import static run.halo.app.extension.index.query.QueryFactory.isNull;
import java.time.Duration;
import java.time.Instant;
import java.util.Set;
import java.util.function.Function;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.dao.OptimisticLockingFailureException;
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;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import run.halo.app.core.extension.User;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.service.RoleService;
import run.halo.app.core.extension.service.UserService;
@ -33,9 +30,7 @@ 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;
import run.halo.app.metrics.MeterUtils;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
import run.halo.app.security.authorization.AuthorityUtils;
/**
* Comment service implementation.
@ -44,28 +39,17 @@ import run.halo.app.security.authorization.AuthorityUtils;
* @since 2.0.0
*/
@Component
public class CommentServiceImpl implements CommentService {
public class CommentServiceImpl extends AbstractCommentService implements CommentService {
private final ReactiveExtensionClient client;
private final UserService userService;
private final RoleService roleService;
private final ExtensionGetter extensionGetter;
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
private final CounterService counterService;
public CommentServiceImpl(ReactiveExtensionClient client,
UserService userService,
SystemConfigurableEnvironmentFetcher environmentFetcher,
CounterService counterService, RoleService roleService,
ExtensionGetter extensionGetter
) {
this.client = client;
this.userService = userService;
this.environmentFetcher = environmentFetcher;
this.counterService = counterService;
this.roleService = roleService;
public CommentServiceImpl(RoleService roleService, ReactiveExtensionClient client,
UserService userService, CounterService counterService, ExtensionGetter extensionGetter,
SystemConfigurableEnvironmentFetcher environmentFetcher) {
super(roleService, client, userService, counterService);
this.extensionGetter = extensionGetter;
this.environmentFetcher = environmentFetcher;
}
@Override
@ -116,38 +100,36 @@ public class CommentServiceImpl implements CommentService {
}
comment.getSpec().setHidden(false);
// return if the comment owner is not null
if (comment.getSpec().getOwner() != null) {
return Mono.just(comment);
}
// populate owner from current user
return fetchCurrentUser()
.flatMap(currentUser -> ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
var authentication = securityContext.getAuthentication();
var roles = AuthorityUtils.authoritiesToRoles(
authentication.getAuthorities());
return roleService.contains(roles,
Set.of(AuthorityUtils.COMMENT_MANAGEMENT_ROLE_NAME))
.doOnNext(result -> {
if (result) {
comment.getSpec().setApproved(true);
comment.getSpec().setApprovedTime(Instant.now());
}
})
.thenReturn(toCommentOwner(currentUser));
}))
.map(owner -> {
comment.getSpec().setOwner(owner);
return comment;
})
.switchIfEmpty(
Mono.error(new IllegalStateException("The owner must not be null.")));
return Mono.just(comment);
})
.flatMap(populatedComment -> Mono.when(populateOwner(populatedComment),
populateApproveState(populatedComment))
.thenReturn(populatedComment)
)
.flatMap(client::create);
}
private Mono<Void> populateApproveState(Comment comment) {
return hasCommentManagePermission()
.filter(Boolean::booleanValue)
.doOnNext(hasPermission -> {
comment.getSpec().setApproved(true);
comment.getSpec().setApprovedTime(Instant.now());
})
.then();
}
Mono<Void> populateOwner(Comment comment) {
if (comment.getSpec().getOwner() != null) {
return Mono.empty();
}
return fetchCurrentUser()
.switchIfEmpty(Mono.error(new IllegalStateException("The owner must not be null.")))
.map(this::toCommentOwner)
.doOnNext(owner -> comment.getSpec().setOwner(owner))
.then();
}
@Override
public Mono<Void> removeBySubject(@NonNull Ref subjectRef) {
Assert.notNull(subjectRef, "The subjectRef must not be null.");
@ -197,55 +179,19 @@ public class CommentServiceImpl implements CommentService {
return false;
}
private Comment.CommentOwner toCommentOwner(User user) {
Comment.CommentOwner owner = new Comment.CommentOwner();
owner.setKind(User.KIND);
owner.setName(user.getMetadata().getName());
owner.setDisplayName(user.getSpec().getDisplayName());
return owner;
}
private Mono<User> fetchCurrentUser() {
return ReactiveSecurityContextHolder.getContext()
.map(securityContext -> securityContext.getAuthentication().getName())
.flatMap(username -> client.fetch(User.class, username));
}
private Mono<ListedComment> toListedComment(Comment comment) {
var builder = ListedComment.builder().comment(comment);
// not empty
var ownerInfoMono = getCommentOwnerInfo(comment.getSpec().getOwner())
var ownerInfoMono = getOwnerInfo(comment.getSpec().getOwner())
.doOnNext(builder::owner);
var subjectMono = getCommentSubject(comment.getSpec().getSubjectRef())
.doOnNext(builder::subject);
var statsMono = fetchStats(comment.getMetadata().getName())
var statsMono = fetchCommentStats(comment.getMetadata().getName())
.doOnNext(builder::stats);
return Mono.when(ownerInfoMono, subjectMono, statsMono)
.then(Mono.fromSupplier(builder::build));
}
Mono<CommentStats> fetchStats(String commentName) {
Assert.notNull(commentName, "The commentName must not be null.");
return counterService.getByName(MeterUtils.nameOf(Comment.class, commentName))
.map(counter -> CommentStats.builder()
.upvote(counter.getUpvote())
.build()
)
.defaultIfEmpty(CommentStats.empty());
}
private Mono<OwnerInfo> getCommentOwnerInfo(Comment.CommentOwner owner) {
if (User.KIND.equals(owner.getKind())) {
return userService.getUserOrGhost(owner.getName())
.map(OwnerInfo::from);
}
if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {
return Mono.just(OwnerInfo.from(owner));
}
throw new IllegalStateException(
"Unsupported owner kind: " + owner.getKind());
}
@SuppressWarnings("unchecked")
Mono<Extension> getCommentSubject(Ref ref) {
return extensionGetter.getExtensions(CommentSubject.class)

View File

@ -3,29 +3,22 @@ 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.security.authorization.AuthorityUtils.COMMENT_MANAGEMENT_ROLE_NAME;
import static run.halo.app.security.authorization.AuthorityUtils.authoritiesToRoles;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Set;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.data.domain.Sort;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import run.halo.app.core.extension.User;
import run.halo.app.core.extension.content.Comment;
import run.halo.app.core.extension.content.Reply;
import run.halo.app.core.extension.service.RoleService;
@ -37,7 +30,6 @@ 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;
/**
* A default implementation of {@link ReplyService}.
@ -46,13 +38,12 @@ import run.halo.app.metrics.MeterUtils;
* @since 2.0.0
*/
@Service
@RequiredArgsConstructor
public class ReplyServiceImpl implements ReplyService {
public class ReplyServiceImpl extends AbstractCommentService implements ReplyService {
private final ReactiveExtensionClient client;
private final UserService userService;
private final RoleService roleService;
private final CounterService counterService;
public ReplyServiceImpl(RoleService roleService, ReactiveExtensionClient client,
UserService userService, CounterService counterService) {
super(roleService, client, userService, counterService);
}
@Override
public Mono<Reply> create(String commentName, Reply reply) {
@ -74,6 +65,12 @@ public class ReplyServiceImpl implements ReplyService {
}
private Mono<Comment> approveComment(Comment comment) {
return hasCommentManagePermission()
.filter(Boolean::booleanValue)
.flatMap(hasPermission -> doApproveComment(comment));
}
private Mono<Comment> doApproveComment(Comment comment) {
UnaryOperator<Comment> updateFunc = commentToUpdate -> {
commentToUpdate.getSpec().setApproved(true);
commentToUpdate.getSpec().setApprovedTime(Instant.now());
@ -85,6 +82,12 @@ public class ReplyServiceImpl implements ReplyService {
}
private Mono<Void> approveReply(String replyName) {
return hasCommentManagePermission()
.filter(Boolean::booleanValue)
.flatMap(hasPermission -> doApproveReply(replyName));
}
private Mono<Void> doApproveReply(String replyName) {
return Mono.defer(() -> client.fetch(Reply.class, replyName)
.flatMap(reply -> {
reply.getSpec().setApproved(true);
@ -144,15 +147,6 @@ public class ReplyServiceImpl implements ReplyService {
return Mono.when(steps).thenReturn(reply);
}
Mono<Boolean> hasCommentManagePermission() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.flatMap(authentication -> {
var roles = authoritiesToRoles(authentication.getAuthorities());
return roleService.contains(roles, Set.of(COMMENT_MANAGEMENT_ROLE_NAME));
});
}
@Override
public Mono<ListResult<ListedReply>> list(ReplyQuery query) {
return client.listBy(Reply.class, query.toListOptions(), query.toPageRequest())
@ -209,52 +203,14 @@ public class ReplyServiceImpl implements ReplyService {
private Mono<ListedReply> toListedReply(Reply reply) {
ListedReply.ListedReplyBuilder builder = ListedReply.builder()
.reply(reply);
return getOwnerInfo(reply)
return getOwnerInfo(reply.getSpec().getOwner())
.map(ownerInfo -> {
builder.owner(ownerInfo);
return builder;
})
.map(ListedReply.ListedReplyBuilder::build)
.flatMap(listedReply -> fetchStats(reply)
.flatMap(listedReply -> fetchReplyStats(reply.getMetadata().getName())
.doOnNext(listedReply::setStats)
.thenReturn(listedReply));
}
Mono<CommentStats> fetchStats(Reply reply) {
Assert.notNull(reply, "The reply must not be null.");
String name = reply.getMetadata().getName();
return counterService.getByName(MeterUtils.nameOf(Reply.class, name))
.map(counter -> CommentStats.builder()
.upvote(counter.getUpvote())
.build()
)
.defaultIfEmpty(CommentStats.empty());
}
private Mono<OwnerInfo> getOwnerInfo(Reply reply) {
Comment.CommentOwner owner = reply.getSpec().getOwner();
if (User.KIND.equals(owner.getKind())) {
return userService.getUserOrGhost(owner.getName())
.map(OwnerInfo::from);
}
if (Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind())) {
return Mono.just(OwnerInfo.from(owner));
}
throw new IllegalStateException(
"Unsupported owner kind: " + owner.getKind());
}
private Comment.CommentOwner toCommentOwner(User user) {
Comment.CommentOwner owner = new Comment.CommentOwner();
owner.setKind(User.KIND);
owner.setName(user.getMetadata().getName());
owner.setDisplayName(user.getSpec().getDisplayName());
return owner;
}
private Mono<User> fetchCurrentUser() {
return ReactiveSecurityContextHolder.getContext()
.map(securityContext -> securityContext.getAuthentication().getName())
.flatMap(username -> client.fetch(User.class, username));
}
}