当前位置: 首页 > news >正文

redis + 拦截器 :防止数据重复提交

1.项目用到,不是核心

我们干系统开发,不免要考虑一个点,数据的重复提交。

我想我们之前如果要校验数据重复提交要求,会怎么干?会在业务层,对数据库操作,查询数据是否存在,存在就禁止插入数据; 但是吧,我们每次crud操作都会连接一次数据库,也就是占用内存,那么在项目中大量crud操作面前,我们通过这种方式来实现数据的重复提交,显然不大可取。因此我们采用通过 redis + 拦截器来实现防止数据重复提交。来分担数据库连接的压力。

数据重复提交有啥坏处?

  1. 数据完整性:如果用户在短时间内多次提交相同的表单,可能会导致数据重复或产生不一致的数据。
  2. 用户体验:如果用户不小心重复提交了表单,而系统没有进行相应的处理,用户可能会收到错误或重复的信息,这会影响用户体验。
  3. 性能考虑:大量的重复提交可能会对服务器造成不必要的负担,影响系统的性能。
  4. 安全考虑:在某些场景下,重复提交可能会被用于发起攻击,如DoS攻击。

 我们要考虑一个事情,就是我们要验证数据的重复提交: 首先第一次提交的数据肯定是要被存储的,当而第二次往后,每次提交数据都会与之前的数据产生比对从而验证数据重复提交,但是通常情况下我们不仅要对提交数据重复性校验,还有前后提交时间差的校验。

下面,就有我通过redis + 拦截器来实现如何防止数据重复提交。

思路: 我们对需要验证重复提交的数据,加上自定义注解限制提交时间段,然后在拦截器中读取第一次提交内容和时间点存储到redi中,当第二次提交时,会拿到新的数据和时间点与存储到redis对比。如果提交2次时间段小于限制提交时间段(拦截器拿到自定义注解的值),就算重复提交。

项目依赖

<dependencies><!--boot-web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--hutool--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.11</version><exclusions><exclusion><groupId>cn.hutool</groupId><artifactId>hutool-json</artifactId></exclusion></exclusions></dependency><!--fastJson2--><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.19.graal</version></dependency><!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--commons-pools连接池,lettuce没有内置的数据库连接池所以要用第三方的 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><!--boot-test--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>2.6.13</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>

application.yml

主要是redis的配置。

spring:# redis 配置redis:# 地址host: 192.168.233.131# 端口,默认为6379port: 6379# 数据库索引database: 0# 密码password:# 连接超时时间timeout: 10slettuce:pool:# 连接池中的最小空闲连接min-idle: 0# 连接池中的最大空闲连接max-idle: 8# 连接池的最大数据库连接数max-active: 8# #连接池最大阻塞等待时间(使用负值表示没有限制)max-wait: -1ms

FastJson2JsonRedisSerializer

主要负责对存入redis的key、value进行序列化。

/*** Redis使用FastJson序列化** @author jzm*/
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);private Class<T> clazz;public FastJson2JsonRedisSerializer(Class<T> clazz){super();this.clazz = clazz;}@Overridepublic byte[] serialize(T t) throws SerializationException{if (t == null){return new byte[0];}return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);}@Overridepublic T deserialize(byte[] bytes) throws SerializationException{if (bytes == null || bytes.length <= 0){return null;}String str = new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);}
}

RedisConfig

redis相关配置。定义通过redisTemplate,设置到redis中的key、value的序列化方式。

/*** redis配置** @author jzm*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport
{@Bean@SuppressWarnings(value = {"unchecked", "rawtypes"})// 设置key、value的序列化方式public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplate<Object, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}@Beanpublic DefaultRedisScript<Long> limitScript(){DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();redisScript.setScriptText(limitScriptText());redisScript.setResultType(Long.class);return redisScript;}/*** 限流脚本*/private String limitScriptText(){return "local key = KEYS[1]\n" +"local count = tonumber(ARGV[1])\n" +"local time = tonumber(ARGV[2])\n" +"local current = redis.call('get', key);\n" +"if current and tonumber(current) > count then\n" +"    return tonumber(current);\n" +"end\n" +"current = redis.call('incr', key)\n" +"if tonumber(current) == 1 then\n" +"    redis.call('expire', key, time)\n" +"end\n" +"return tonumber(current);";}
}

WebAppConfig

web mvc的相关配置。这里主要是注册自定义拦截器。

/*** web 配置** @author: jzm* @date: 2024-01-25 11:30**/@Configuration
public class WebAppConfig implements WebMvcConfigurer
{@Autowiredprivate SameUrlDataInterceptor sameUrlDataInterceptor;// 注册拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry){// 可添加多个registry.addInterceptor(sameUrlDataInterceptor).addPathPatterns("/**");}
}

FilterConfig

过滤器配置。注册自定义过滤器。

/*** 过滤器配置** @author: jzm* @date: 2024-01-26 08:53**/@Configuration
public class FilterConfig
{@Autowiredprivate RepeatableFilter repeatableFilter;@SuppressWarnings({"rawtypes", "unchecked"})@Beanpublic FilterRegistrationBean someFilterRegistration(){FilterRegistrationBean registration = new FilterRegistrationBean();registration.setFilter(repeatableFilter);registration.addUrlPatterns("/*");registration.setName("repeatableFilter");registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);return registration;}
}

项目用到的常量类

常量类。直接CV。

