SpringBoot 防刷 重复提交问题 重复点击问题 注解 RequestParam RequestBody

发布于:2025-06-28 ⋅ 阅读:(15) ⋅ 点赞:(0)

下订单要调用第三方接口,操作比较久,用户很容易点击两次,前端后端要做防刷处理

注解

package net.digital.smart.config;

import java.lang.annotation.*;

/**
 * 用于防刷限流的注解
 * 默认是5秒内只能调用一次
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {

    /**
     * 限流的key
     */
    String key() default "RateLimit:";

    /**
     * 周期,单位是秒
     */
    int cycle() default 5;

    /**
     * 默认提示信息
     */
    String msg() default "正在处理中,请您稍微歇一会。";


    /**
     * 请求参数名称,用于获取请求中的参数值 RequestParam为参数index,RequestBody为参数名
     */
    String paramName() default "";


    /**
     * 请求参数类型
     */
    String paramType() default "";
}

实现类

package net.digital.smart.config;

import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import net.digital.common.core.exception.BizErrorException;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
@Slf4j
public class RateLimitInterceptor {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;


    @Before("@annotation(net.digital.smart.config.RateLimit)")
    public void before(JoinPoint point) {
        // 获取方法上的RateLimit注解
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        RateLimit rateLimit = method.getAnnotation(RateLimit.class);
        if (rateLimit == null) {
            return;
        }
        try {
            // 生成限流key,格式为:类名+方法名+IP+自定义key+参数值
            String key = generateKey(rateLimit, method, point.getArgs());
            log.info("接口防刷key:" + key);
            // 检查是否超过限流次数
            if (!tryAcquire(key, rateLimit.cycle())) {
                throw new BizErrorException(rateLimit.msg());
            }
        } catch (Exception e) {
            throw new BizErrorException(e.getMessage());
        }


    }

    private String generateKey(RateLimit rateLimit, Method method, Object[] args) throws IOException {
        StringBuilder keyBuilder = new StringBuilder();
        String paramValue = getParameterValue(args, rateLimit.paramName(),rateLimit.paramType());
        keyBuilder.append(rateLimit.key())
                .append(paramValue)
                .append(":")
                .append(method.getDeclaringClass().getName())
                .append(":")
                .append(method.getName());

        return keyBuilder.toString();
    }

    private boolean tryAcquire(String key, int cycle) {
        // 使用Redis的setIfAbsent实现原子操作
        Boolean success = redisTemplate.opsForValue().setIfAbsent(key, 1, cycle, TimeUnit.SECONDS);
        return success != null && success;
    }

    private String getParameterValue(Object[] args, String paramName,String paramType) {
        if (ArrayUtil.isEmpty(args)) {
            throw new BizErrorException("获取防刷key失败");
        }
        String argsStr = JSONUtil.toJsonStr(args);
        log.info("获取防刷key-argsStr=>{}",argsStr);
        if (StrUtil.isBlank(argsStr)) {
            throw new BizErrorException("获取防刷key失败");
        }
        JSONArray jsonArray = JSONObject.parseArray(argsStr);
        if (jsonArray.isEmpty()){
            throw new BizErrorException("获取防刷key失败");
        }
        if (StrUtil.equals(paramType,"RequestBody")){
            JSONObject jsonObject = jsonArray.getJSONObject(0);
            if (ObjectUtil.isEmpty(jsonObject)) {
                throw new BizErrorException("获取防刷key失败");
            }
            log.info("防刷jsonObject=>{}", JSON.toJSONString(jsonObject));
            String uuid = jsonObject.getString(paramName);
            if (StrUtil.isNotBlank(uuid)) {
                return uuid;
            }
            throw new BizErrorException("获取防刷key失败");
        } else if ((StrUtil.equals(paramType, "RequestParam"))) {
            for (int i = 0; i < jsonArray.size(); i++) {
                if (StrUtil.equals(String.valueOf(i), paramName)) {
                    return jsonArray.getString(i);
                }
            }
        }
        throw new BizErrorException("获取防刷key失败");
    }
}

使用方法

    /**
     * 锁号|预约挂号
     */
    @RateLimit(paramName = "contactId",paramType = "RequestBody")
    @PostMapping("/order/create")
    @Operation(description = "锁号|预约挂号", summary = "锁号|预约挂号")
    @PreAuthorize("@pms.hasPermission('pms_member_biz')")
    public R createOrder(@Validated @RequestBody RegOrderDto regOrderDto) {
        if (ObjectUtil.isEmpty(regOrderDto.getDeptId())) {
            throw new BizErrorException("deptId不能为空");
        }
        if (StrUtil.isEmpty(regOrderDto.getAvailableNumStr())) {
            throw new BizErrorException("availableNumStr不能为空");
        }
        RegisteredHistory order = registeredService.createOrder(regOrderDto);
        if (null == order) {
            throw new BizErrorException(ErrorEnum.REG_APPLY_FAILED);
        }

        // 同步挂号信息到第三方

        return R.ok(order);
    }


 /**
     * 取消锁号|取消预约订单
     */
    @RateLimit(paramName = "1",paramType = "RequestParam")
    @PostMapping("/order/cancel")
    @Operation(description = "取消锁号|取消预约订单", summary = "取消锁号|取消预约订单")
    @PreAuthorize("@pms.hasPermission('pms_member_biz')")
    public R cancelOrder(@RequestParam(value = "regId") Long regId,
                         @RequestParam(value = "contactId", required = false) Long contactId) {
        if (null == contactId || null == regId) {
            throw new BizErrorException(ErrorEnum.SYSTEM_VARIABLE_INVALID);
        }
        Long mechanismId = RequestCtx.getHeader(request).getMechanismId();
        CommonCardInfo commonCardInfo = getCommonCardInfo(SecurityUtils.getUser().getId(), contactId, mechanismId);

        boolean cancel = smtRegisteredRecordService.cancel(regId, CancelTypeEnum.USER_CANCEL.getType(), commonCardInfo.getHisPatientId());
        if (cancel) {
            return R.ok();
        }
        return R.failed();
    }

网站公告

今日签到

点亮在社区的每一天
去签到