Procházet zdrojové kódy

益寿缘app-优化App奖品接口防重复提交

cgp před 6 dny
rodič
revize
d4c00beab8

+ 5 - 1
fs-service/src/main/java/com/fs/his/service/impl/AppUserRewardServiceImpl.java

@@ -61,7 +61,11 @@ public class AppUserRewardServiceImpl implements IAppUserRewardService {
         Long rewardsId=claimRewardsAddDTO.getRewardsId();
         FsUserRewards reward = rewardsMapper.selectByUserIdAndRewardsId(claimRewardsAddDTO.getFsUserId(), rewardsId);
         if (reward==null){
-            log.info("用户:{}没有奖品:{}", fsUserId, rewardsId);
+            log.info("用户:{}没有可领取的奖品:{}", fsUserId, rewardsId);
+            return;
+        }
+        if (reward.getStatus()==1||reward.getStatus()==2){
+            log.info("用户:{}奖品已领取: rewardsId={}",fsUserId, rewardsId);
             return;
         }
         // 校验是否过期(目前只有看课奖品需要校验过期)

+ 34 - 0
fs-user-app/src/main/java/com/fs/app/annotation/NoRepeatSubmit.java

@@ -0,0 +1,34 @@
+package com.fs.app.annotation;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 防重复App提交注解
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface NoRepeatSubmit {
+    
+    /**
+     * 过期时间,默认10秒
+     */
+    int expire() default 10;
+    
+    /**
+     * 时间单位,默认秒
+     */
+    TimeUnit timeUnit() default TimeUnit.SECONDS;
+    
+    /**
+     * 提示消息
+     */
+    String message() default "领取人数过多,请稍后再试";
+    
+    /**
+     * 是否锁定用户所有请求,默认false只锁定当前接口
+     * 如果为true,则同一个用户在规定时间内不能调用任何带有@NoRepeatSubmit注解的接口
+     */
+    boolean lockUser() default false;
+}

+ 4 - 0
fs-user-app/src/main/java/com/fs/app/controller/AppUserRewardController.java

@@ -1,6 +1,7 @@
 package com.fs.app.controller;
 
 import com.fs.app.annotation.Login;
+import com.fs.app.annotation.NoRepeatSubmit;
 import com.fs.app.domain.FsAppRole;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.page.TableDataInfo;
@@ -36,6 +37,7 @@ public class AppUserRewardController  extends AppBaseController{
     @Login
     @ApiOperation("跳转下发注册登录奖励")
     @GetMapping("/issueFirstLoginReward")
+    @NoRepeatSubmit(expire = 10, message = "请勿重复点击,正在处理中...")
     public R issueFirstLoginReward() {
         String loginUserId = getUserId();
         if (StringUtils.isEmpty(loginUserId)){
@@ -57,6 +59,7 @@ public class AppUserRewardController  extends AppBaseController{
     @Login
     @ApiOperation("跳转下发App连续看课奖励")
     @GetMapping("/issueWatchCourseReward")
+    @NoRepeatSubmit(expire = 10, message = "请勿重复点击,正在处理中...")
     public R issueWatchCourseReward() {
         String loginUserId = getUserId();
         if (StringUtils.isEmpty(loginUserId)){
@@ -97,6 +100,7 @@ public class AppUserRewardController  extends AppBaseController{
     @Login
     @ApiOperation("奖品列表用户领取奖励")
     @PostMapping("/claim")
+    @NoRepeatSubmit(expire = 10, message = "领取请求处理中,请勿重复提交")
     public R claimRewards(@RequestBody ClaimRewardsAddDTO claimRewardsAddDTO) {//领取商品时必传收货地址id
         String loginUserId = getUserId();
         if (StringUtils.isEmpty(loginUserId)){

+ 109 - 0
fs-user-app/src/main/java/com/fs/app/utils/RepeatSubmitKeyGenerator.java

@@ -0,0 +1,109 @@
+package com.fs.app.utils;
+
+import com.fs.app.annotation.NoRepeatSubmit;
+import com.fs.common.utils.StringUtils;
+import io.jsonwebtoken.Claims;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+
+/**
+ * 防重复提交Key生成器
+ */
+public class RepeatSubmitKeyGenerator {
+
+    private static JwtUtils jwtUtils;
+
+    /**
+     * 设置JwtUtils(通过静态方法注入)
+     */
+    public static void setJwtUtils(JwtUtils jwtUtils) {
+        RepeatSubmitKeyGenerator.jwtUtils = jwtUtils;
+    }
+
+    /**
+     * 生成防重复提交的key
+     * @param joinPoint 切点
+     * @return key
+     */
+    public static String generateKey(ProceedingJoinPoint joinPoint) {
+        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+        Method method = signature.getMethod();
+
+        // 获取当前请求对象
+        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attributes == null) {
+            throw new RuntimeException("无法获取请求信息");
+        }
+
+        HttpServletRequest request = attributes.getRequest();
+
+        // 获取用户ID(使用项目中的方式)
+        String userId = getUserId(request);
+        if (StringUtils.isEmpty(userId)) {
+            userId = "anonymous";
+        }
+
+        // 获取请求URI
+        String uri = request.getRequestURI();
+
+        // 获取请求参数
+        String params = getParamsString(joinPoint.getArgs());
+
+        // 组合Key:前缀:userId:uri:参数哈希
+        String key = "no_repeat:" + userId + ":" + uri;
+
+        // 如果不锁定用户所有请求,则加上方法签名和参数
+        NoRepeatSubmit annotation = method.getAnnotation(NoRepeatSubmit.class);
+        if (annotation != null && !annotation.lockUser()) {
+            // 加上方法名和参数的哈希值
+            String methodInfo = method.getDeclaringClass().getName() + "." + method.getName();
+            String paramHash = Integer.toHexString(params.hashCode());
+            key = key + ":" + methodInfo + ":" + paramHash;
+        }
+
+        return key;
+    }
+
+    /**
+     * 获取用户ID - 适配项目中的方式
+     */
+    private static String getUserId(HttpServletRequest request) {
+        try {
+            // 从Header中获取APPToken
+            String token = request.getHeader("APPToken");
+            if (StringUtils.isNotEmpty(token) && jwtUtils != null) {
+                Claims claims = jwtUtils.getClaimByToken(token);
+                if (claims != null) {
+                    return claims.getSubject();
+                }
+            }
+        } catch (Exception e) {
+            // 忽略异常,返回null
+        }
+        return null;
+    }
+
+    /**
+     * 将参数数组转换为字符串
+     */
+    private static String getParamsString(Object[] args) {
+        if (args == null || args.length == 0) {
+            return "";
+        }
+
+        StringBuilder sb = new StringBuilder();
+        for (Object arg : args) {
+            if (arg != null) {
+                sb.append(arg.toString()).append(",");
+            } else {
+                sb.append("null,");
+            }
+        }
+        return sb.toString();
+    }
+}

+ 133 - 0
fs-user-app/src/main/java/com/fs/framework/aspectj/NoRepeatSubmitAspect.java

@@ -0,0 +1,133 @@
+package com.fs.framework.aspectj;
+
+import com.fs.app.annotation.NoRepeatSubmit;
+import com.fs.app.utils.JwtUtils;
+import com.fs.app.utils.RepeatSubmitKeyGenerator;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.redis.RedisCache;
+import com.fs.common.exception.CustomException;
+import com.fs.common.utils.StringUtils;
+import io.jsonwebtoken.Claims;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 防重复提交切面
+ */
+@Aspect
+@Component
+public class NoRepeatSubmitAspect {
+
+    private static final Logger log = LoggerFactory.getLogger(NoRepeatSubmitAspect.class);
+
+    @Autowired
+    private RedisCache redisCache;
+
+    @Autowired
+    private JwtUtils jwtUtils;
+
+    /**
+     * 初始化,将JwtUtils注入到KeyGenerator中
+     */
+    @PostConstruct
+    public void init() {
+        RepeatSubmitKeyGenerator.setJwtUtils(jwtUtils);
+    }
+
+    /**
+     * 定义切点:所有添加了@NoRepeatSubmit注解的方法
+     */
+    @Pointcut("@annotation(com.fs.app.annotation.NoRepeatSubmit)")
+    public void noRepeatSubmitPointcut() {
+    }
+
+    /**
+     * 环绕通知处理防重复提交
+     */
+    @Around("noRepeatSubmitPointcut()")
+    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
+        // 获取方法上的注解
+        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+        Method method = signature.getMethod();
+        NoRepeatSubmit annotation = method.getAnnotation(NoRepeatSubmit.class);
+
+        // 获取过期时间
+        int expire = annotation.expire();
+        TimeUnit timeUnit = annotation.timeUnit();
+        String message = annotation.message();
+
+        // 生成Redis Key
+        String key = RepeatSubmitKeyGenerator.generateKey(joinPoint);
+
+        // 尝试设置Redis键,使用setIfAbsent(相当于setnx)
+        Boolean success = redisCache.setIfAbsent(key, "1", expire, timeUnit);
+
+        if (Boolean.TRUE.equals(success)) {
+            // 第一次请求,正常执行方法
+            try {
+                return joinPoint.proceed();
+            } catch (Exception e) {
+                // 如果方法执行异常,删除key,允许重试
+                redisCache.deleteObject(key);
+                throw e;
+            }
+        } else {
+            // 重复请求,记录日志并返回提示
+            String userId = getUserIdFromToken();
+            log.warn("防重复提交拦截: key={}, 用户={}, URI={}, 方法={}",
+                    key, userId, getRequestURI(), method.getName());
+            return R.error(message);
+        }
+    }
+
+    /**
+     * 从Token中获取用户ID
+     */
+    private String getUserIdFromToken() {
+        try {
+            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+            if (attributes != null) {
+                HttpServletRequest request = attributes.getRequest();
+                String token = request.getHeader("APPToken");
+                if (StringUtils.isNotEmpty(token)) {
+                    Claims claims = jwtUtils.getClaimByToken(token);
+                    if (claims != null) {
+                        return claims.getSubject();
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.error("获取用户ID失败", e);
+        }
+        return "unknown";
+    }
+
+    /**
+     * 获取当前请求的URI
+     */
+    private String getRequestURI() {
+        try {
+            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+            if (attributes != null) {
+                return attributes.getRequest().getRequestURI();
+            }
+        } catch (Exception e) {
+            log.error("获取请求URI失败", e);
+        }
+        return "unknown";
+    }
+}