/*** 缓存的key 常量** @author jzm*/
public class CacheConstants
{/*** 登录用户 redis key*/public static final String LOGIN_TOKEN_KEY = "login_tokens:";/*** 验证码 redis key*/public static final String CAPTCHA_CODE_KEY = "captcha_codes:";/*** 参数管理 cache key*/public static final String SYS_CONFIG_KEY = "sys_config:";/*** 字典管理 cache key*/public static final String SYS_DICT_KEY = "sys_dict:";/*** 防重提交 redis key*/public static final String REPEAT_SUBMIT_KEY = "repeat_submit:";/*** 限流 redis key*/public static final String RATE_LIMIT_KEY = "rate_limit:";/*** 登录账户密码错误次数 redis key*/public static final String PWD_ERR_CNT_KEY = "pwd_err_cnt:";
}
/*** 通用常量信息** @author jzm*/
public class Constants
{/*** UTF-8 字符集*/public static final String UTF8 = "UTF-8";/*** GBK 字符集*/public static final String GBK = "GBK";/*** www主域*/public static final String WWW = "www.";/*** http请求*/public static final String HTTP = "http://";/*** https请求*/public static final String HTTPS = "https://";/*** 通用成功标识*/public static final String SUCCESS = "0";/*** 通用失败标识*/public static final String FAIL = "1";/*** 登录成功*/public static final String LOGIN_SUCCESS = "Success";/*** 注销*/public static final String LOGOUT = "Logout";/*** 注册*/public static final String REGISTER = "Register";/*** 登录失败*/public static final String LOGIN_FAIL = "Error";/*** 所有权限标识*/public static final String ALL_PERMISSION = "*:*:*";/*** 管理员角色权限标识*/public static final String SUPER_ADMIN = "admin";/*** 角色权限分隔符*/public static final String ROLE_DELIMETER = ",";/*** 权限标识分隔符*/public static final String PERMISSION_DELIMETER = ",";/*** 验证码有效期(分钟)*/public static final Integer CAPTCHA_EXPIRATION = 2;/*** 令牌*/public static final String TOKEN = "token";/*** 令牌前缀*/public static final String TOKEN_PREFIX = "Bearer ";/*** 令牌前缀*/public static final String LOGIN_USER_KEY = "login_user_key";/*** 用户ID*/public static final String JWT_USERID = "userid";/*** 用户名称*/public static final String JWT_USERNAME = "sub";/*** 用户头像*/public static final String JWT_AVATAR = "avatar";/*** 创建时间*/public static final String JWT_CREATED = "created";/*** 用户权限*/public static final String JWT_AUTHORITIES = "authorities";/*** 资源映射路径 前缀*/public static final String RESOURCE_PREFIX = "/profile";/*** RMI 远程方法调用*/public static final String LOOKUP_RMI = "rmi:";/*** LDAP 远程方法调用*/public static final String LOOKUP_LDAP = "ldap:";/*** LDAPS 远程方法调用*/public static final String LOOKUP_LDAPS = "ldaps:";/*** 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全)*/public static final String[] JSON_WHITELIST_STR = {"org.springframework", "com.ruoyi"};/*** 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)*/public static final String[] JOB_WHITELIST_STR = {"com.ruoyi"};/*** 定时任务违规的字符*/public static final String[] JOB_ERROR_STR = {"java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml","org.springframework", "org.apache", "com.ruoyi.common.utils.file", "com.ruoyi.common.config"};
}
/*** 返回状态码* * @author jzm*/
public class HttpStatus
{/*** 操作成功*/public static final int SUCCESS = 200;/*** 对象创建成功*/public static final int CREATED = 201;/*** 请求已经被接受*/public static final int ACCEPTED = 202;/*** 操作已经执行成功,但是没有返回数据*/public static final int NO_CONTENT = 204;/*** 资源已被移除*/public static final int MOVED_PERM = 301;/*** 重定向*/public static final int SEE_OTHER = 303;/*** 资源没有被修改*/public static final int NOT_MODIFIED = 304;/*** 参数列表错误(缺少,格式不匹配)*/public static final int BAD_REQUEST = 400;/*** 未授权*/public static final int UNAUTHORIZED = 401;/*** 访问受限,授权过期*/public static final int FORBIDDEN = 403;/*** 资源,服务未找到*/public static final int NOT_FOUND = 404;/*** 不允许的http方法*/public static final int BAD_METHOD = 405;/*** 资源冲突,或者资源被锁*/public static final int CONFLICT = 409;/*** 不支持的数据,媒体类型*/public static final int UNSUPPORTED_TYPE = 415;/*** 系统内部错误*/public static final int ERROR = 500;/*** 接口未实现*/public static final int NOT_IMPLEMENTED = 501;/*** 系统警告消息*/public static final int WARN = 601;
}

 项目用到的工具类

也是直接。CV。

RedisCache

