Forráskód Böngészése

Merge remote-tracking branch 'remotes/origin/developer' into developerA

Aslee 2 éve
szülő
commit
94c5af9f0e

+ 27 - 0
src/main/java/com/caimei/annotation/Idempotent.java

@@ -0,0 +1,27 @@
+package com.caimei.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 自定义幂等注解
+ *
+ * @author : Charles
+ * @date : 2021/2/26
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Idempotent {
+    /**
+     * 前缀属性,作为redis缓存Key的一部分。
+     */
+    String prefix() default "idempotent_";
+    /**
+     * 需要的参数名数组
+     */
+    String[] keys();
+    /**
+     * 幂等过期时间(秒),即:在此时间段内,对API进行幂等处理。
+     */
+    int expire() default 3;
+}

+ 103 - 0
src/main/java/com/caimei/annotation/IdempotentAspect.java

@@ -0,0 +1,103 @@
+package com.caimei.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 redis.clients.jedis.params.SetParams;
+
+import javax.annotation.Resource;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+
+/**
+ * 幂等切面
+ *
+ * @author : Charles
+ * @date : 2021/2/26
+ */
+@Slf4j
+@Aspect
+@Component
+@ConditionalOnClass(RedisTemplate.class)
+public class IdempotentAspect {
+
+    private static final String LOCK_SUCCESS = "OK";
+
+    @Resource
+    private RedisTemplate<String,String> redisTemplate;
+
+    /**
+     * 切入点,根据自定义Idempotent实际路径进行调整
+     */
+    @Pointcut("@annotation(com.caimei.annotation.Idempotent)")
+    public void executeIdempotent() {
+    }
+
+    @Around("executeIdempotent()")
+    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
+        // 获取参数对象列表
+        Object[] args = joinPoint.getArgs();
+      	//获取方法
+        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
+        // 得到方法名
+        String methodName = method.getName();
+        // 获取参数名称数组
+        String[] parameters = new LocalVariableTableParameterNameDiscoverer().getParameterNames(method);
+
+      	//获取幂等注解
+        Idempotent idempotent = method.getAnnotation(Idempotent.class);
+
+        // 初始化springEL表达式解析器实例
+        ExpressionParser parser = new SpelExpressionParser();
+        // 初始化解析内容上下文
+        EvaluationContext context = new StandardEvaluationContext();
+        // 把参数名和参数值放入解析内容上下文里
+        for (int i = 0; i < parameters.length; i++) {
+            if (args[i] != null) {
+                // 添加解析对象目标
+                context.setVariable(parameters[i], args[i].hashCode());
+            }
+        }
+        // 解析定义key对应的值,拼接成key
+        StringBuilder idempotentKey = new StringBuilder(idempotent.prefix() + ":" + methodName);
+        for (String s : idempotent.keys()) {
+            // 解析对象
+            Expression expression = parser.parseExpression(s);
+            // 根据参数生成唯一标识,拼接到key上
+            idempotentKey.append(":").append(expression.getValue(context));
+        }
+        // 通过 setnx 确保只有一个接口能够正常访问
+        String result = redisTemplate.execute(
+            (RedisCallback<String>) connection -> (
+                (JedisCommands) connection.getNativeConnection()
+            ).set(
+                idempotentKey.toString(),
+                idempotentKey.toString(),
+                new SetParams().nx().ex(idempotent.expire())
+            )
+        );
+
+        if (LOCK_SUCCESS.equals(result)) {
+            return joinPoint.proceed();
+        } else {
+            log.error("API幂等处理, key=" + idempotentKey);
+            throw new IdempotentException("手速太快了,稍后重试!");
+        }
+    }
+}
+

+ 19 - 0
src/main/java/com/caimei/annotation/IdempotentException.java

@@ -0,0 +1,19 @@
+package com.caimei.annotation;
+
+/**
+ * 处理幂等相关异常
+ *
+ * @author : Charles
+ * @date : 2021/2/26
+ */
+public class IdempotentException extends RuntimeException {
+
+    public IdempotentException(String message) {
+        super(message);
+    }
+
+    @Override
+    public String getMessage() {
+        return super.getMessage();
+    }
+}

+ 26 - 0
src/main/java/com/caimei/annotation/IdempotentExceptionHandler.java

