From cead6aca6e85d834b05f5586bad7a6d91cd6aaed Mon Sep 17 00:00:00 2001
From: sxu <602087911@qq.com>
Date: Fri, 27 Sep 2024 12:59:44 +0800
Subject: [PATCH] PreventRepeatSubmit
---
bonus-common-biz/pom.xml | 4 +
.../bonus/common/biz/enums/HttpCodeEnum.java | 38 +++
.../biz/exception/BusinessException.java | 26 ++
bonus-modules/bonus-material/pom.xml | 6 +
.../annotation/PreventRepeatSubmit.java | 24 ++
.../aspect/PreventRepeatSubmitAspect.java | 70 +++++
.../basic/controller/BmConfigController.java | 2 +
.../com/bonus/material/utils/RedisCache.java | 269 ++++++++++++++++++
8 files changed, 439 insertions(+)
create mode 100644 bonus-common-biz/src/main/java/com/bonus/common/biz/enums/HttpCodeEnum.java
create mode 100644 bonus-common-biz/src/main/java/com/bonus/common/biz/exception/BusinessException.java
create mode 100644 bonus-modules/bonus-material/src/main/java/com/bonus/material/annotation/PreventRepeatSubmit.java
create mode 100644 bonus-modules/bonus-material/src/main/java/com/bonus/material/aspect/PreventRepeatSubmitAspect.java
create mode 100644 bonus-modules/bonus-material/src/main/java/com/bonus/material/utils/RedisCache.java
diff --git a/bonus-common-biz/pom.xml b/bonus-common-biz/pom.xml
index d658be94..0933fb21 100644
--- a/bonus-common-biz/pom.xml
+++ b/bonus-common-biz/pom.xml
@@ -172,6 +172,10 @@
com.bonus
bonus-common-core
+
+ org.springframework.data
+ spring-data-redis
+
diff --git a/bonus-common-biz/src/main/java/com/bonus/common/biz/enums/HttpCodeEnum.java b/bonus-common-biz/src/main/java/com/bonus/common/biz/enums/HttpCodeEnum.java
new file mode 100644
index 00000000..df53128d
--- /dev/null
+++ b/bonus-common-biz/src/main/java/com/bonus/common/biz/enums/HttpCodeEnum.java
@@ -0,0 +1,38 @@
+package com.bonus.common.biz.enums;
+
+public enum HttpCodeEnum {
+ // 成功
+ SUCCESS(200, "操作成功"),
+ // 登录
+ NEED_LOGIN(401, "需要登录后操作"),
+ NO_OPERATOR_AUTH(403, "无权限操作"),
+ SYSTEM_ERROR(500, "出现错误"),
+ USERNAME_EXIST(501, "用户名已存在"),
+ PHONENUMBER_EXIST(502, "手机号已存在"),
+ EMAIL_EXIST(503, "邮箱已存在"),
+ REQUIRE_USERNAME(504, "必需填写用户名"),
+ CONTENT_NOT_NULL(506, "评论内容不能为空"),
+ FILE_TYPE_ERROR(507, "文件类型错误"),
+ USERNAME_NOT_NULL(508, "用户名不能为空"),
+ NICKNAME_NOT_NULL(509, "昵称不能为空"),
+ PASSWORD_NOT_NULL(510, "密码不能为空"),
+ EMAIL_NOT_NULL(511, "邮箱不能为空"),
+ NICKNAME_EXIST(512, "昵称已存在"),
+ LOGIN_ERROR(505, "用户名或密码错误"),
+ REPEATE_ERROR(600, "不允许重复提交,请稍候再试");
+ int code;
+ String msg;
+
+ HttpCodeEnum(int code, String errorMessage) {
+ this.code = code;
+ this.msg = errorMessage;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public String getMsg() {
+ return msg;
+ }
+}
diff --git a/bonus-common-biz/src/main/java/com/bonus/common/biz/exception/BusinessException.java b/bonus-common-biz/src/main/java/com/bonus/common/biz/exception/BusinessException.java
new file mode 100644
index 00000000..efc04833
--- /dev/null
+++ b/bonus-common-biz/src/main/java/com/bonus/common/biz/exception/BusinessException.java
@@ -0,0 +1,26 @@
+package com.bonus.common.biz.exception;
+
+
+import com.bonus.common.biz.enums.HttpCodeEnum;
+
+public class BusinessException extends RuntimeException {
+
+ private int code;
+ //使用枚举构造
+ public BusinessException(HttpCodeEnum httpCodeEnum){
+ super(httpCodeEnum.getMsg());
+ this.code=httpCodeEnum.getCode();
+ }
+ //使用自定义消息体
+ public BusinessException(HttpCodeEnum httpCodeEnum, String msg){
+ super(msg);
+ this.code=httpCodeEnum.getCode();
+ }
+
+ //根据异常构造
+ public BusinessException(HttpCodeEnum httpCodeEnum, Throwable msg){
+ super(msg);
+ this.code=httpCodeEnum.getCode();
+ }
+
+}
diff --git a/bonus-modules/bonus-material/pom.xml b/bonus-modules/bonus-material/pom.xml
index 3988d818..ec196475 100644
--- a/bonus-modules/bonus-material/pom.xml
+++ b/bonus-modules/bonus-material/pom.xml
@@ -94,6 +94,12 @@
org.springframework.boot
spring-boot-starter-mail
+
+ com.bonus
+ bonus-common-biz
+ 24.8.0
+ compile
+
diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/annotation/PreventRepeatSubmit.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/annotation/PreventRepeatSubmit.java
new file mode 100644
index 00000000..c43f8657
--- /dev/null
+++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/annotation/PreventRepeatSubmit.java
@@ -0,0 +1,24 @@
+package com.bonus.material.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 自定义注解防止表单重复提交
+ *
+ */
+@Inherited
+@Target({ElementType.METHOD, ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface PreventRepeatSubmit
+{
+ /**
+ * 间隔时间(s),小于此时间视为重复提交
+ */
+ public int interval() default 5;
+
+ /**
+ * 提示消息
+ */
+ public String message() default "不允许重复提交,请稍候再试";
+}
diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/aspect/PreventRepeatSubmitAspect.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/aspect/PreventRepeatSubmitAspect.java
new file mode 100644
index 00000000..067b928c
--- /dev/null
+++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/aspect/PreventRepeatSubmitAspect.java
@@ -0,0 +1,70 @@
+package com.bonus.material.aspect;
+
+import com.alibaba.fastjson2.JSON;
+import com.bonus.common.core.exception.BusinessException;
+import com.bonus.common.core.utils.HttpCodeEnum;
+import com.bonus.material.annotation.PreventRepeatSubmit;
+import com.bonus.material.utils.RedisCache;
+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.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.util.concurrent.TimeUnit;
+
+@Aspect
+@Component
+public class PreventRepeatSubmitAspect {
+ private static final Logger LOG = LoggerFactory.getLogger(PreventRepeatSubmitAspect.class);
+ private static final String header = "Authorization";
+
+ @Autowired
+ private RedisCache redisCache;
+
+ // 定义一个切入点
+ @Pointcut("@annotation(com.bonus.material.annotation.PreventRepeatSubmit)")
+ public void preventRepeatSubmit() {
+
+ }
+
+ @Around("preventRepeatSubmit()")
+ public Object checkPrs(ProceedingJoinPoint pjp) throws Throwable {
+ LOG.info("进入preventRepeatSubmit切面");
+ //得到request对象
+ HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
+ String requestURI = request.getRequestURI();
+ LOG.info("防重复提交的请求地址:{} ,请求方式:{}",requestURI,request.getMethod());
+ LOG.info("防重复提交拦截到的类名:{} ,方法:{}",pjp.getTarget().getClass().getSimpleName(),pjp.getSignature().getName());
+
+ //获取请求参数
+ Object[] args = pjp.getArgs();
+ String argStr = JSON.toJSONString(args);
+ //这里替换是为了在redis可视化工具中方便查看
+ argStr=argStr.replace(":","#");
+ // 唯一值(没有消息头则使用请求地址)
+ String submitKey = request.getHeader(header).trim();
+ // 唯一标识(指定key + url +参数+token)
+ String cacheRepeatKey = "repeat_submit:" + requestURI+":" +argStr+":"+ submitKey;
+ MethodSignature ms = (MethodSignature) pjp.getSignature();
+ Method method=ms.getMethod();
+ PreventRepeatSubmit preventRepeatSubmit=method.getAnnotation(PreventRepeatSubmit.class);
+ int interval = preventRepeatSubmit.interval();
+ LOG.info("获取到preventRepeatSubmit的有效期时间"+interval);
+ //redis分布式锁
+ Boolean aBoolean = redisCache.setNxCacheObject(cacheRepeatKey, 1, preventRepeatSubmit.interval(), TimeUnit.SECONDS);
+ //aBoolean为true则证明没有重复提交
+ if(!aBoolean){
+ throw new BusinessException(HttpCodeEnum.REPEATE_ERROR);
+ }
+ return pjp.proceed();
+ }
+
+}
diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/basic/controller/BmConfigController.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/basic/controller/BmConfigController.java
index 4e7e4d00..b9d5b97c 100644
--- a/bonus-modules/bonus-material/src/main/java/com/bonus/material/basic/controller/BmConfigController.java
+++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/basic/controller/BmConfigController.java
@@ -3,6 +3,7 @@ package com.bonus.material.basic.controller;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import com.bonus.common.log.enums.OperaType;
+import com.bonus.material.annotation.PreventRepeatSubmit;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
@@ -41,6 +42,7 @@ public class BmConfigController extends BaseController
* 查询功能参数配置列表
*/
@ApiOperation(value = "查询功能参数配置列表")
+ @PreventRepeatSubmit
@RequiresPermissions("basic:config:list")
@GetMapping("/list")
public TableDataInfo list(BmConfig bmConfig)
diff --git a/bonus-modules/bonus-material/src/main/java/com/bonus/material/utils/RedisCache.java b/bonus-modules/bonus-material/src/main/java/com/bonus/material/utils/RedisCache.java
new file mode 100644
index 00000000..c84f4119
--- /dev/null
+++ b/bonus-modules/bonus-material/src/main/java/com/bonus/material/utils/RedisCache.java
@@ -0,0 +1,269 @@
+package com.bonus.material.utils;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.BoundSetOperations;
+import org.springframework.data.redis.core.HashOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * spring redis 工具类
+ *
+ **/
+@Component
+public class RedisCache
+{
+ @Autowired
+ public RedisTemplate redisTemplate;
+
+ //添加分布式锁
+ public Boolean setNxCacheObject(final String key, final T value,long lt,TimeUnit tu)
+ {
+ return redisTemplate.opsForValue().setIfAbsent(key,value,lt,tu);
+ }
+
+ /**
+ * 缓存基本的对象,Integer、String、实体类等
+ *
+ * @param key 缓存的键值
+ * @param value 缓存的值
+ */
+ public void setCacheObject(final String key, final T value)
+ {
+ redisTemplate.opsForValue().set(key, value);
+ }
+
+ /**
+ * 缓存基本的对象,Integer、String、实体类等
+ *
+ * @param key 缓存的键值
+ * @param value 缓存的值
+ * @param timeout 时间
+ * @param timeUnit 时间颗粒度
+ */
+ public void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
+ {
+ redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
+ }
+
+ /**
+ * 设置有效时间
+ *
+ * @param key Redis键
+ * @param timeout 超时时间
+ * @return true=设置成功;false=设置失败
+ */
+ public boolean expire(final String key, final long timeout)
+ {
+ return expire(key, timeout, TimeUnit.SECONDS);
+ }
+
+ /**
+ * 设置有效时间
+ *
+ * @param key Redis键
+ * @param timeout 超时时间
+ * @param unit 时间单位
+ * @return true=设置成功;false=设置失败
+ */
+ public boolean expire(final String key, final long timeout, final TimeUnit unit)
+ {
+ return redisTemplate.expire(key, timeout, unit);
+ }
+
+ /**
+ * 获取有效时间
+ *
+ * @param key Redis键
+ * @return 有效时间
+ */
+ public long getExpire(final String key)
+ {
+ return redisTemplate.getExpire(key);
+ }
+
+ /**
+ * 判断 key是否存在
+ *
+ * @param key 键
+ * @return true 存在 false不存在
+ */
+ public Boolean hasKey(String key)
+ {
+ return redisTemplate.hasKey(key);
+ }
+
+ /**
+ * 获得缓存的基本对象。
+ *
+ * @param key 缓存键值
+ * @return 缓存键值对应的数据
+ */
+ public T getCacheObject(final String key)
+ {
+ ValueOperations operation = redisTemplate.opsForValue();
+ return operation.get(key);
+ }
+
+ /**
+ * 删除单个对象
+ *
+ * @param key
+ */
+ public boolean deleteObject(final String key)
+ {
+ return redisTemplate.delete(key);
+ }
+
+ /**
+ * 删除集合对象
+ *
+ * @param collection 多个对象
+ * @return
+ */
+ public boolean deleteObject(final Collection collection)
+ {
+ return redisTemplate.delete(collection) > 0;
+ }
+
+ /**
+ * 缓存List数据
+ *
+ * @param key 缓存的键值
+ * @param dataList 待缓存的List数据
+ * @return 缓存的对象
+ */
+ public long setCacheList(final String key, final List dataList)
+ {
+ Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
+ return count == null ? 0 : count;
+ }
+
+ /**
+ * 获得缓存的list对象
+ *
+ * @param key 缓存的键值
+ * @return 缓存键值对应的数据
+ */
+ public List getCacheList(final String key)
+ {
+ return redisTemplate.opsForList().range(key, 0, -1);
+ }
+
+ /**
+ * 缓存Set
+ *
+ * @param key 缓存键值
+ * @param dataSet 缓存的数据
+ * @return 缓存数据的对象
+ */
+ public BoundSetOperations setCacheSet(final String key, final Set dataSet)
+ {
+ BoundSetOperations setOperation = redisTemplate.boundSetOps(key);
+ Iterator it = dataSet.iterator();
+ while (it.hasNext())
+ {
+ setOperation.add(it.next());
+ }
+ return setOperation;
+ }
+
+ /**
+ * 获得缓存的set
+ *
+ * @param key
+ * @return
+ */
+ public Set getCacheSet(final String key)
+ {
+ return redisTemplate.opsForSet().members(key);
+ }
+
+ /**
+ * 缓存Map
+ *
+ * @param key
+ * @param dataMap
+ */
+ public void setCacheMap(final String key, final Map dataMap)
+ {
+ if (dataMap != null) {
+ redisTemplate.opsForHash().putAll(key, dataMap);
+ }
+ }
+
+ /**
+ * 获得缓存的Map
+ *
+ * @param key
+ * @return
+ */
+ public Map getCacheMap(final String key)
+ {
+ return redisTemplate.opsForHash().entries(key);
+ }
+
+ /**
+ * 往Hash中存入数据
+ *
+ * @param key Redis键
+ * @param hKey Hash键
+ * @param value 值
+ */
+ public void setCacheMapValue(final String key, final String hKey, final T value)
+ {
+ redisTemplate.opsForHash().put(key, hKey, value);
+ }
+
+ /**
+ * 获取Hash中的数据
+ *
+ * @param key Redis键
+ * @param hKey Hash键
+ * @return Hash中的对象
+ */
+ public T getCacheMapValue(final String key, final String hKey)
+ {
+ HashOperations opsForHash = redisTemplate.opsForHash();
+ return opsForHash.get(key, hKey);
+ }
+
+ /**
+ * 获取多个Hash中的数据
+ *
+ * @param key Redis键
+ * @param hKeys Hash键集合
+ * @return Hash对象集合
+ */
+ public List getMultiCacheMapValue(final String key, final Collection