/*** redis 工具类** @author jzm**/
@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisCache
{@Autowiredpublic RedisTemplate redisTemplate;/*** 缓存基本的对象,Integer、String、实体类等** @param key   缓存的键值* @param value 缓存的值*/public <T> void setCacheObject(final String key, final T value){redisTemplate.opsForValue().set(key, value);}/*** 缓存基本的对象,Integer、String、实体类等** @param key      缓存的键值* @param value    缓存的值* @param timeout  时间* @param timeUnit 时间颗粒度*/public <T> 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> T getCacheObject(final String key){ValueOperations<String, T> 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 <T> long setCacheList(final String key, final List<T> dataList){Long count = redisTemplate.opsForList().rightPushAll(key, dataList);return count == null ? 0 : count;}/*** 获得缓存的list对象** @param key 缓存的键值* @return 缓存键值对应的数据*/public <T> List<T> getCacheList(final String key){return redisTemplate.opsForList().range(key, 0, -1);}/*** 缓存Set** @param key     缓存键值* @param dataSet 缓存的数据* @return 缓存数据的对象*/public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet){BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);Iterator<T> it = dataSet.iterator();while (it.hasNext()){setOperation.add(it.next());}return setOperation;}/*** 获得缓存的set** @param key* @return*/public <T> Set<T> getCacheSet(final String key){return redisTemplate.opsForSet().members(key);}/*** 缓存Map** @param key* @param dataMap*/public <T> void setCacheMap(final String key, final Map<String, T> dataMap){if (dataMap != null){redisTemplate.opsForHash().putAll(key, dataMap);}}/*** 获得缓存的Map** @param key* @return*/public <T> Map<String, T> getCacheMap(final String key){return redisTemplate.opsForHash().entries(key);}/*** 往Hash中存入数据** @param key   Redis键* @param hKey  Hash键* @param value 值*/public <T> 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> T getCacheMapValue(final String key, final String hKey){HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();return opsForHash.get(key, hKey);}/*** 获取多个Hash中的数据** @param key   Redis键* @param hKeys Hash键集合* @return Hash对象集合*/public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys){return redisTemplate.opsForHash().multiGet(key, hKeys);}/*** 删除Hash中的某条数据** @param key  Redis键* @param hKey Hash键* @return 是否成功*/public boolean deleteCacheMapValue(final String key, final String hKey){return redisTemplate.opsForHash().delete(key, hKey) > 0;}/*** 获得缓存的基本对象列表** @param pattern 字符串前缀* @return 对象列表*/public Collection<String> keys(final String pattern){return redisTemplate.keys(pattern);}
}

HttpHelper

主要是为了读取http请求体的数据。

/*** 通用http工具封装** @author ruoyi*/
public class HttpHelper
{private static final Logger LOGGER = LoggerFactory.getLogger(HttpHelper.class);public static String getBodyString(ServletRequest request){StringBuilder sb = new StringBuilder();BufferedReader reader = null;try (InputStream inputStream = request.getInputStream()){reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));String line = "";while ((line = reader.readLine()) != null){sb.append(line);}} catch (IOException e){LOGGER.warn("getBodyString出现问题!");} finally{if (reader != null){try{reader.close();} catch (IOException e){LOGGER.error("Exceptions:", e.getMessage());}}}return sb.toString();}
}

StringUtils

/*** 字符串工具类** @author jzm*/
public class StringUtils extends StrUtil
{/*** 空字符串*/private static final String NULLSTR = "";/*** 下划线*/private static final char SEPARATOR = '_';/*** 获取参数不为空值** @param value defaultValue 要判断的value* @return value 返回值*/public static <T> T nvl(T value, T defaultValue){return value != null ? value : defaultValue;}/*** * 判断一个Collection是否为空, 包含List,Set,Queue** @param coll 要判断的Collection* @return true:为空 false:非空*/public static boolean isEmpty(Collection<?> coll){return isNull(coll) || coll.isEmpty();}/*** * 判断一个Collection是否非空,包含List,Set,Queue** @param coll 要判断的Collection* @return true:非空 false:空*/public static boolean isNotEmpty(Collection<?> coll){return !isEmpty(coll);}/*** * 判断一个对象数组是否为空** @param objects 要判断的对象数组*                * @return true:为空 false:非空*/public static boolean isEmpty(Object[] objects){return isNull(objects) || (objects.length == 0);}/*** * 判断一个对象数组是否非空** @param objects 要判断的对象数组* @return true:非空 false:空*/public static boolean isNotEmpty(Object[] objects){return !isEmpty(objects);}/*** * 判断一个Map是否为空** @param map 要判断的Map* @return true:为空 false:非空*/public static boolean isEmpty(Map<?, ?> map){return isNull(map) || map.isEmpty();}/*** * 判断一个Map是否为空** @param map 要判断的Map* @return true:非空 false:空*/public static boolean isNotEmpty(Map<?, ?> map){return !isEmpty(map);}/*** * 判断一个字符串是否为空串** @param str String* @return true:为空 false:非空*/public static boolean isEmpty(String str){return isNull(str) || NULLSTR.equals(str.trim());}/*** * 判断一个字符串是否为非空串** @param str String* @return true:非空串 false:空串*/public static boolean isNotEmpty(String str){return !isEmpty(str);}/*** * 判断一个对象是否为空** @param object Object* @return true:为空 false:非空*/public static boolean isNull(Object object){return object == null;}/*** * 判断一个对象是否非空** @param object Object* @return true:非空 false:空*/public static boolean isNotNull(Object object){return !isNull(object);}public static boolean inStringIgnoreCase(String str, String... strs){if (str != null && strs != null){for (String s : strs){if (str.equalsIgnoreCase(s)){return true;}}}return false;}
}

ServletUtils

客户端工具类