@@ -0,0 +1,26 @@
+package com.caimei.annotation;
+
+
+import com.caimei.model.ResponseJson;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+/**
+ * ApI幂等异常处理
+ *
+ * @author : Charles
+ * @date : 2021/3/29
+ */
+@Slf4j
+@ControllerAdvice
+public class IdempotentExceptionHandler {
+    @ExceptionHandler(IdempotentException.class)
+    @ResponseBody
+    public ResponseJson convertExceptionMsg(Exception e) {
+        //自定义逻辑,可返回其他值
+        log.error("ApI幂等错误拦截,错误信息:", e);
+        return ResponseJson.error("幂等异常处理:" + e.getMessage());
+    }
+}

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

@@ -0,0 +1,30 @@
+package com.caimei.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;
+}

+ 106 - 0
src/main/java/com/caimei/annotation/StatisticsAspect.java

@@ -0,0 +1,106 @@
+package com.caimei.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.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;
+
+/**
+ * 统计切面
+ *
+ * @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.caimei.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();
+        String fieldKey = null;
+        boolean contains = null != parameters && Arrays.asList(parameters).contains(field);
+        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)
+        );
+    }
+}

+ 2 - 0
src/main/java/com/caimei/controller/admin/auth/AuthProductApi.java

@@ -2,6 +2,7 @@ package com.caimei.controller.admin.auth;
 
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.caimei.annotation.Idempotent;
 import com.caimei.model.ResponseJson;
 import com.caimei.model.dto.ProductSaveDto;
 import com.caimei.model.po.ProductParamPo;
@@ -128,6 +129,7 @@ public class AuthProductApi {
     @ApiOperation("审核商品")
     @ApiImplicitParam(name = "params", value = "productId:授权商品id;auditStatus:审核状态:0审核未通过,1审核通过,2待审核;" +
             "invalidReason:审核不通过原因;auditBy:审核人用户id;source:来源:1管理员审核,2供应商审核", required = true)
+    @Idempotent(prefix = "idempotent_product", keys = {"#params"}, expire = 5)
     @PostMapping("/audit")
     public ResponseJson auditProduct(@RequestBody String params) {
         JSONObject paramsMap = JSONObject.parseObject(params);

+ 5 - 0
src/main/java/com/caimei/model/po/ProductTypePo.java

@@ -28,6 +28,11 @@ public class ProductTypePo {
      */
     private Integer brandId;
 
+    /**
+     * 品牌名称
+     */
+    private String brandName;
+
     /**
      * 设备分类名称
      */

+ 19 - 16
src/main/java/com/caimei/service/auth/impl/AuthProductServiceImpl.java

@@ -138,16 +138,22 @@ public class AuthProductServiceImpl implements AuthProductService {
             return ResponseJson.error("参数异常,请输入授权id", null);
         }
         AuthVo auth = authMapper.getAuthById(authId);
