mirror of
				https://gitee.com/hhyykk/ipms-sjy.git
				synced 2025-11-04 04:08:43 +08:00 
			
		
		
		
	基于 Redis 实现幂等性操作
This commit is contained in:
		
							
								
								
									
										5
									
								
								pom.xml
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								pom.xml
									
									
									
									
									
								
							@@ -197,6 +197,11 @@
 | 
				
			|||||||
            <artifactId>hutool-http</artifactId>
 | 
					            <artifactId>hutool-http</artifactId>
 | 
				
			||||||
            <version>${hutool.version}</version>
 | 
					            <version>${hutool.version}</version>
 | 
				
			||||||
        </dependency>
 | 
					        </dependency>
 | 
				
			||||||
 | 
					        <dependency>
 | 
				
			||||||
 | 
					            <groupId>cn.hutool</groupId>
 | 
				
			||||||
 | 
					            <artifactId>hutool-crypto</artifactId>
 | 
				
			||||||
 | 
					            <version>${hutool.version}</version>
 | 
				
			||||||
 | 
					        </dependency>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <dependency>
 | 
					        <dependency>
 | 
				
			||||||
            <groupId>com.alibaba</groupId>
 | 
					            <groupId>com.alibaba</groupId>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,21 +0,0 @@
 | 
				
			|||||||
package com.ruoyi.common.annotation;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import java.lang.annotation.Documented;
 | 
					 | 
				
			||||||
import java.lang.annotation.ElementType;
 | 
					 | 
				
			||||||
import java.lang.annotation.Inherited;
 | 
					 | 
				
			||||||
import java.lang.annotation.Retention;
 | 
					 | 
				
			||||||
import java.lang.annotation.RetentionPolicy;
 | 
					 | 
				
			||||||
import java.lang.annotation.Target;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * 自定义注解防止表单重复提交
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author ruoyi
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
@Inherited
 | 
					 | 
				
			||||||
@Target(ElementType.METHOD)
 | 
					 | 
				
			||||||
@Retention(RetentionPolicy.RUNTIME)
 | 
					 | 
				
			||||||
@Documented
 | 
					 | 
				
			||||||
public @interface RepeatSubmit {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,49 +0,0 @@
 | 
				
			|||||||
package com.ruoyi.framework.interceptor;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import java.lang.reflect.Method;
 | 
					 | 
				
			||||||
import javax.servlet.http.HttpServletRequest;
 | 
					 | 
				
			||||||
import javax.servlet.http.HttpServletResponse;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import org.springframework.stereotype.Component;
 | 
					 | 
				
			||||||
import org.springframework.web.method.HandlerMethod;
 | 
					 | 
				
			||||||
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
 | 
					 | 
				
			||||||
import com.alibaba.fastjson.JSONObject;
 | 
					 | 
				
			||||||
import com.ruoyi.common.annotation.RepeatSubmit;
 | 
					 | 
				
			||||||
import com.ruoyi.common.core.domain.AjaxResult;
 | 
					 | 
				
			||||||
import com.ruoyi.common.utils.ServletUtils;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * 防止重复提交拦截器
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author ruoyi
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
@Component
 | 
					 | 
				