/*** 客户端工具类** @author Jzm*/
public class ServletUtils
{/*** 获取String参数*/public static String getParameter(String name){return getRequest().getParameter(name);}/*** 获取String参数*/public static String getParameter(String name, String defaultValue){return Convert.toStr(getRequest().getParameter(name), defaultValue);}/*** 获取Integer参数*/public static Integer getParameterToInt(String name){return Convert.toInt(getRequest().getParameter(name));}/*** 获取Integer参数*/public static Integer getParameterToInt(String name, Integer defaultValue){return Convert.toInt(getRequest().getParameter(name), defaultValue);}/*** 获取Boolean参数*/public static Boolean getParameterToBool(String name){return Convert.toBool(getRequest().getParameter(name));}/*** 获取Boolean参数*/public static Boolean getParameterToBool(String name, Boolean defaultValue){return Convert.toBool(getRequest().getParameter(name), defaultValue);}/*** 获得所有请求参数** @param request 请求对象{@link ServletRequest}* @return Map*/public static Map<String, String[]> getParams(ServletRequest request){final Map<String, String[]> map = request.getParameterMap();return Collections.unmodifiableMap(map);}/*** 获得所有请求参数** @param request 请求对象{@link ServletRequest}* @return Map*/public static Map<String, String> getParamMap(ServletRequest request){Map<String, String> params = new HashMap<>();for (Map.Entry<String, String[]> entry : getParams(request).entrySet()){params.put(entry.getKey(), StringUtils.join(",", entry.getValue()));}return params;}/*** 获取request*/public static HttpServletRequest getRequest(){return getRequestAttributes().getRequest();}/*** 获取response*/public static HttpServletResponse getResponse(){return getRequestAttributes().getResponse();}/*** 获取session*/public static HttpSession getSession(){return getRequest().getSession();}public static ServletRequestAttributes getRequestAttributes(){RequestAttributes attributes = RequestContextHolder.getRequestAttributes();return (ServletRequestAttributes) attributes;}/*** 将字符串渲染到客户端** @param response 渲染对象* @param string   待渲染的字符串*/public static void renderString(HttpServletResponse response, String string){try{response.setStatus(200);response.setContentType("application/json");response.setCharacterEncoding("utf-8");response.getWriter().print(string);} catch (IOException e){e.printStackTrace();}}/*** 是否是Ajax异步请求** @param request*/public static boolean isAjaxRequest(HttpServletRequest request){String accept = request.getHeader("accept");if (accept != null && accept.contains("application/json")){return true;}String xRequestedWith = request.getHeader("X-Requested-With");if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")){return true;}String uri = request.getRequestURI();if (StringUtils.inStringIgnoreCase(uri, ".json", ".xml")){return true;}String ajax = request.getParameter("__ajax");return StringUtils.inStringIgnoreCase(ajax, "json", "xml");}/*** 内容编码** @param str 内容* @return 编码后的内容*/public static String urlEncode(String str){try{return URLEncoder.encode(str, Constants.UTF8);} catch (UnsupportedEncodingException e){return StringUtils.EMPTY;}}/*** 内容解码** @param str 内容* @return 解码后的内容*/public static String urlDecode(String str){try{return URLDecoder.decode(str, Constants.UTF8);} catch (UnsupportedEncodingException e){return StringUtils.EMPTY;}}
}

项目用到的模型

AjaxResult: 公共响应类

/*** 操作消息提醒** @author jzm*/
public class AjaxResult extends HashMap<String, Object>
{private static final long serialVersionUID = 1L;/*** 状态码*/public static final String CODE_TAG = "code";/*** 返回内容*/public static final String MSG_TAG = "msg";/*** 数据对象*/public static final String DATA_TAG = "data";/*** 初始化一个新创建的 AjaxResult 对象,使其表示一个空消息。*/public AjaxResult(){}/*** 初始化一个新创建的 AjaxResult 对象** @param code 状态码* @param msg  返回内容*/public AjaxResult(int code, String msg){super.put(CODE_TAG, code);super.put(MSG_TAG, msg);}/*** 初始化一个新创建的 AjaxResult 对象** @param code 状态码* @param msg  返回内容* @param data 数据对象*/public AjaxResult(int code, String msg, Object data){super.put(CODE_TAG, code);super.put(MSG_TAG, msg);if (StringUtils.isNotNull(data)){super.put(DATA_TAG, data);}}/*** 返回成功消息** @return 成功消息*/public static AjaxResult success(){return AjaxResult.success("操作成功");}/*** 返回成功数据** @return 成功消息*/public static AjaxResult success(Object data){return AjaxResult.success("操作成功", data);}/*** 返回成功消息** @param msg 返回内容* @return 成功消息*/public static AjaxResult success(String msg){return AjaxResult.success(msg, null);}/*** 返回成功消息** @param msg  返回内容* @param data 数据对象* @return 成功消息*/public static AjaxResult success(String msg, Object data){return new AjaxResult(HttpStatus.SUCCESS, msg, data);}/*** 返回警告消息** @param msg 返回内容* @return 警告消息*/public static AjaxResult warn(String msg){return AjaxResult.warn(msg, null);}/*** 返回警告消息** @param msg  返回内容* @param data 数据对象* @return 警告消息*/public static AjaxResult warn(String msg, Object data){return new AjaxResult(HttpStatus.WARN, msg, data);}/*** 返回错误消息** @return 错误消息*/public static AjaxResult error(){return AjaxResult.error("操作失败");}/*** 返回错误消息** @param msg 返回内容* @return 错误消息*/public static AjaxResult error(String msg){return AjaxResult.error(msg, null);}/*** 返回错误消息** @param msg  返回内容* @param data 数据对象* @return 错误消息*/public static AjaxResult error(String msg, Object data){return new AjaxResult(HttpStatus.ERROR, msg, data);}/*** 返回错误消息** @param code 状态码* @param msg  返回内容* @return 错误消息*/public static AjaxResult error(int code, String msg){return new AjaxResult(code, msg, null);}/*** 是否为成功消息** @return 结果*/public boolean isSuccess(){return Objects.equals(HttpStatus.SUCCESS, this.get(CODE_TAG));}/*** 是否为警告消息** @return 结果*/public boolean isWarn(){return Objects.equals(HttpStatus.WARN, this.get(CODE_TAG));}/*** 是否为错误消息** @return 结果*/public boolean isError(){return Objects.equals(HttpStatus.ERROR, this.get(CODE_TAG));}/*** 方便链式调用** @param key   键* @param value 值* @return 数据对象*/@Overridepublic AjaxResult put(String key, Object value){super.put(key, value);return this;}
}

2.核心

