From 08fe1858cfe67829b92423ac2be922200fbba1c8 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Sat, 15 Oct 2022 12:53:34 +0800 Subject: [PATCH] feat: add upvote and downvote tracker (#2566) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### 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 ``` --- .../run/halo/app/core/extension/Counter.java | 5 ++ .../extension/endpoint/TrackerEndpoint.java | 69 ++++++++++++++++++- .../halo/app/metrics/CounterMeterHandler.java | 5 ++ .../java/run/halo/app/metrics/MeterUtils.java | 13 ++++ .../authorization/RbacRequestEvaluation.java | 2 +- .../extensions/role-template-anonymous.yaml | 2 +- .../run/halo/app/metrics/MeterUtilsTest.java | 9 +++ 7 files changed, 100 insertions(+), 5 deletions(-) diff --git a/src/main/java/run/halo/app/core/extension/Counter.java b/src/main/java/run/halo/app/core/extension/Counter.java index 885966867..bdd959d79 100644 --- a/src/main/java/run/halo/app/core/extension/Counter.java +++ b/src/main/java/run/halo/app/core/extension/Counter.java @@ -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; } diff --git a/src/main/java/run/halo/app/core/extension/endpoint/TrackerEndpoint.java b/src/main/java/run/halo/app/core/extension/endpoint/TrackerEndpoint.java index ca4772992..054316dbc 100644 --- a/src/main/java/run/halo/app/core/extension/endpoint/TrackerEndpoint.java +++ b/src/main/java/run/halo/app/core/extension/endpoint/TrackerEndpoint.java @@ -43,7 +43,7 @@ public class TrackerEndpoint implements CustomEndpoint { public RouterFunction 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 increase(ServerRequest request) { + private Mono 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 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 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) { /** diff --git a/src/main/java/run/halo/app/metrics/CounterMeterHandler.java b/src/main/java/run/halo/app/metrics/CounterMeterHandler.java index da04813a2..16ed6baac 100644 --- a/src/main/java/run/halo/app/metrics/CounterMeterHandler.java +++ b/src/main/java/run/halo/app/metrics/CounterMeterHandler.java @@ -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); diff --git a/src/main/java/run/halo/app/metrics/MeterUtils.java b/src/main/java/run/halo/app/metrics/MeterUtils.java index 64a259dde..c61c77345 100644 --- a/src/main/java/run/halo/app/metrics/MeterUtils.java +++ b/src/main/java/run/halo/app/metrics/MeterUtils.java @@ -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)) { diff --git a/src/main/java/run/halo/app/security/authorization/RbacRequestEvaluation.java b/src/main/java/run/halo/app/security/authorization/RbacRequestEvaluation.java index 95f5c3b03..f810a44c4 100644 --- a/src/main/java/run/halo/app/security/authorization/RbacRequestEvaluation.java +++ b/src/main/java/run/halo/app/security/authorization/RbacRequestEvaluation.java @@ -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; diff --git a/src/main/resources/extensions/role-template-anonymous.yaml b/src/main/resources/extensions/role-template-anonymous.yaml index 8597bd2fa..be8d6bfc6 100644 --- a/src/main/resources/extensions/role-template-anonymous.yaml +++ b/src/main/resources/extensions/role-template-anonymous.yaml @@ -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" ] diff --git a/src/test/java/run/halo/app/metrics/MeterUtilsTest.java b/src/test/java/run/halo/app/metrics/MeterUtilsTest.java index 6cee04d06..e217b74db 100644 --- a/src/test/java/run/halo/app/metrics/MeterUtilsTest.java +++ b/src/test/java/run/halo/app/metrics/MeterUtilsTest.java @@ -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();