Browse Source

自定义统计注解

chao 3 năm trước cách đây
mục cha
commit
17c789f846

+ 8 - 1
backup.sql

@@ -31,4 +31,11 @@ CREATE TABLE `cm_product_sales_record`(
      `productCount` int(11) DEFAULT NULL COMMENT '销量',
      `addTime` DATETIME DEFAULT NULL COMMENT '销售时间',
      PRIMARY KEY (`id`)
-) ENGINE=INNODB CHARSET=utf8mb4 COMMENT='用户销量记录表';
+) ENGINE=INNODB CHARSET=utf8mb4 COMMENT='商品销量记录表';
+CREATE TABLE `cm_product_views_record`(
+     `id` INT NOT NULL AUTO_INCREMENT,
+     `productId` int(11) NOT NULL COMMENT '商品ID',
+     `views` int(11) DEFAULT NULL COMMENT '访问量',
+     `countTime` DATETIME DEFAULT NULL COMMENT '统计日期',
+     PRIMARY KEY (`id`)
+) ENGINE=INNODB CHARSET=utf8mb4 COMMENT='商品详情访问量记录表';

+ 1 - 1
src/main/java/com/caimei365/commodity/idempotent/Idempotent.java → src/main/java/com/caimei365/commodity/annotation/Idempotent.java

@@ -1,4 +1,4 @@
-package com.caimei365.commodity.idempotent;
+package com.caimei365.commodity.annotation;
 
 import java.lang.annotation.*;
 

+ 2 - 2
src/main/java/com/caimei365/commodity/idempotent/IdempotentAspect.java → src/main/java/com/caimei365/commodity/annotation/IdempotentAspect.java

@@ -1,4 +1,4 @@
-package com.caimei365.commodity.idempotent;
+package com.caimei365.commodity.annotation;
 
 import lombok.extern.slf4j.Slf4j;
 import org.aspectj.lang.ProceedingJoinPoint;
@@ -44,7 +44,7 @@ public class IdempotentAspect {
     /**
      * 切入点,根据自定义Idempotent实际路径进行调整
      */
-    @Pointcut("@annotation(com.caimei365.commodity.idempotent.Idempotent)")
+    @Pointcut("@annotation(com.caimei365.commodity.annotation.Idempotent)")
     public void executeIdempotent() {
     }
 

+ 1 - 1
src/main/java/com/caimei365/commodity/idempotent/IdempotentException.java → src/main/java/com/caimei365/commodity/annotation/IdempotentException.java

@@ -1,4 +1,4 @@
-package com.caimei365.commodity.idempotent;
+package com.caimei365.commodity.annotation;
 
 /**
  * 处理幂等相关异常

+ 1 - 1
src/main/java/com/caimei365/commodity/idempotent/IdempotentExceptionHandler.java → src/main/java/com/caimei365/commodity/annotation/IdempotentExceptionHandler.java

@@ -1,4 +1,4 @@
-package com.caimei365.commodity.idempotent;
+package com.caimei365.commodity.annotation;
 
 
 import com.caimei365.commodity.model.ResponseJson;

+ 30 - 0
src/main/java/com/caimei365/commodity/annotation/Statistics.java

@@ -0,0 +1,30 @@
+package com.caimei365.commodity.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 自定义统计注解
+ *
+ * @author : Charles
+ * @date : 2021/12/29
+ */
+// @Target 指定注解作用的地方:方法,变量,类
+@Target(ElementType.METHOD)
+// @Retention 生命周期
+@Retention(RetentionPolicy.RUNTIME)
+// @Documented 是否能随着被定义的java文件生成到JavaDoc文档当中 (非必填)
+@Documented
+public @interface Statistics {
+    /**
+     * 前缀属性,作为redis缓存Key的一部分。
+     */
+    String prefix() default "statistics_";
+    /**
+     * 需要的参数名
+     */
+    String field();
+    /**
+     * 过期时间(秒),默认两天 60*60*24*2 = 172800
+     */
+    int expire() default 172800;
+}

+ 112 - 0
src/main/java/com/caimei365/commodity/annotation/StatisticsAspect.java

@@ -0,0 +1,112 @@
+package com.caimei365.commodity.annotation;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Component;
+import redis.clients.jedis.commands.JedisCommands;
+
+import javax.annotation.Resource;
+import java.lang.reflect.Method;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * 统计切面
+ *
+ * @author : Charles
+ * @date : 2021/12/29
+ */
+@Slf4j
+@Aspect
+@Component
+@ConditionalOnClass(RedisTemplate.class)
+public class StatisticsAspect {
+    @Resource
+    private RedisTemplate<String,String> redisTemplate;
+
+    /**
+     * 切入点,根据自定义Statistics实际路径进行调整
+     */
+    @Pointcut("@annotation(com.caimei365.commodity.annotation.Statistics)")
+    public void executeStatistics() {}
+
+    @Around("executeStatistics()")
+    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
+        // 获取方法
+        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
+        // 获取参数名数组
+        String[] parameters = new LocalVariableTableParameterNameDiscoverer().getParameterNames(method);
+        // 获取参数值列表
+        Object[] args = joinPoint.getArgs();
+        // 获取自定义注解
+        Statistics statistics = method.getAnnotation(Statistics.class);
+        String prefix = statistics.prefix();
+        String field = statistics.field();
+        int expire = statistics.expire();
+        boolean contains = Arrays.asList(parameters).contains(field);
+        String fieldKey = null;
+        if (contains) {
+            for (int i = 0; i < parameters.length; i++) {
+                if (parameters[i] != null && parameters[i].equals(field)) {
+                    fieldKey = args[i].toString();
+                }
+            }
+        }
+        if (null != fieldKey) {
+            // 当前日期串
+            String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
+            // 拼接redisKey
+            String redisKey = prefix + ":" + field + ":" + dateStr;
+            // 获取Redis已有的统计值 + 1
+            int count = getHash(redisKey, fieldKey) + 1;
+            // 设置统计数量 + 1, 过期时间默认两天,(后面定时任务每天凌晨把前一天统计量存入数据库(按天存))
+            setHash(redisKey, fieldKey, count, expire);
+        }
+        return joinPoint.proceed();
+    }
+
+    /**
+     * 获取Redis中hash对应的统计值
+     */
+    public Integer getHash(String key, String fieldKey) {
+        String result = redisTemplate.execute(
+                (RedisCallback<String>) connection -> (
+                        (JedisCommands) connection.getNativeConnection()
+                ).hget(key, fieldKey)
+        );
+        return result == null ? 0 : Integer.parseInt(result);
+    }
+
+    /**
+     * 添加/更新Redis中hash的值
+     */
+    public void setHash(String key, String fieldKey, int count, int expire) {
+        String value = Integer.toString(count);
+        redisTemplate.execute(
+                (RedisCallback<Object>) connection -> (
+                        (JedisCommands) connection.getNativeConnection()
+                ).hset(key, fieldKey, value)
+        );
+        // 设置超时时间 (秒)
+        redisTemplate.execute(
+                (RedisCallback<Object>) connection -> (
+                        (JedisCommands) connection.getNativeConnection()
+                ).expire(key, expire)
+        );
+    }
+}