 RepeatSubmit

重复提交注解。主要用来设置前后提交数据时间差,至少要大于的时间差的上限。

/*** 自定义注解防止表单重复提交** @author jzm*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{/*** 间隔时间(ms),小于此时间视为重复提交*/public int interval() default 5000;/*** 提示消息*/public String message() default "不允许重复提交,请稍候再试";
}

 RepeatSubmitInterceptor

 我们重复提交拦截器的抽象类。我们主要把 preHandle()方法给实现了,但是具体判断是否重复提交的逻辑交给子类来实现。好处是,灵活度高,代码可读性强。当我们有其他相似功能拦截器需要实现时,也只需要继承该类即可。

/*** 拦截器** @author: jzm* @date: 2024-01-24 21:20**/public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
{@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception{if (handler instanceof HandlerMethod){HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();RepeatSubmit repeatSubmit = method.getAnnotation(RepeatSubmit.class); // 能拿到处理方法if (repeatSubmit != null){if (this.isRepeatSubmit(request, repeatSubmit)) // 我们只有加了这个注解才表示限制重复提交{AjaxResult result = AjaxResult.error(repeatSubmit.message());ServletUtils.renderString(response, JSONUtil.toJsonStr(result));return false;}}return true;} else{return true;}}/*** 验证是否重复提交由子类实现具体的防重复提交的规则** @param request    请求信息* @param annotation 防重复注解参数* @return 结果* @throws Exception*/public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
}

SameUrlDataInterceptor

我们要具体判断数据是否重复提交的子类。最后,将这个注入spring容器里面,然后我们在webmvc里面进行配置就可以正常使用了。

/*** 判断请求url和数据是否和上一次相同,* 如果和上次相同,则是重复提交表单。 有效时间为10秒内。** @author jzm*/
// 我们使用拦截器,防止重复提交
// 现在我们知道,为什么不用面向切面了? 切面需要拦截controller里面的方法,但是若依controller分布比较分散
// 用拦截器,会拦截controller的映射接口
// 首先,我们知道 Handler能够获得映射为方法的Method
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{@AutowiredRedisCache redisCache;public final String header = "Authorization";public final String REPEAT_PARAMS = "repeatParams";public final String REPEAT_TIME = "repeatTime";@Overridepublic boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation){String nowParams = "";// 拿请求body里面的内容// 拷贝副本--给拦截器读取 clone拷贝不显示,对于引用对象,是引用拷贝...,//if (request instanceof RequestReaderHttpServletRequestWrapper){RequestReaderHttpServletRequestWrapper requestWrapper = (RequestReaderHttpServletRequestWrapper) request;nowParams = HttpHelper.getBodyString(requestWrapper);}// body参数为空,获取Parameter的数据if (StringUtils.isEmpty(nowParams)){nowParams = JSONUtil.toJsonStr(request.getParameterMap());}// 当前数据映射,提交参数、提交时间Map<String, Object> nowDataMap = new HashMap<String, Object>();nowDataMap.put(REPEAT_PARAMS, nowParams);nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());// 请求地址(作为存放cache的key值)String uri = request.getRequestURI();// 唯一值(没有消息头则使用请求地址)String submitKey = StringUtils.trimToEmpty(request.getHeader(header));// 唯一标识(指定key + url + 消息头)String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + uri + submitKey;// 如果 == null,代表提交过Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);if (sessionObj != null){Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;if (sessionMap.containsKey(uri)){Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(uri);// 两次提交内容一致 && 提交时间间隔差 < 要求时间段if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())){return true;}}}HashMap<String, Object> cacheMap = new HashMap<>();cacheMap.put(uri, nowDataMap);redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);// 最后设置这上一个缓存对象重复提交时间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, int interval){long time1 = (Long) nowMap.get(REPEAT_TIME);long time2 = (Long) preMap.get(REPEAT_TIME);if ((time1 - time2) < interval){return true;}return false;}
}

RepeatedlyRequestWrapper

 关于为什么要这个东西呢?我们post请求,拦截器要预先读取HtppServletRequest里面的body的数据,是通过io的方式,都知道io读取完毕之后,之前的数据是变为null的,但是,当我么后面的接口来委派的时候,也是通过io读取body。这时候bodu里面是null的。那么鸡儿就会报io错。

因此我们需要这个类建立复制流。

/*** 将请求包装,用来建立复制流** @author jzm*/
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
{private final byte[] body;public RepeatedlyRequestWrapper(HttpServletRequest request) throws IOException{super(request);body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8"));}@Overridepublic BufferedReader getReader() throws IOException{return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException{final ByteArrayInputStream bais = new ByteArrayInputStream(body);return new ServletInputStream(){@Overridepublic int read() throws IOException{return bais.read();}@Overridepublic boolean isFinished(){return false;}@Overridepublic boolean isReady(){return false;}@Overridepublic void setReadListener(ReadListener readListener){}};}
}

RepeatableFilter

一般情况下,还用到Filterl来对reques来进行包装成wrapper。然后传递到拦截器。

/*** Repeatable 过滤器** @author jzm*/
@Component
public class RepeatableFilter implements Filter
{@Overridepublic void init(FilterConfig filterConfig) throws ServletException{}@Override// 我们下面对于包装,前提是application/jsonpublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException{ServletRequest requestWrapper = null;if (request instanceof HttpServletRequest&& StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)){requestWrapper = new RequestReaderHttpServletRequestWrapper((HttpServletRequest) request);}if (null == requestWrapper){chain.doFilter(request, response);} else{chain.doFilter(requestWrapper, response);}}@Overridepublic void destroy(){}
}

3.测试

测试用到的to

​/*** 测试to** @author: jzm* @date: 2024-01-25 14:26**/public class TestTo
{public TestTo(){}public String getName(){return name;}public void setName(String name){this.name = name;}public Integer getAge(){return age;}public void setAge(Integer age){this.age = age;}private String name;private Integer age;}

我们新建controller用来测试。分别对get路径参数、post请求体中的数据进行来校验。

*** 测试控制器** @author: jzm* @date: 2024-01-25 11:10**/@RestController
@ResponseBody
public class BaseController
{private Logger log = LoggerFactory.getLogger(BaseController.class);@RequestMapping(value = "/get/test", method = {RequestMethod.GET})@RepeatSubmit(interval = 10 * 1000, message = "对不起您重复提交get请求!")public AjaxResult getTest(@RequestParam("name") String name, @RequestParam("age") Integer age){String res = "get_test:" + name + age;log.info(res);return AjaxResult.success(res);}@RequestMapping(value = "/post/test", method = {RequestMethod.POST})@RepeatSubmit(interval = 10 * 1000, message = "对不起重复提交post请求")public AjaxResult postTest(@RequestBody TestTo testTo){String res = "post_test" + testTo.getName() + testTo.getAge();log.info(res);return AjaxResult.success(res);}
}

我们启动项目,利用Apifox  来进行测试:

这时候打开Resp:免费Redis图形化界面(RESP)下载地址和连接步骤_resp下载-CSDN博客

 发现数据是成功存入的,剩余7s过期,在10s之内,也就是数据没过期之前,在发送一次。

 因此,确实数据重复提交了。

post请求测试。

相关文章:

redis + 拦截器 :防止数据重复提交

1.项目用到,不是核心 我们干系统开发,不免要考虑一个点&#xff0c;数据的重复提交。 我想我们之前如果要校验数据重复提交要求&#xff0c;会怎么干?会在业务层&#xff0c;对数据库操作&#xff0c;查询数据是否存在,存在就禁止插入数据; 但是吧,我们每次crud操作都会连接…...

如何进行H.265视频播放器EasyPlayer.js的中性化设置?

H5无插件流媒体播放器EasyPlayer属于一款高效、精炼、稳定且免费的流媒体播放器&#xff0c;可支持多种流媒体协议播放&#xff0c;可支持H.264与H.265编码格式&#xff0c;性能稳定、播放流畅&#xff0c;能支持WebSocket-FLV、HTTP-FLV&#xff0c;HLS&#xff08;m3u8&#…...

Ubuntu22.04安装4090显卡驱动

1、安装完Ubuntu系统&#xff0c;打完所有补丁后再进行后续操作 2、下载系统所需要的版本的NV显卡驱动&#xff0c;本次由于使用CUDA12.1&#xff0c;故选用的驱动版本为NVIDIA-Linux-x86_64-530.41.03.run 3、卸载NV驱动&#xff08;只是保险起见&#xff0c;并不是一定会卸…...

YOLOv8优化策略:注意力涨点系列篇 | 一种轻量级的加强通道信息和空间信息提取能力的MLCA注意力

🚀🚀🚀本文改进:一种轻量级的加强通道信息和空间信息提取能力 MLCA注意力 🚀🚀🚀在YOLOv8中如何使用 1)作为注意力机制使用;2)与c2f结合使用; 🚀🚀🚀YOLOv8改进专栏:http://t.csdnimg.cn/hGhVK 学姐带你学习YOLOv8,从入门到创新,轻轻松松搞定科研…...

【新书推荐】2.5节 有符号整数和无符号整数

本节内容&#xff1a;整数的编码规则。 ■数据的编码规则&#xff1a;计算机的二进制数对于计算机本身而言仅仅表示0和1。人们按照不同的编码规则赋予二进制数不同的含义。整数的编码规则分为有符号整数和无符号整数。 ■数据的存储规则&#xff1a;x86计算机以字节为单位&…...

RT-Thread: 串口操作、增加串口、串口函数

说明&#xff1a;本文记录RT-Thread添加串口的步骤和串口的使用。 1.新增串口 官方链接&#xff1a;https://www.rt-thread.org/document/site/rtthread-studio/drivers/uart/v4.0.2/rtthread-studio-uart-v4.0.2/ 新增串口只需要在 board.h 文件中定义相关串口的宏定…...

自然语言处理的新突破:如何推动语音助手和机器翻译的进步

一、语音助手方面的进展 语音助手作为人机交互的重要入口之一,其性能的提升离不开自然语言处理技术的进步。基于深度学习的语音识别和语义理解技术,使得语音助手可以更准确地分析用户意图,提供个性化服务。 语音识别精度的持续提高 语音识别是语音助手的基础。随着深度神经网…...

vue3 + jeecgBoot 获取项目IP地址

封装的useGlobSetting 函数 引入并使用 import { useGlobSetting } from //hooks/setting;const glob useGlobSetting();console.log(glob.uploadUrl) //http://192.168.105.57:7900/bs-axfd...

Java Server-Sent Events通信

Server-Sent Events特点与优势 后端可以向前端发送信息&#xff0c;类似于websocket&#xff0c;但是websocket是双向通信&#xff0c;但是sse为单向通信&#xff0c;服务器只能向客户端发送文本信息&#xff0c;效率比websocket高。 单向通信&#xff1a;SSE只支持服务器到客…...

[蓝桥杯]真题讲解:冶炼金属(暴力+二分)

蓝桥杯真题视频讲解&#xff1a;冶炼金属&#xff08;暴力做法与二分做法&#xff09; 一、视频讲解二、暴力代码三、正解代码 一、视频讲解 视频讲解 二、暴力代码 //暴力代码 #include<bits/stdc.h> #define endl \n #define deb(x) cout << #x << &qu…...

Fastbee开源物联网项目RoadMap

架构优化 代码简化业务&协议解耦关键组件支持横向拓展网络协议支持横向拓展&#xff0c;包括&#xff1a;mqtt broker,tcp,coap,udp,sip等协议插件化编码脚本化业务代码模版化消息总线 功能优化 网关/子网关&#xff1a;上线&#xff0c;绑定&#xff0c;拓扑&#xff0…...

Linux文件管理技术实践

shell shell的种类(了解) shell是用于和Linux内核进行交互的一个程序&#xff0c;他的功能和window系统下的cmd是一样的。而且shell的种类也有很多常见的有c shell、bash shell、Korn shell等等。而本文就是使用Linux最常见的bash shell对Linux常见指令展开探讨。 内置shell…...

Python如何按指定列的空值删除行?

目录 1、按指定列的空值删除行2、滑动窗口按指定列的值填充最前面的缺失值 1、按指定列的空值删除行 数据准备&#xff1a; df pd.DataFrame({C1: [1, 2, 3, 4], C2: [A, np.NaN, C, D], C3: [V1, V2, V3, np.NaN]}) print(df.to_string()) C1 C2 C3 0 1 A V1 1 …...

【云原生】Docker的镜像创建

目录 1&#xff0e;基于现有镜像创建 &#xff08;1&#xff09;首先启动一个镜像&#xff0c;在容器里做修改 ​编辑&#xff08;2&#xff09;然后将修改后的容器提交为新的镜像&#xff0c;需要使用该容器的 ID 号创建新镜像 实验 2&#xff0e;基于本地模板创建 3&am…...

大语言模型推理提速:TensorRT-LLM 高性能推理实践

作者&#xff1a;顾静 TensorRT-LLM 如何提升 LLM 模型推理效率 大型语言模型&#xff08;Large language models,LLM&#xff09;是基于大量数据进行预训练的超大型深度学习模型。底层转换器是一组神经网络&#xff0c;这些神经网络由具有 self-attention 的编码器和解码器组…...

全面理解“张量”概念

1. 多重视角看“张量” 张量&#xff08;Tensor&#xff09;是一个多维数组的概念&#xff0c;在不同的学科领域中有不同的应用和解释&#xff1a; 物理学中的张量&#xff1a; 在物理学中&#xff0c;张量是一个几何对象&#xff0c;用来表示在不同坐标系下变换具有特定规律的…...

MacOS X 安装免费的 LaTex 环境

最近把工作终端一步步迁移到Mac上来了&#xff0c;搭了个 Latex的环境&#xff0c;跟windows上一样好用。 首先&#xff0c;如果是 intel 芯片的 macOS&#xff0c;那么可以使用组合1&#xff0c; 如果是 M1、M2 或 M3 芯片或者 intel 芯片的 Mac book&#xff0c;则应该使用…...

深入Amazon S3:实战指南

Amazon S3(Simple Storage Service)是AWS(Amazon Web Services)提供的一项强大的云存储服务,广泛用于存储和检索各种类型的数据。本篇实战指南将深入介绍如何在实际项目中充分利用Amazon S3的功能,包括存储桶的创建、对象的管理、权限控制、版本控制、日志记录等方面的实…...

Ansible自动化运维(三)Playbook 模式详解

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01; &#x1f40b; 希望大家多多支…...

LCS板子加逆向搜索

LCS 题面翻译 题目描述&#xff1a; 给定一个字符串 s s s 和一个字符串 t t t &#xff0c;输出 s s s 和 t t t 的最长公共子序列。 输入格式&#xff1a; 两行&#xff0c;第一行输入 s s s &#xff0c;第二行输入 t t t 。 输出格式&#xff1a; 输出 s s s…...

不同知识表示方法与知识图谱

目录 前言1 一阶谓词逻辑1.1 简介1.2 优势1.3 局限性 2 产生式规则2.1 简介2.2 优势2.3 局限性 3 框架系统3.1 简介3.2 优势3.3 局限性 4 描述逻辑4.1 简介4.2 优势4.3 局限性 5 语义网络5.1 简介5.2 优势5.3 局限性 结语 前言 知识表示是人工智能领域中至关重要的一环&#x…...

Kotlin程序设计 扩展篇(一)

Kotlin程序设计&#xff08;扩展一&#xff09; **注意&#xff1a;**开启本视频学习前&#xff0c;需要先完成以下内容的学习&#xff1a; 请先完成《Kotlin程序设计》视频教程。请先完成《JavaSE》视频教程。 Kotlin在设计时考虑到了与Java的互操作性&#xff0c;现有的Ja…...

星环科技基于第五代英特尔®至强®可扩展处理器的分布式向量数据库解决方案重磅发布

12月15日&#xff0c;2023 英特尔新品发布会暨 AI 技术创新派对上&#xff0c;星环科技基于第五代英特尔至强可扩展处理器的Transwarp Hippo分布式向量数据库解决方案重磅发布。该方案利用第五代英特尔至强可扩展处理器带来的强大算力&#xff0c;实现了约 2 倍的代际性能提升&…...

一体化运维的发展趋势与未来展望

随着信息技术的迅猛发展&#xff0c;企业的IT系统已经从单一的、孤立的应用转变为多元化、复杂化的系统集群。云计算、大数据、物联网等前沿技术的广泛应用&#xff0c;使得企业的IT运维面临着前所未有的挑战。在这样的背景下&#xff0c;一体化运维作为一种新型的运维模式&…...

科技云报道:金融大模型落地,还需跨越几重山?

科技云报道原创。 时至今日&#xff0c;大模型的狂欢盛宴仍在持续&#xff0c;而金融行业得益于数据密集且有强劲的数字化基础&#xff0c;从一众场景中脱颖而出。 越来越多的公司开始布局金融行业大模型&#xff0c;无论是乐信、奇富科技、度小满、蚂蚁这样的金融科技公司&a…...

C语言入门到精通之练习34:求100之内的素数

题目&#xff1a;求100之内的素数。 程序分析&#xff1a;质数&#xff08;素数&#xff09;酵母素数&#xff0c;有无限个。一个大于1的自然数&#xff0c;除了1和它本身外&#xff0c;不能被其他自然数整除。 代码如下&#xff1a; #include <stdio.h># #include &l…...

Qt采集本地摄像头推流成rtsp/rtmp(可网页播放/支持嵌入式linux)

一、功能特点 支持各种本地视频文件和网络视频文件。支持各种网络视频流&#xff0c;网络摄像头&#xff0c;协议包括rtsp、rtmp、http。支持将本地摄像头设备推流&#xff0c;可指定分辨率和帧率等。支持将本地桌面推流&#xff0c;可指定屏幕区域和帧率等。自动启动流媒体服…...

Oracle按日周月年自动分区

目录 1、分区键 2、初始分区 3、周月年自动分区 4、按日自动分区表建表语句 与普通建表语句相比&#xff0c;分区表多了一些分区信息&#xff1b; 1、分区键 以下面销售明细表为例&#xff0c;以data_dt为分区键&#xff0c;NUMTODSINTERVAL(1, day) 按日分区 PARTITION …...

单元测试、模块测试、web接口测试

单元测试与模块测试 什么是“单元测试”、“模块测试”&#xff1f; 然而在功能的实现代码中并没有“单元”&#xff0c;也没有“模块”&#xff1b;只有函数、类和方法。先来分别看看它们 的定义&#xff1a; 单元测试&#xff08;Unit testing&#xff09;&#xff0c;是指…...

DAY10_SpringBoot—SpringMVC重定向和转发RestFul风格JSON格式SSM框架整合Ajax-JQuery

目录 1 SpringMVC1.1 重定向和转发1.1.1 转发1.1.2 重定向1.1.3 转发练习1.1.4 重定向练习1.1.5 重定向/转发特点1.1.6 重定向/转发意义 1.2 RestFul风格1.2.1 RestFul入门案例1.2.2 简化业务调用 1.3 JSON1.3.1 JSON介绍1.3.2 JSON格式1.3.2.1 Object格式1.3.2.2 Array格式1.3…...

wordpress幻灯片图片主题/企业推广策划公司

文章目录一、DockerFile是什么二、DockerFile构建的过程1、DockerFile内容基础知识语法2、Docker执行Dockerfile的大致流程3、小结三、DockerFile的保留字指令四、案例分析编写DockerFile1、Base镜像(scratch)2、案例一&#xff1a;编写centos&#xff08;1&#xff09;DockerF…...

免费服务器搭建网站详细教程/网站统计数据

在外连接中&#xff0c;where后出现的表等同于内连接&#xff0c;因此&#xff0c;如果用了where条件&#xff0c;就应当将left join改为inner join。以下测试验证了这点。with tab_a as(select 1 id1, 11 id2 from dual union allselect 2 id1, 22 id2 from dual union allsel…...

北龙中网 可信网站验证 费用/广告投放平台都有哪些

题目 本题是谭浩强《c语言程序设计》第五章第十六题 题目&#xff1a; 输出图案&#xff1a; * 1 *** 2 ***** 3 ******* 4***** 5*** 6* 7以下是…...

如何建设一个公司网站/互联网域名交易中心

jQuery –当今最先进的JavaScript库之一&#xff0c;全球各地的多个程序都在使用jQuery来创建出色的效果和动画 。 我们随机列出了您要使用的插件。 玩得开心&#xff01; 1. Aga&#xff08;手风琴画廊&#xff09; Aga是一个简单&#xff0c;易于使用且可完全自定义的手风琴插…...

鄞州区住房和城乡建设委员网站/免费自己建网页

实时文件夹&#xff0c;就是指用于显示ContentProvider提供的数据的桌面组件。当用户把实时文件夹添加到系统桌面上之后&#xff0c;如果用户单击该实时文件夹图标&#xff0c;系统将会显示从指定ContentProvider查出来的数据。可以以列表形式&#xff0c;也可以以网格形式来显…...

网站模板/广州百度seo 网站推广

一、方案背景&#xff1a; 最近由于国内煤炭价格上涨&#xff0c;同时叠加国家碳中和的相关政策影响全国&#xff0c;很多地区出现了有序限电&#xff0c;甚至拉闸限电等多种情况&#xff0c;导致加油站无法正常的运作。为此&#xff0c;加油站都会配备UPS电源为加油站主机及油…...