mirror of https://github.com/halo-dev/halo
feat: support sorting posts by views and comment count (#5614)
#### What type of PR is this? /kind feature #### What this PR does / why we need it: 文章支持根据访问量和评论量排序 #### Which issue(s) this PR fixes: Fixes #3216 #### Does this PR introduce a user-facing change? ```release-note 文章支持根据访问量和评论量排序 ```pull/5696/head
parent
3ef1461c32
commit
817963c15e
|
@ -43,6 +43,8 @@ public class Post extends AbstractExtension {
|
|||
"content.halo.run/last-released-snapshot";
|
||||
public static final String LAST_ASSOCIATED_TAGS_ANNO = "content.halo.run/last-associated-tags";
|
||||
|
||||
public static final String STATS_ANNO = "content.halo.run/stats";
|
||||
|
||||
public static final String DELETED_LABEL = "content.halo.run/deleted";
|
||||
public static final String PUBLISHED_LABEL = "content.halo.run/published";
|
||||
public static final String OWNER_LABEL = "content.halo.run/owner";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Value;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Stats value object.
|
||||
|
@ -9,17 +9,27 @@ import lombok.Value;
|
|||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Value
|
||||
@Builder
|
||||
@Data
|
||||
public class Stats {
|
||||
|
||||
Integer visit;
|
||||
private Integer visit;
|
||||
|
||||
Integer upvote;
|
||||
private Integer upvote;
|
||||
|
||||
Integer totalComment;
|
||||
private Integer totalComment;
|
||||
|
||||
Integer approvedComment;
|
||||
private Integer approvedComment;
|
||||
|
||||
public Stats() {
|
||||
}
|
||||
|
||||
@Builder
|
||||
public Stats(Integer visit, Integer upvote, Integer totalComment, Integer approvedComment) {
|
||||
this.visit = visit;
|
||||
this.upvote = upvote;
|
||||
this.totalComment = totalComment;
|
||||
this.approvedComment = approvedComment;
|
||||
}
|
||||
|
||||
public static Stats empty() {
|
||||
return Stats.builder()
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import static run.halo.app.extension.index.query.QueryFactory.startsWith;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.event.post.PostStatsChangedEvent;
|
||||
import run.halo.app.extension.DefaultExtensionMatcher;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
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.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PostCounterReconciler implements Reconciler<Reconciler.Request> {
|
||||
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final ExtensionClient client;
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
if (!isSameAsPost(request.name())) {
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
client.fetch(Counter.class, request.name()).ifPresent(counter -> {
|
||||
eventPublisher.publishEvent(new PostStatsChangedEvent(this, counter));
|
||||
});
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
var extension = new Counter();
|
||||
return builder
|
||||
.extension(extension)
|
||||
.onAddMatcher(DefaultExtensionMatcher.builder(client, extension.groupVersionKind())
|
||||
.fieldSelector(FieldSelector.of(
|
||||
startsWith("metadata.name", MeterUtils.nameOf(Post.class, "")))
|
||||
)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
static boolean isSameAsPost(String name) {
|
||||
return name.startsWith(MeterUtils.nameOf(Post.class, ""));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package run.halo.app.event.post;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.metrics.MeterUtils;
|
||||
|
||||
@Getter
|
||||
public class PostStatsChangedEvent extends ApplicationEvent {
|
||||
private final Counter counter;
|
||||
|
||||
public PostStatsChangedEvent(Object source, Counter counter) {
|
||||
super(source);
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
public String getPostName() {
|
||||
var counterName = counter.getMetadata().getName();
|
||||
return StringUtils.removeStart(counterName, MeterUtils.nameOf(Post.class, ""));
|
||||
}
|
||||
}
|
|
@ -9,11 +9,13 @@ import static run.halo.app.extension.index.IndexAttributeFactory.simpleAttribute
|
|||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import java.util.Set;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.boot.context.event.ApplicationContextInitializedEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.content.Stats;
|
||||
import run.halo.app.core.extension.AnnotationSetting;
|
||||
import run.halo.app.core.extension.AuthProvider;
|
||||
import run.halo.app.core.extension.Counter;
|
||||
|
@ -182,6 +184,30 @@ public class SchemeInitializer implements ApplicationListener<ApplicationContext
|
|||
// do not care about the false case so return null to avoid indexing
|
||||
return null;
|
||||
})));
|
||||
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("stats.visit")
|
||||
.setIndexFunc(simpleAttribute(Post.class, post -> {
|
||||
var annotations = MetadataUtil.nullSafeAnnotations(post);
|
||||
var statsStr = annotations.get(Post.STATS_ANNO);
|
||||
if (StringUtils.isBlank(statsStr)) {
|
||||
return "0";
|
||||
}
|
||||
var stats = JsonUtils.jsonToObject(statsStr, Stats.class);
|
||||
return ObjectUtils.defaultIfNull(stats.getVisit(), 0).toString();
|
||||
})));
|
||||
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("stats.totalComment")
|
||||
.setIndexFunc(simpleAttribute(Post.class, post -> {
|
||||
var annotations = MetadataUtil.nullSafeAnnotations(post);
|
||||
var statsStr = annotations.get(Post.STATS_ANNO);
|
||||
if (StringUtils.isBlank(statsStr)) {
|
||||
return "0";
|
||||
}
|
||||
var stats = JsonUtils.jsonToObject(statsStr, Stats.class);
|
||||
return ObjectUtils.defaultIfNull(stats.getTotalComment(), 0).toString();
|
||||
})));
|
||||
});
|
||||
schemeManager.register(Category.class, indexSpecs -> {
|
||||
indexSpecs.add(new IndexSpec()
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
package run.halo.app.metrics;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.content.Stats;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.event.post.PostStatsChangedEvent;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.DefaultController;
|
||||
import run.halo.app.extension.controller.DefaultQueue;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.extension.controller.RequestQueue;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
@Component
|
||||
public class PostStatsUpdater implements Reconciler<PostStatsUpdater.StatsRequest>,
|
||||
SmartLifecycle {
|
||||
|
||||
private volatile boolean running = false;
|
||||
|
||||
private final ExtensionClient client;
|
||||
private final RequestQueue<StatsRequest> queue;
|
||||
private final Controller controller;
|
||||
|
||||
public PostStatsUpdater(ExtensionClient client) {
|
||||
this.client = client;
|
||||
queue = new DefaultQueue<>(Instant::now);
|
||||
controller = this.setupWith(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result reconcile(StatsRequest request) {
|
||||
client.fetch(Post.class, request.postName()).ifPresent(post -> {
|
||||
var annotations = MetadataUtil.nullSafeAnnotations(post);
|
||||
annotations.put(Post.STATS_ANNO, JsonUtils.objectToJson(request.stats()));
|
||||
client.update(post);
|
||||
});
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return new DefaultController<>(
|
||||
this.getClass().getName(),
|
||||
this,
|
||||
queue,
|
||||
null,
|
||||
Duration.ofMillis(100),
|
||||
Duration.ofMinutes(10));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.controller.start();
|
||||
this.running = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
this.running = false;
|
||||
this.controller.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return this.running;
|
||||
}
|
||||
|
||||
@EventListener(PostStatsChangedEvent.class)
|
||||
public void onReplyEvent(PostStatsChangedEvent event) {
|
||||
var counter = event.getCounter();
|
||||
var stats = Stats.builder()
|
||||
.visit(counter.getVisit())
|
||||
.upvote(counter.getUpvote())
|
||||
.totalComment(counter.getTotalComment())
|
||||
.approvedComment(counter.getApprovedComment())
|
||||
.build();
|
||||
var request = new StatsRequest(event.getPostName(), stats);
|
||||
queue.addImmediately(request);
|
||||
}
|
||||
|
||||
public record StatsRequest(String postName, Stats stats) {
|
||||
}
|
||||
}
|
|
@ -420,6 +420,14 @@ watch(selectedPostNames, (newValue) => {
|
|||
label: t('core.post.filters.sort.items.create_time_asc'),
|
||||
value: 'metadata.creationTimestamp,asc',
|
||||
},
|
||||
{
|
||||
label: t('core.post.filters.sort.items.visit_desc'),
|
||||
value: 'stats.visit,desc',
|
||||
},
|
||||
{
|
||||
label: t('core.post.filters.sort.items.comment_desc'),
|
||||
value: 'stats.totalComment,desc',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div class="flex flex-row gap-2">
|
||||
|
|
|
@ -217,6 +217,8 @@ core:
|
|||
publish_time_asc: Earliest Published
|
||||
create_time_desc: Latest Created
|
||||
create_time_asc: Earliest Created
|
||||
visit_desc: Most Visits
|
||||
comment_desc: Most Comments
|
||||
list:
|
||||
fields:
|
||||
categories: "Categories:"
|
||||
|
|
|
@ -179,6 +179,8 @@ core:
|
|||
publish_time_asc: Publicado más antiguo
|
||||
create_time_desc: Creado más reciente
|
||||
create_time_asc: Creado más antiguo
|
||||
visit_desc: Máximo de visitas
|
||||
comment_desc: Número máximo de comentarios
|
||||
list:
|
||||
fields:
|
||||
categories: "Categorías:"
|
||||
|
|
|
@ -209,6 +209,8 @@ core:
|
|||
publish_time_asc: 较早发布
|
||||
create_time_desc: 较近创建
|
||||
create_time_asc: 较早创建
|
||||
visit_desc: 最多访问量
|
||||
comment_desc: 最多评论量
|
||||
list:
|
||||
fields:
|
||||
categories: 分类:
|
||||
|
|
|
@ -209,6 +209,8 @@ core:
|
|||
publish_time_asc: 較早發布
|
||||
create_time_desc: 較近創建
|
||||
create_time_asc: 較早創建
|
||||
visit_desc: 最多訪問量
|
||||
comment_desc: 最多評論量
|
||||
list:
|
||||
fields:
|
||||
categories: 分類:
|
||||
|
|
Loading…
Reference in New Issue