+ 1 - 1
src/main/java/com/caimei365/commodity/controller/CouponApi.java

@@ -1,6 +1,6 @@
 package com.caimei365.commodity.controller;
 
-import com.caimei365.commodity.idempotent.Idempotent;
+import com.caimei365.commodity.annotation.Idempotent;
 import com.caimei365.commodity.model.ResponseJson;
 import com.caimei365.commodity.model.dto.CollarCouponsDto;
 import com.caimei365.commodity.model.dto.RedeemCouponsDto;

+ 3 - 2
src/main/java/com/caimei365/commodity/controller/ProductPageApi.java

@@ -1,8 +1,8 @@
 package com.caimei365.commodity.controller;
 
+import com.caimei365.commodity.annotation.Statistics;
 import com.caimei365.commodity.model.ResponseJson;
 import com.caimei365.commodity.model.po.ProductParameterPo;
-import com.caimei365.commodity.model.vo.ProductRepairVo;
 import com.caimei365.commodity.model.search.ProductListVo;
 import com.caimei365.commodity.model.vo.*;
 import com.caimei365.commodity.service.PageService;
@@ -13,7 +13,6 @@ import io.swagger.annotations.ApiOperation;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.*;
 
-import javax.ws.rs.GET;
 import java.util.List;
 import java.util.Map;
 
@@ -192,12 +191,14 @@ public class ProductPageApi {
      *
      * @param productId 商品Id
      * @param userId    用户Id
+     * Statistics统计存入Redis的hash => prefix + ":" + field + ":" + date("yyyyMMdd");
      */
     @ApiOperation("商品详情页(旧:/product/details)")
     @ApiImplicitParams({
             @ApiImplicitParam(required = false, name = "userId", value = "用户id"),
             @ApiImplicitParam(required = false, name = "productId", value = "商品Id")
     })
+    @Statistics(prefix = "statistics_details", field = "productId")
     @GetMapping("/product/details")
     public ResponseJson<ProductDetailVo> getProductDetails(Integer productId, Integer userId) {
         return pageService.getProductDetails(productId, userId);

+ 1 - 1
src/main/java/com/caimei365/commodity/controller/SecondHandApi.java

@@ -1,6 +1,6 @@
 package com.caimei365.commodity.controller;
 
-import com.caimei365.commodity.idempotent.Idempotent;
+import com.caimei365.commodity.annotation.Idempotent;
 import com.caimei365.commodity.model.ResponseJson;
 import com.caimei365.commodity.model.dto.SecondDto;
 import com.caimei365.commodity.model.vo.BrandVo;