-        if (null == brandId) {
-            return ResponseJson.error("参数异常,请输入品牌id", null);
-        }
-        if (null == productTypeId && StringUtils.isEmpty(productName)) {
-            return ResponseJson.error("参数异常,请输入设备分类id或设备名称", null);
-        }
-        if (null == productTypeId && StringUtils.isNotEmpty(productName)) {
-            ProductTypePo productType = authProductMapper.getProductType(null, productName, auth.getAuthUserId());
-            if (null != productType) {
-                productTypeId = productType.getProductTypeId();
+        if (null == productTypeId) {
+            // 新增设备分类,需要传入设备名称,品牌id,参数列表
+            if (null == brandId) {
+                return ResponseJson.error("参数异常,请输入品牌id", null);
+            }
+            if (StringUtils.isEmpty(productName)) {
+                return ResponseJson.error("参数异常,请输入设备分类id或设备名称", null);
+            }
+            if (StringUtils.isNotEmpty(productName)) {
+                ProductTypePo productType = authProductMapper.getProductType(null, productName, auth.getAuthUserId());
+                if (null != productType) {
+                    productTypeId = productType.getProductTypeId();
+                }
+            }
+            if (null == paramList || paramList.size() <= 0) {
+                return ResponseJson.error("参数异常,商品参数列表不能为空", null);
             }
         }
         if (StringUtils.isBlank(snCode)) {
@@ -168,9 +174,6 @@ public class AuthProductServiceImpl implements AuthProductService {
                 return ResponseJson.error("参数异常,请选择二维码授权牌模板", null);
             }
         }
-        /*if (null == paramList || paramList.size() <= 0) {
-            return ResponseJson.error("参数异常,商品参数列表不能为空", null);
-        }*/
         // 是否为添加操作
         boolean insertFlag = null == productId;
         ProductFormVo dbProduct = null;
@@ -332,7 +335,7 @@ public class AuthProductServiceImpl implements AuthProductService {
         // 删除商品参数
         authProductMapper.deleteParamsByProductId(product.getProductId());
         // 保存商品参数
-        if (paramList != null) {
+        if (null == productTypeId) {
             paramList.forEach(param -> {
                 if (StringUtils.isNotBlank(param.getParamName()) && StringUtils.isNotBlank(param.getParamContent())) {
                     authProductMapper.insertProductParam(product.getProductId(), param.getParamName(), param.getParamContent());
@@ -389,9 +392,9 @@ public class AuthProductServiceImpl implements AuthProductService {
         if (null == productForm) {
             return ResponseJson.error("商品不存在", null);
         }
-        List<ProductParamPo> paramList = authProductMapper.getParamsByProductId(productId);
+        List<ProductParamPo> paramList = authProductMapper.getProductTypeParamList(productForm.getProductTypeId());
         if (null == paramList || paramList.size() == 0) {
-            paramList = authProductMapper.getProductTypeParamList(productForm.getProductTypeId());
+            paramList = authProductMapper.getParamsByProductId(productId);
         }
         productForm.setParamList(paramList);
         return ResponseJson.success(productForm);

+ 12 - 11
src/main/resources/mapper/AuthProductMapper.xml

@@ -206,13 +206,13 @@
         select id from cm_brand_auth_product where snCode = #{snCode} limit 1
     </select>
     <select id="getProductFormByProductId" resultType="com.caimei.model.vo.ProductFormVo">
-        select p.`id`    as productId,
+        select p.`id`                                                      as productId,
                `authId`,
-               cb.name as brandName,
+               if(p.productTypeId is null, cb1.name, cb2.name)             as brandName,
                p.productTypeId,
-               if(p.productTypeId is null, p.name, t.name) as productName,
+               if(p.productTypeId is null, p.name, t.name)                 as productName,
                `snCode`,
-               if(p.productTypeId is null, p.image, t.image) as productImage,
+               if(p.productTypeId is null, p.image, t.image)               as productImage,
                `certificateImage`,
                `originalCertificateImage`,
                addQrCodeFlag,
@@ -221,10 +221,11 @@
                invoiceImage,
                p.`status`,
                p.auditStatus,
-               if(shopAuditStatus = 0,shopInvalidReason,p.invalidReason) as invalidReason
+               if(shopAuditStatus = 0, shopInvalidReason, p.invalidReason) as invalidReason
         from cm_brand_auth_product p
                  left join cm_brand_product_type t on p.productTypeId = t.id and t.delFlag = 0
-                 left join cm_brand cb on p.brandId = cb.id
+                 left join cm_brand cb1 on p.brandId = cb1.id
+                 left join cm_brand cb2 on t.brandId = cb2.id
         where p.id = #{productId}
     </select>
     <select id="getParamsByProductId" resultType="com.caimei.model.po.ProductParamPo">
@@ -407,14 +408,14 @@
         order by t.id desc
     </select>
     <select id="getProductType" resultType="com.caimei.model.po.ProductTypePo">
-        select id as productTypeId, brandId, name, image, pcImage, appletsImage, authUserId, createBy
-        from cm_brand_product_type
+        select t.id as productTypeId, brandId, b.name as brandName, t.name, image, pcImage, appletsImage, authUserId, createBy
+        from cm_brand_product_type t left join cm_brand b on t.brandId = b.id
         <where>
             <if test="productTypeId != null">
-                and id = #{productTypeId}
+                and t.id = #{productTypeId}
             </if>
             <if test="productName != null and productName != ''">
-                and name = #{productName}
+                and t.name = #{productName}
             </if>
             <if test="authUserId != null">
                 and authUserId = #{authUserId}
@@ -426,7 +427,7 @@
         select count(*) from cm_brand_auth_product where productTypeId = #{productTypeId}
     </select>
     <select id="getProductPo" resultType="com.caimei.model.po.ProductPo">
-        select id as productId, productTypeId, name, image, createBy
+        select id as productId, brandId, productTypeId, name, image, createBy
         from cm_brand_auth_product
         where id = #{productId}
     </select>

+ 1 - 1
src/main/resources/mapper/ClubMapper.xml

@@ -102,7 +102,7 @@
         <if test="townId != null">
             and a.townId = #{townId}
         </if>
-        order by pc.productCount desc, distance
+        order by distance
     </select>
     <select id="checkMobile" resultType="java.lang.Integer">
         select cu.id as clubUserId from cm_brand_club_user cu