表单重复提交,我认为和幂等还是有一点区别的。

这里要防止的就是表单重复提交,比如前端没做loading而你手速快点了多次,网络重试等。

某些操作不做防护其实还是有问题的,比如项目里面批量生成兑换码,一批生成10000个,手一抖生成了2万个。

思路:

  URI+userId+参数 ,做一下md5哈希,作为key,放到redis打标记,value值可以随意,并设置有效期,比如3秒。

统一在aop做处理,自定义注解支持不同接口自定义延迟时间,因为每个业务要求不同。

代码:

注解

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {

    /**
     * 延迟多少秒后可以重复提交
     * @return
     */
    long delaySecond() default 1;
}

Aspect切面

package com.xdf.ucan.web.aop;


import cn.hutool.crypto.digest.MD5;
import com.alibaba.fastjson.JSONArray;
import com.xdf.ucan.service.response.Result;
import com.xdf.ucan.service.utils.RedisKeyUtil;
import com.xdf.ucan.service.utils.RequestWebUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
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.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @author lihaoyang
 * @date 2022/2/18
 */
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {


    @Autowired
    private HttpServletRequest request;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    @Pointcut("execution(public * com.xdf.ucan.web.controller..*.*(..))")
    public void controllerPoint() {
    }

    @Around("controllerPoint()")
    public Object doControllerPointAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //获取注解
        RepeatSubmit repeatSubmit = AnnotationUtils.findAnnotation(method, RepeatSubmit.class);
        if (Objects.isNull(repeatSubmit)) {
            return joinPoint.proceed();
        }
        //延迟秒数
        long delaySecond = repeatSubmit.delaySecond();
        //请求参数
        Object[] args = joinPoint.getArgs();
        String accountId = RequestWebUtil.getAccountId();
        String lockKeyParam = request.getRequestURI() + "/" + accountId;
        if (ArrayUtils.isNotEmpty(args)) {
            List<Object> argsList = Arrays.asList(args);
            JSONArray jsonArray = new JSONArray(argsList);
            lockKeyParam += jsonArray.toJSONString();
        }
        String lockKey = RedisKeyUtil.getRepeatSubmitKey(MD5.create().digestHex(lockKeyParam));
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, accountId, delaySecond, TimeUnit.SECONDS);
        if (!isLock) {
            //请稍后执行
            log.info("重复提交,lockKey:{} ", lockKey);
            return Result.error("重复提交,请"+delaySecond+"秒后重试");
        }
        return joinPoint.proceed();
    }
}

使用

/**
     * 生成商品兑换码
     *
     * @param generateReq
     * @return
     */
    @RepeatSubmit(delaySecond = 5)
    @PostMapping("generate")
    public Result generate(@Valid @RequestBody ExchangeCodeGenerateReq generateReq) {
        generateReq.setAccountId(RequestWebUtil.getAccountId());
        log.info("生成兑换码,参数:{}", JSONUtil.toJsonStr(generateReq));
        String batchId = "";
        try {
            batchId = exchangeCodeService.batchGenerate(generateReq);
        } catch (Exception e) {
            return Result.error("生成失败,请重试");
        }
        return Result.OK(batchId);
    }

 

 

Redis打的标记

 

 

 

SECONDS