			||||||
public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter {
 | 
					 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
 | 
					 | 
				
			||||||
        if (handler instanceof HandlerMethod) {
 | 
					 | 
				
			||||||
            HandlerMethod handlerMethod = (HandlerMethod) handler;
 | 
					 | 
				
			||||||
            Method method = handlerMethod.getMethod();
 | 
					 | 
				
			||||||
            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
 | 
					 | 
				
			||||||
            if (annotation != null) {
 | 
					 | 
				
			||||||
                if (this.isRepeatSubmit(request)) {
 | 
					 | 
				
			||||||
                    AjaxResult ajaxResult = AjaxResult.error("不允许重复提交,请稍后再试");
 | 
					 | 
				
			||||||
                    ServletUtils.renderString(response, JSONObject.toJSONString(ajaxResult));
 | 
					 | 
				
			||||||
                    return false;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return true;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            return super.preHandle(request, response, handler);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 验证是否重复提交由子类实现具体的防重复提交的规则
 | 
					 | 
				
			||||||
     *
 | 
					 | 
				
			||||||
     * @param request
 | 
					 | 
				
			||||||
     * @return
 | 
					 | 
				
			||||||
     * @throws Exception
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    public abstract boolean isRepeatSubmit(HttpServletRequest request);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,114 +0,0 @@
 | 
				
			|||||||
package com.ruoyi.framework.interceptor.impl;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import java.util.HashMap;
 | 
					 | 
				
			||||||
import java.util.Map;
 | 
					 | 
				
			||||||
import java.util.concurrent.TimeUnit;
 | 
					 | 
				
			||||||
import javax.servlet.http.HttpServletRequest;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
					 | 
				
			||||||
import org.springframework.beans.factory.annotation.Value;
 | 
					 | 
				
			||||||
import org.springframework.stereotype.Component;
 | 
					 | 
				
			||||||
import com.alibaba.fastjson.JSONObject;
 | 
					 | 
				
			||||||
import com.ruoyi.common.constant.Constants;
 | 
					 | 
				
			||||||
import com.ruoyi.common.core.redis.RedisCache;
 | 
					 | 
				
			||||||
import com.ruoyi.common.filter.RepeatedlyRequestWrapper;
 | 
					 | 
				
			||||||
import com.ruoyi.common.utils.StringUtils;
 | 
					 | 
				
			||||||
import com.ruoyi.common.utils.http.HttpHelper;
 | 
					 | 
				
			||||||
import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * 判断请求url和数据是否和上一次相同,
 | 
					 | 
				
			||||||
 * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * @author ruoyi
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
@Component
 | 
					 | 
				
			||||||
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor {
 | 
					 | 
				
			||||||
    public final String REPEAT_PARAMS = "repeatParams";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public final String REPEAT_TIME = "repeatTime";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // 令牌自定义标识
 | 
					 | 
				
			||||||
    @Value("${token.header}")
 | 
					 | 
				
			||||||
    private String header;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @Autowired
 | 
					 | 
				
			||||||
    private RedisCache redisCache;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 间隔时间,单位:秒 默认10秒
 | 
					 | 
				
			||||||
     * <p>
 | 
					 | 
				
			||||||
     * 两次相同参数的请求,如果间隔时间大于该参数,系统不会认定为重复提交的数据
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    private int intervalTime = 10;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public void setIntervalTime(int intervalTime) {
 | 
					 | 
				
			||||||
        this.intervalTime = intervalTime;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @SuppressWarnings("unchecked")
 | 
					 | 
				
			||||||
    @Override
 | 
					 | 
				
			||||||
    public boolean isRepeatSubmit(HttpServletRequest request) {
 | 
					 | 
				
			||||||
        String nowParams = "";
 | 
					 | 
				
			||||||
        if (request instanceof RepeatedlyRequestWrapper) {
 | 
					 | 
				
			||||||
            RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
 | 
					 | 
				
			||||||
            nowParams = HttpHelper.getBodyString(repeatedlyRequest);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // body参数为空,获取Parameter的数据
 | 
					 | 
				
			||||||
        if (StringUtils.isEmpty(nowParams)) {
 | 
					 | 
				
			||||||
            nowParams = JSONObject.toJSONString(request.getParameterMap());
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        Map<String, Object> nowDataMap = new HashMap<String, Object>();
 | 
					 | 
				
			||||||
        nowDataMap.put(REPEAT_PARAMS, nowParams);
 | 
					 | 
				
			||||||
        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 请求地址(作为存放cache的key值)
 | 
					 | 
				
			||||||
        String url = request.getRequestURI();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 唯一值(没有消息头则使用请求地址)
 | 
					 | 
				
			||||||
        String submitKey = request.getHeader(header);
 | 
					 | 
				
			||||||
        if (StringUtils.isEmpty(submitKey)) {
 | 
					 | 
				
			||||||
            submitKey = url;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // 唯一标识(指定key + 消息头)
 | 
					 | 
				
			||||||
        String cache_repeat_key = Constants.REPEAT_SUBMIT_KEY + submitKey;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Object sessionObj = redisCache.getCacheObject(cache_repeat_key);
 | 
					 | 
				
			||||||
        if (sessionObj != null) {
 | 
					 | 
				
			||||||
            Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
 | 
					 | 
				
			||||||
            if (sessionMap.containsKey(url)) {
 | 
					 | 
				
			||||||
                Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
 | 
					 | 
				
			||||||
                if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap)) {
 | 
					 | 
				
			||||||
                    return true;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        Map<String, Object> cacheMap = new HashMap<String, Object>();
 | 
					 | 
				
			||||||
        cacheMap.put(url, nowDataMap);
 | 
					 | 
				
			||||||
        redisCache.setCacheObject(cache_repeat_key, cacheMap, intervalTime, TimeUnit.SECONDS);
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 判断参数是否相同
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) {
 | 
					 | 
				
			||||||
        String nowParams = (String) nowMap.get(REPEAT_PARAMS);
 | 
					 | 
				
			||||||
        String preParams = (String) preMap.get(REPEAT_PARAMS);
 | 
					 | 
				
			||||||
        return nowParams.equals(preParams);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 判断两次间隔时间
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap) {
 | 
					 | 
				
			||||||
        long time1 = (Long) nowMap.get(REPEAT_TIME);
 | 
					 | 
				
			||||||
        long time2 = (Long) preMap.get(REPEAT_TIME);
 | 
					 | 
				
			||||||
        if ((time1 - time2) < (this.intervalTime * 1000)) {
 | 
					 | 
				
			||||||
            return true;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -28,6 +28,9 @@ public interface GlobalErrorCodeConstants {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
 | 
					    ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ========== 自定义错误段 ==========
 | 
				
			||||||
 | 
					    ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
 | 
					    ErrorCode UNKNOWN = new ErrorCode(999, "未知错误");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   static boolean isMatch(Integer code) {
 | 
					   static boolean isMatch(Integer code) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
/**
 | 
					/**
 | 
				
			||||||
 * 字典数据模块
 | 
					 * 字典数据模块,提供 {@link cn.iocoder.dashboard.framework.dict.core.util.DictUtils} 工具类
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * 通过将字典缓存在内存中,保证性能
 | 
					 * 通过将字典缓存在内存中,保证性能
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,42 @@
 | 
				
			|||||||
 | 
					package cn.iocoder.dashboard.framework.idempotent.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.aop.IdempotentAspect;
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.DefaultIdempotentKeyResolver;
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.ExpressionIdempotentKeyResolver;
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.redis.IdempotentRedisDAO;
 | 
				
			||||||
 | 
					import org.springframework.boot.autoconfigure.AutoConfigureAfter;
 | 
				
			||||||
 | 
					import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
 | 
				
			||||||
 | 
					import org.springframework.context.annotation.Bean;
 | 
				
			||||||
 | 
					import org.springframework.context.annotation.Configuration;
 | 
				
			||||||
 | 
					import org.springframework.data.redis.core.StringRedisTemplate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Configuration(proxyBeanMethods = false)
 | 
				
			||||||
 | 
					@AutoConfigureAfter(RedisAutoConfiguration.class)
 | 
				
			||||||
 | 
					public class IdempotentConfiguration {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Bean
 | 
				
			||||||
 | 
					    public IdempotentAspect idempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
 | 
				
			||||||
 | 
					        return new IdempotentAspect(keyResolvers, idempotentRedisDAO);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Bean
 | 
				
			||||||
 | 
					    public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) {
 | 
				
			||||||
 | 
					        return new IdempotentRedisDAO(stringRedisTemplate);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // ========== 各种 IdempotentKeyResolver Bean ==========
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Bean
 | 
				
			||||||
 | 
					    public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() {
 | 
				
			||||||
 | 
					        return new DefaultIdempotentKeyResolver();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Bean
 | 
				
			||||||
 | 
					    public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() {
 | 
				
			||||||
 | 
					        return new ExpressionIdempotentKeyResolver();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,46 @@
 | 
				
			|||||||
 | 
					package cn.iocoder.dashboard.framework.idempotent.core.annotation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.DefaultIdempotentKeyResolver;
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.lang.annotation.ElementType;
 | 
				
			||||||
 | 
					import java.lang.annotation.Retention;
 | 
				
			||||||
 | 
					import java.lang.annotation.RetentionPolicy;
 | 
				
			||||||
 | 
					import java.lang.annotation.Target;
 | 
				
			||||||
 | 
					import java.util.concurrent.TimeUnit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 幂等注解
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @author 芋道源码
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Target({ElementType.METHOD})
 | 
				
			||||||
 | 
					@Retention(RetentionPolicy.RUNTIME)
 | 
				
			||||||
 | 
					public @interface Idempotent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 幂等的超时时间,默认为 1 秒
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * 注意,如果执行时间超过它,请求还是会进来
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    int timeout() default 1;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 时间单位,默认为 SECONDS 秒
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    TimeUnit timeUnit() default TimeUnit.SECONDS;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 提示信息,正在执行中的提示
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    String message() default "重复请求,请稍后重试";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 使用的 Key 解析器
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 使用的 Key 参数
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    String keyArg() default "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					package cn.iocoder.dashboard.framework.idempotent.core.aop;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.common.exception.ServiceException;
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.common.exception.enums.GlobalErrorCodeConstants;
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.redis.IdempotentRedisDAO;
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.util.collection.CollectionUtils;
 | 
				
			||||||
 | 
					import lombok.extern.slf4j.Slf4j;
 | 
				
			||||||
 | 
					import org.aspectj.lang.JoinPoint;
 | 
				
			||||||
 | 
					import org.aspectj.lang.annotation.Aspect;
 | 
				
			||||||
 | 
					import org.aspectj.lang.annotation.Before;
 | 
				
			||||||
 | 
					import org.springframework.util.Assert;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.List;
 | 
				
			||||||
 | 
					import java.util.Map;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @author 芋道源码
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@Aspect
 | 
				
			||||||
 | 
					@Slf4j
 | 
				
			||||||
 | 
					public class IdempotentAspect {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * IdempotentKeyResolver 集合
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private final Map<Class<? extends IdempotentKeyResolver>, IdempotentKeyResolver> keyResolvers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private final IdempotentRedisDAO idempotentRedisDAO;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public IdempotentAspect(List<IdempotentKeyResolver> keyResolvers, IdempotentRedisDAO idempotentRedisDAO) {
 | 
				
			||||||
 | 
					        this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass);
 | 
				
			||||||
 | 
					        this.idempotentRedisDAO = idempotentRedisDAO;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Before("@annotation(idempotent)")
 | 
				
			||||||
 | 
					    public void beforePointCut(JoinPoint joinPoint, Idempotent idempotent) {
 | 
				
			||||||
 | 
					        // 获得 IdempotentKeyResolver
 | 
				
			||||||
 | 
					        IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver());
 | 
				
			||||||
 | 
					        Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver");
 | 
				
			||||||
 | 
					        // 解析 Key
 | 
				
			||||||
 | 
					        String key = keyResolver.resolver(joinPoint, idempotent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // 锁定 Key。
 | 
				
			||||||
 | 
					        boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit());
 | 
				
			||||||
 | 
					        // 锁定失败,抛出异常
 | 
				
			||||||
 | 
					        if (!success) {
 | 
				
			||||||
 | 
					            log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs());
 | 
				
			||||||
 | 
					            throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					package cn.iocoder.dashboard.framework.idempotent.core.keyresolver;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.hutool.core.util.StrUtil;
 | 
				
			||||||
 | 
					import cn.hutool.crypto.SecureUtil;
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
 | 
				
			||||||
 | 
					import org.aspectj.lang.JoinPoint;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 默认幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * 为了避免 Key 过长,使用 MD5 进行“压缩”
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @author 芋道源码
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
 | 
				
			||||||
 | 
					        String methodName = joinPoint.getSignature().toString();
 | 
				
			||||||
 | 
					        String argsStr = StrUtil.join(",", joinPoint.getArgs());
 | 
				
			||||||
 | 
					        return SecureUtil.md5(methodName + argsStr);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					package cn.iocoder.dashboard.framework.idempotent.core.keyresolver;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
 | 
				
			||||||
 | 
					import org.aspectj.lang.JoinPoint;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 基于 Spring EL 表达式,
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @author 芋道源码
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @Override
 | 
				
			||||||
 | 
					    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
 | 
				
			||||||
 | 
					        // TODO 稍后实现
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					package cn.iocoder.dashboard.framework.idempotent.core.keyresolver;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
 | 
				
			||||||
 | 
					import org.aspectj.lang.JoinPoint;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 幂等 Key 解析器接口
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @author 芋道源码
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					public interface IdempotentKeyResolver {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * 解析一个 Key
 | 
				
			||||||
 | 
					     *
 | 
				
			||||||
 | 
					     * @param idempotent 幂等注解
 | 
				
			||||||
 | 
					     * @param joinPoint  AOP 切面
 | 
				
			||||||
 | 
					     * @return Key
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    String resolver(JoinPoint joinPoint, Idempotent idempotent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					package cn.iocoder.dashboard.framework.idempotent.core.redis;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine;
 | 
				
			||||||
 | 
					import lombok.AllArgsConstructor;
 | 
				
			||||||
 | 
					import org.springframework.data.redis.core.StringRedisTemplate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import java.util.concurrent.TimeUnit;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import static cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine.KeyTypeEnum.STRING;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 幂等 Redis DAO
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @author 芋道源码
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@AllArgsConstructor
 | 
				
			||||||
 | 
					public class IdempotentRedisDAO {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static final RedisKeyDefine IDEMPOTENT = new RedisKeyDefine("幂等操作",
 | 
				
			||||||
 | 
					            "idempotent:%s", // 参数为 uuid
 | 
				
			||||||
 | 
					            STRING, String.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private final StringRedisTemplate redisTemplate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) {
 | 
				
			||||||
 | 
					        String redisKey = formatKey(key);
 | 
				
			||||||
 | 
					        return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static String formatKey(String key) {
 | 
				
			||||||
 | 
					        return String.format(IDEMPOTENT.getKeyTemplate(), key);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 幂等组件,参考 https://github.com/it4alla/idempotent 项目实现
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					package cn.iocoder.dashboard.framework.idempotent;
 | 
				
			||||||
@@ -9,6 +9,11 @@ import java.lang.annotation.Retention;
 | 
				
			|||||||
import java.lang.annotation.RetentionPolicy;
 | 
					import java.lang.annotation.RetentionPolicy;
 | 
				
			||||||
import java.lang.annotation.Target;
 | 
					import java.lang.annotation.Target;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * 操作日志注解
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * @author 芋道源码
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
@Target({ElementType.METHOD})
 | 
					@Target({ElementType.METHOD})
 | 
				
			||||||
@Retention(RetentionPolicy.RUNTIME)
 | 
					@Retention(RetentionPolicy.RUNTIME)
 | 
				
			||||||
public @interface OperateLog {
 | 
					public @interface OperateLog {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
 | 
				
			|||||||
import cn.iocoder.dashboard.common.pojo.CommonResult;
 | 
					import cn.iocoder.dashboard.common.pojo.CommonResult;
 | 
				
			||||||
import cn.iocoder.dashboard.common.pojo.PageResult;
 | 
					import cn.iocoder.dashboard.common.pojo.PageResult;
 | 
				
			||||||
import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
 | 
					import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
 | 
				
			||||||
 | 
					import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
 | 
				
			||||||
import cn.iocoder.dashboard.modules.infra.controller.config.vo.*;
 | 
					import cn.iocoder.dashboard.modules.infra.controller.config.vo.*;
 | 
				
			||||||
import cn.iocoder.dashboard.modules.infra.convert.config.InfConfigConvert;
 | 
					import cn.iocoder.dashboard.modules.infra.convert.config.InfConfigConvert;
 | 
				
			||||||
import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO;
 | 
					import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO;
 | 
				
			||||||
@@ -91,7 +92,7 @@ public class InfConfigController {
 | 
				
			|||||||
    @PostMapping("/create")
 | 
					    @PostMapping("/create")
 | 
				
			||||||
//    @PreAuthorize("@ss.hasPermi('infra:config:add')")
 | 
					//    @PreAuthorize("@ss.hasPermi('infra:config:add')")
 | 
				
			||||||
//    @Log(title = "参数管理", businessType = BusinessType.INSERT)
 | 
					//    @Log(title = "参数管理", businessType = BusinessType.INSERT)
 | 
				
			||||||
//    @RepeatSubmit
 | 
					    @Idempotent(timeout = 10)
 | 
				
			||||||
    public CommonResult<Long> createConfig(@Validated @RequestBody InfConfigCreateReqVO reqVO) {
 | 
					    public CommonResult<Long> createConfig(@Validated @RequestBody InfConfigCreateReqVO reqVO) {
 | 
				
			||||||
        return success(configService.createConfig(reqVO));
 | 
					        return success(configService.createConfig(reqVO));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -100,7 +101,8 @@ public class InfConfigController {
 | 
				
			|||||||
    @PutMapping("/update")
 | 
					    @PutMapping("/update")
 | 
				
			||||||
//    @PreAuthorize("@ss.hasPermi('infra:config:edit')")
 | 
					//    @PreAuthorize("@ss.hasPermi('infra:config:edit')")
 | 
				
			||||||
//    @Log(title = "参数管理", businessType = BusinessType.UPDATE)
 | 
					//    @Log(title = "参数管理", businessType = BusinessType.UPDATE)
 | 
				
			||||||
    public CommonResult<Boolean> edit(@Validated @RequestBody InfConfigUpdateReqVO reqVO) {
 | 
					    @Idempotent(timeout = 60)
 | 
				
			||||||
 | 
					    public CommonResult<Boolean> updateConfig(@Validated @RequestBody InfConfigUpdateReqVO reqVO) {
 | 
				
			||||||
        configService.updateConfig(reqVO);
 | 
					        configService.updateConfig(reqVO);
 | 
				
			||||||
        return success(true);
 | 
					        return success(true);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,12 +4,14 @@ import io.swagger.annotations.ApiModel;
 | 
				
			|||||||
import io.swagger.annotations.ApiModelProperty;
 | 
					import io.swagger.annotations.ApiModelProperty;
 | 
				
			||||||
import lombok.Data;
 | 
					import lombok.Data;
 | 
				
			||||||
import lombok.EqualsAndHashCode;
 | 
					import lombok.EqualsAndHashCode;
 | 
				
			||||||
 | 
					import lombok.ToString;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import javax.validation.constraints.NotNull;
 | 
					import javax.validation.constraints.NotNull;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ApiModel("参数配置创建 Request VO")
 | 
					@ApiModel("参数配置创建 Request VO")
 | 
				
			||||||
@Data
 | 
					@Data
 | 
				
			||||||
@EqualsAndHashCode(callSuper = true)
 | 
					@EqualsAndHashCode(callSuper = true)
 | 
				
			||||||
 | 
					@ToString(callSuper = true)
 | 
				
			||||||
public class InfConfigUpdateReqVO extends InfConfigBaseVO {
 | 
					public class InfConfigUpdateReqVO extends InfConfigBaseVO {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @ApiModelProperty(value = "参数配置序号", required = true, example = "1024")
 | 
					    @ApiModelProperty(value = "参数配置序号", required = true, example = "1024")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,7 @@ import static cn.iocoder.dashboard.framework.redis.core.RedisKeyDefine.KeyTypeEn
 | 
				
			|||||||
 *
 | 
					 *
 | 
				
			||||||
 * @author 芋道源码
 | 
					 * @author 芋道源码
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
public interface RedisKeyConstants {
 | 
					public interface SysRedisKeyConstants {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    RedisKeyDefine LOGIN_USER = new RedisKeyDefine("登陆用户的缓存",
 | 
					    RedisKeyDefine LOGIN_USER = new RedisKeyDefine("登陆用户的缓存",
 | 
				
			||||||
            "login_user:%s", // 参数为 sessionId
 | 
					            "login_user:%s", // 参数为 sessionId
 | 
				
			||||||
@@ -7,7 +7,7 @@ import org.springframework.stereotype.Repository;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import javax.annotation.Resource;
 | 
					import javax.annotation.Resource;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import static cn.iocoder.dashboard.modules.system.dal.redis.RedisKeyConstants.LOGIN_USER;
 | 
					import static cn.iocoder.dashboard.modules.system.dal.redis.SysRedisKeyConstants.LOGIN_USER;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * {@link LoginUser} 的 RedisDAO
 | 
					 * {@link LoginUser} 的 RedisDAO
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ import org.springframework.stereotype.Repository;
 | 
				
			|||||||
import javax.annotation.Resource;
 | 
					import javax.annotation.Resource;
 | 
				
			||||||
import java.time.Duration;
 | 
					import java.time.Duration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import static cn.iocoder.dashboard.modules.system.dal.redis.RedisKeyConstants.CAPTCHA_CODE;
 | 
					import static cn.iocoder.dashboard.modules.system.dal.redis.SysRedisKeyConstants.CAPTCHA_CODE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 验证码的 Redis DAO
 | 
					 * 验证码的 Redis DAO
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -98,6 +98,8 @@ spring:
 | 
				
			|||||||
      # Spring Boot Admin Client 客户端的相关配置
 | 
					      # Spring Boot Admin Client 客户端的相关配置
 | 
				
			||||||
      client:
 | 
					      client:
 | 
				
			||||||
        url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # 设置 Spring Boot Admin Server 地址
 | 
					        url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # 设置 Spring Boot Admin Server 地址
 | 
				
			||||||
 | 
					        instance:
 | 
				
			||||||
 | 
					          prefer-ip: true # 注册实例时,优先使用 IP
 | 
				
			||||||
      # Spring Boot Admin Server 服务端的相关配置
 | 
					      # Spring Boot Admin Server 服务端的相关配置
 | 
				
			||||||
      context-path: /admin # 配置 Spring
 | 
					      context-path: /admin # 配置 Spring
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -99,7 +99,7 @@ spring:
 | 
				
			|||||||
      client:
 | 
					      client:
 | 
				
			||||||
        url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # 设置 Spring Boot Admin Server 地址
 | 
					        url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # 设置 Spring Boot Admin Server 地址
 | 
				
			||||||
        instance:
 | 
					        instance:
 | 
				
			||||||
          prefer-ip: true
 | 
					          prefer-ip: true # 注册实例时,优先使用 IP
 | 
				
			||||||
      # Spring Boot Admin Server 服务端的相关配置
 | 
					      # Spring Boot Admin Server 服务端的相关配置
 | 
				
			||||||
      context-path: /admin # 配置 Spring
 | 
					      context-path: /admin # 配置 Spring
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user