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 upvote;
|
||||||
|
|
||||||
|
private Integer downvote;
|
||||||
|
|
||||||
private Integer totalComment;
|
private Integer totalComment;
|
||||||
|
|
||||||
private Integer approvedComment;
|
private Integer approvedComment;
|
||||||
|
@ -41,6 +43,8 @@ public class Counter extends AbstractExtension {
|
||||||
this.visit = (int) meterCounter.count();
|
this.visit = (int) meterCounter.count();
|
||||||
} else if (MeterUtils.isUpvoteCounter(meterCounter)) {
|
} else if (MeterUtils.isUpvoteCounter(meterCounter)) {
|
||||||
this.upvote = (int) meterCounter.count();
|
this.upvote = (int) meterCounter.count();
|
||||||
|
} else if (MeterUtils.isDownvoteCounter(meterCounter)) {
|
||||||
|
this.downvote = (int) meterCounter.count();
|
||||||
} else if (MeterUtils.isTotalCommentCounter(meterCounter)) {
|
} else if (MeterUtils.isTotalCommentCounter(meterCounter)) {
|
||||||
this.totalComment = (int) meterCounter.count();
|
this.totalComment = (int) meterCounter.count();
|
||||||
} else if (MeterUtils.isApprovedCommentCounter(meterCounter)) {
|
} else if (MeterUtils.isApprovedCommentCounter(meterCounter)) {
|
||||||
|
@ -53,6 +57,7 @@ public class Counter extends AbstractExtension {
|
||||||
private void populateDefaultValue() {
|
private void populateDefaultValue() {
|
||||||
this.visit = 0;
|
this.visit = 0;
|
||||||
this.upvote = 0;
|
this.upvote = 0;
|
||||||
|
this.downvote = 0;
|
||||||
this.totalComment = 0;
|
this.totalComment = 0;
|
||||||
this.approvedComment = 0;
|
this.approvedComment = 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ public class TrackerEndpoint implements CustomEndpoint {
|
||||||
public RouterFunction<ServerResponse> endpoint() {
|
public RouterFunction<ServerResponse> endpoint() {
|
||||||
final var tag = "api.halo.run/v1alpha1/Tracker";
|
final var tag = "api.halo.run/v1alpha1/Tracker";
|
||||||
return SpringdocRouteBuilder.route()
|
return SpringdocRouteBuilder.route()
|
||||||
.POST("trackers/counter", this::increase,
|
.POST("trackers/counter", this::increaseVisit,
|
||||||
builder -> builder.operationId("count")
|
builder -> builder.operationId("count")
|
||||||
.description("Count an extension resource visits.")
|
.description("Count an extension resource visits.")
|
||||||
.tag(tag)
|
.tag(tag)
|
||||||
|
@ -55,12 +55,40 @@ public class TrackerEndpoint implements CustomEndpoint {
|
||||||
.implementation(CounterRequest.class))
|
.implementation(CounterRequest.class))
|
||||||
))
|
))
|
||||||
.response(responseBuilder()
|
.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();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<ServerResponse> increase(ServerRequest request) {
|
private Mono<ServerResponse> increaseVisit(ServerRequest request) {
|
||||||
return request.bodyToMono(CounterRequest.class)
|
return request.bodyToMono(CounterRequest.class)
|
||||||
.switchIfEmpty(
|
.switchIfEmpty(
|
||||||
Mono.error(new IllegalArgumentException("Counter request body must not be empty")))
|
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));
|
.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,
|
public record CounterRequest(String group, String plural, String name, String hostname,
|
||||||
String screen, String language, String referrer) {
|
String screen, String language, String referrer) {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -59,6 +59,11 @@ public class CounterMeterHandler implements DisposableBean {
|
||||||
MeterUtils.upvoteCounter(meterRegistry, name);
|
MeterUtils.upvoteCounter(meterRegistry, name);
|
||||||
upvoteCounter.increment(nullSafe(counter.getUpvote()));
|
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
|
// total comment counter
|
||||||
io.micrometer.core.instrument.Counter totalCommentCounter =
|
io.micrometer.core.instrument.Counter totalCommentCounter =
|
||||||
MeterUtils.totalCommentCounter(meterRegistry, name);
|
MeterUtils.totalCommentCounter(meterRegistry, name);
|
||||||
|
|
|
@ -20,6 +20,7 @@ public class MeterUtils {
|
||||||
public static final String SCENE = "scene";
|
public static final String SCENE = "scene";
|
||||||
public static final String VISIT_SCENE = "visit";
|
public static final String VISIT_SCENE = "visit";
|
||||||
public static final String UPVOTE_SCENE = "upvote";
|
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 TOTAL_COMMENT_SCENE = "total_comment";
|
||||||
public static final String APPROVED_COMMENT_SCENE = "approved_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));
|
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) {
|
public static Counter totalCommentCounter(MeterRegistry registry, String name) {
|
||||||
return counter(registry, name, Tag.of(SCENE, TOTAL_COMMENT_SCENE));
|
return counter(registry, name, Tag.of(SCENE, TOTAL_COMMENT_SCENE));
|
||||||
}
|
}
|
||||||
|
@ -75,6 +80,14 @@ public class MeterUtils {
|
||||||
return UPVOTE_SCENE.equals(sceneValue);
|
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) {
|
public static boolean isTotalCommentCounter(Counter counter) {
|
||||||
String sceneValue = counter.getId().getTag(SCENE);
|
String sceneValue = counter.getId().getTag(SCENE);
|
||||||
if (StringUtils.isBlank(sceneValue)) {
|
if (StringUtils.isBlank(sceneValue)) {
|
||||||
|
|
|
@ -125,7 +125,7 @@ public class RbacRequestEvaluation {
|
||||||
if (Objects.equals(ruleURL, requestedURL)) {
|
if (Objects.equals(ruleURL, requestedURL)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (StringUtils.startsWith(ruleURL, WildCard.NonResourceAll)
|
if (StringUtils.endsWith(ruleURL, WildCard.NonResourceAll)
|
||||||
&& StringUtils.startsWith(requestedURL,
|
&& StringUtils.startsWith(requestedURL,
|
||||||
StringUtils.stripEnd(ruleURL, WildCard.NonResourceAll))) {
|
StringUtils.stripEnd(ruleURL, WildCard.NonResourceAll))) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -12,5 +12,5 @@ rules:
|
||||||
- apiGroups: [ "api.halo.run" ]
|
- apiGroups: [ "api.halo.run" ]
|
||||||
resources: [ "*" ]
|
resources: [ "*" ]
|
||||||
verbs: [ "*" ]
|
verbs: [ "*" ]
|
||||||
- nonResourceURLs: [ "/apis/api.halo.run/v1alpha1/trackers/counter" ]
|
- nonResourceURLs: [ "/apis/api.halo.run/v1alpha1/trackers/*" ]
|
||||||
verbs: [ "create" ]
|
verbs: [ "create" ]
|
||||||
|
|
|
@ -99,6 +99,15 @@ class MeterUtilsTest {
|
||||||
assertThat(MeterUtils.isVisitCounter(upvoteCounter)).isFalse();
|
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
|
@Test
|
||||||
void isTotalCommentCounter() {
|
void isTotalCommentCounter() {
|
||||||
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
MeterRegistry meterRegistry = new SimpleMeterRegistry();
|
||||||
|
|
Loading…
Reference in New Issue