mirror of https://github.com/halo-dev/halo
feat: add upvote and downvote tracker (#2566)
#### What type of PR is this? /kind feature /area core /milestone 2.0 /kind api-change #### What this PR does / why we need it: 新增文章和评论等资源的点赞和踩 API #### Which issue(s) this PR fixes: Fixes #2565 #### Special notes for your reviewer: how to test it? 1. 创建并发布一篇文章 替换下面的 `your-post-name` 然后执行它 ```curl curl --location --request POST 'http://localhost:8090/apis/api.halo.run/v1alpha1/trackers/upvote' \ --header 'Content-Type: application/json' \ --data-raw '{ "group": "content.halo.run", "plural": "posts", "name": your-post-name } ``` 2. 请求成功并得到 response 为点赞数 3. 踩的 API 为 `http://localhost:8090/apis/api.halo.run/v1alpha1/trackers/downvote` 请求体与步骤1相同的测试方法 4. 重启 Halo 后 Counter 数据应该依然存在并且正确 /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 新增文章和评论等资源的点赞和踩 API ```pull/2582/head
parent
83b40b6dad
commit
08fe1858cf
|
@ -24,6 +24,8 @@ public class Counter extends AbstractExtension {
|
|||
|
||||
private Integer upvote;
|
||||
|
||||
private Integer downvote;
|
||||
|
||||
private Integer totalComment;
|
||||
|
||||
private Integer approvedComment;
|
||||
|
@ -41,6 +43,8 @@ public class Counter extends AbstractExtension {
|
|||
this.visit = (int) meterCounter.count();
|
||||
} else if (MeterUtils.isUpvoteCounter(meterCounter)) {
|
||||
this.upvote = (int) meterCounter.count();
|
||||
} else if (MeterUtils.isDownvoteCounter(meterCounter)) {
|
||||
this.downvote = (int) meterCounter.count();
|
||||
} else if (MeterUtils.isTotalCommentCounter(meterCounter)) {
|
||||
this.totalComment = (int) meterCounter.count();
|
||||
} else if (MeterUtils.isApprovedCommentCounter(meterCounter)) {
|
||||
|
@ -53,6 +57,7 @@ public class Counter extends AbstractExtension {
|
|||
private void populateDefaultValue() {
|
||||
this.visit = 0;
|
||||
this.upvote = 0;
|
||||
this.downvote = 0;
|
||||
this.totalComment = 0;
|
||||
this.approvedComment = 0;
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ public class TrackerEndpoint implements CustomEndpoint {
|
|||
public RouterFunction<ServerResponse> endpoint() {
|
||||
final var tag = "api.halo.run/v1alpha1/Tracker";
|
||||
return SpringdocRouteBuilder.route()
|
||||
.POST("trackers/counter", this::increase,
|
||||
.POST("trackers/counter", this::increaseVisit,
|
||||
builder -> builder.operationId("count")
|
||||
.description("Count an extension resource visits.")
|
||||
.tag(tag)
|
||||
|
@ -55,12 +55,40 @@ public class TrackerEndpoint implements CustomEndpoint {
|
|||
.implementation(CounterRequest.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Double.class))
|
||||
.implementation(Integer.class))
|
||||
)
|
||||
.POST("trackers/upvote", this::upvote,
|
||||
builder -> builder.operationId("upvote")
|
||||
.description("Upvote an extension resource.")
|
||||
.tag(tag)
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(Builder.schemaBuilder()
|
||||
.implementation(VoteRequest.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Integer.class))
|
||||
)
|
||||
.POST("trackers/downvote", this::downvote,
|
||||
builder -> builder.operationId("downvote")
|
||||
.description("Downvote an extension resource.")
|
||||
.tag(tag)
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder()
|
||||
.mediaType(MediaType.APPLICATION_JSON_VALUE)
|
||||
.schema(Builder.schemaBuilder()
|
||||
.implementation(VoteRequest.class))
|
||||
))
|
||||
.response(responseBuilder()
|
||||
.implementation(Integer.class))
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> increase(ServerRequest request) {
|
||||
private Mono<ServerResponse> increaseVisit(ServerRequest request) {
|
||||
return request.bodyToMono(CounterRequest.class)
|
||||
.switchIfEmpty(
|
||||
Mono.error(new IllegalArgumentException("Counter request body must not be empty")))
|
||||
|
@ -78,6 +106,41 @@ public class TrackerEndpoint implements CustomEndpoint {
|
|||
.flatMap(count -> ServerResponse.ok().bodyValue(count));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> upvote(ServerRequest request) {
|
||||
return request.bodyToMono(VoteRequest.class)
|
||||
.switchIfEmpty(
|
||||
Mono.error(new IllegalArgumentException("Upvote request body must not be empty")))
|
||||
.map(voteRequest -> {
|
||||
String counterName =
|
||||
MeterUtils.nameOf(voteRequest.group(), voteRequest.plural(),
|
||||
voteRequest.name());
|
||||
|
||||
Counter counter = MeterUtils.upvoteCounter(meterRegistry, counterName);
|
||||
counter.increment();
|
||||
return (int) counter.count();
|
||||
})
|
||||
.flatMap(count -> ServerResponse.ok().bodyValue(count));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> downvote(ServerRequest request) {
|
||||
return request.bodyToMono(VoteRequest.class)
|
||||
.switchIfEmpty(
|
||||
Mono.error(new IllegalArgumentException("Downvote request body must not be empty")))
|
||||
.map(voteRequest -> {
|
||||
String counterName =
|
||||
MeterUtils.nameOf(voteRequest.group(), voteRequest.plural(),
|
||||
voteRequest.name());
|
||||
|
||||
Counter counter = MeterUtils.downvoteCounter(meterRegistry, counterName);
|
||||
counter.increment();
|
||||
return (int) counter.count();
|
||||
})
|
||||
.flatMap(count -> ServerResponse.ok().bodyValue(count));
|
||||
}
|
||||
|
||||
public record VoteRequest(String group, String plural, String name) {
|
||||
}
|
||||
|
||||
public record CounterRequest(String group, String plural, String name, String hostname,
|
||||
String screen, String language, String referrer) {
|
||||
/**
|
||||
|
|
|
@ -59,6 +59,11 @@ public class CounterMeterHandler implements DisposableBean {
|
|||
MeterUtils.upvoteCounter(meterRegistry, name);
|
||||
upvoteCounter.increment(nullSafe(counter.getUpvote()));
|
||||
|
||||
// downvote counter
|
||||
io.micrometer.core.instrument.Counter downvoteCounter =
|
||||
MeterUtils.downvoteCounter(meterRegistry, name);
|
||||
downvoteCounter.increment(nullSafe(counter.getDownvote()));
|
||||
|
||||
// total comment counter
|
||||
io.micrometer.core.instrument.Counter totalCommentCounter =
|
||||
MeterUtils.totalCommentCounter(meterRegistry, name);
|
||||
|
|
|
@ -20,6 +20,7 @@ public class MeterUtils {
|
|||
public static final String SCENE = "scene";
|
||||
public static final String VISIT_SCENE = "visit";
|
||||
public static final String UPVOTE_SCENE = "upvote";
|
||||
public static final String DOWNVOTE_SCENE = "downvote";
|
||||
public static final String TOTAL_COMMENT_SCENE = "total_comment";
|
||||
public static final String APPROVED_COMMENT_SCENE = "approved_comment";
|
||||
|
||||
|
@ -51,6 +52,10 @@ public class MeterUtils {
|
|||
return counter(registry, name, Tag.of(SCENE, UPVOTE_SCENE));
|
||||
}
|
||||
|
||||
public static Counter downvoteCounter(MeterRegistry registry, String name) {
|
||||
return counter(registry, name, Tag.of(SCENE, DOWNVOTE_SCENE));
|
||||
}
|
||||
|
||||
public static Counter totalCommentCounter(MeterRegistry registry, String name) {
|
||||
return counter(registry, name, Tag.of(SCENE, TOTAL_COMMENT_SCENE));
|
||||
}
|
||||
|
@ -75,6 +80,14 @@ public class MeterUtils {
|
|||
return UPVOTE_SCENE.equals(sceneValue);
|
||||
}
|
||||
|
||||
public static boolean isDownvoteCounter(Counter counter) {
|
||||
String sceneValue = counter.getId().getTag(SCENE);
|
||||
if (StringUtils.isBlank(sceneValue)) {
|
||||
return false;
|
||||
}
|
||||
return DOWNVOTE_SCENE.equals(sceneValue);
|
||||
}
|
||||
|
||||
public static boolean isTotalCommentCounter(Counter counter) {
|
||||
String sceneValue = counter.getId().getTag(SCENE);
|
||||
if (StringUtils.isBlank(sceneValue)) {
|
||||
|
|
|
@ -125,7 +125,7 @@ public class RbacRequestEvaluation {
|
|||
if (Objects.equals(ruleURL, requestedURL)) {
|
||||
return true;
|
||||
}
|
||||
if (StringUtils.startsWith(ruleURL, WildCard.NonResourceAll)
|
||||
if (StringUtils.endsWith(ruleURL, WildCard.NonResourceAll)
|
||||
&& StringUtils.startsWith(requestedURL,
|
||||
StringUtils.stripEnd(ruleURL, WildCard.NonResourceAll))) {
|
||||
return true;
|
||||
|
|
|
@ -12,5 +12,5 @@ rules:
|
|||
- apiGroups: [ "api.halo.run" ]
|
||||
resources: [ "*" ]
|
||||
verbs: [ "*" ]
|
||||
- nonResourceURLs: [ "/apis/api.halo.run/v1alpha1/trackers/counter" ]
|
||||
- nonResourceURLs: [ "/apis/api.halo.run/v1alpha1/trackers/*" ]
|
||||
verbs: [ "create" ]
|
||||
|
|
|
@ -99,6 +99,15 @@ class MeterUtilsTest {
|
|||
assertThat(MeterUtils.isVisitCounter(upvoteCounter)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isDownvoteCounter() {
|
||||
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||
Counter downvoteCounter =
|
||||
MeterUtils.downvoteCounter(meterRegistry, "posts.content.halo.run/fake-post");
|
||||
assertThat(MeterUtils.isDownvoteCounter(downvoteCounter)).isTrue();
|
||||
assertThat(MeterUtils.isVisitCounter(downvoteCounter)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isTotalCommentCounter() {
|
||||
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||
|
|
Loading…
Reference in New Issue