初始化项目

This commit is contained in:
2026-08-06 15:19:45 +08:00
commit 63c8d8b757
229 changed files with 12653 additions and 0 deletions
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.xujun</groupId>
<artifactId>xtools-boot-idempotent</artifactId>
<version>5.1.0</version>
</parent>
<artifactId>xtools-boot-idempotent-web</artifactId>
<!-- 依赖 -->
<dependencies>
<!-- xtools-boot begin -->
<!-- xtools-boot-idempotent-base(核心层) -->
<dependency>
<groupId>org.xujun</groupId>
<artifactId>xtools-boot-idempotent-base</artifactId>
</dependency>
<!-- xtools-boot end -->
<!-- xtools begin -->
<!-- xtools-web(HeaderUtils 获取 Web 信息) -->
<dependency>
<groupId>org.xujun</groupId>
<artifactId>xtools-web</artifactId>
</dependency>
<!-- xtools end -->
<!-- Spring-Web(RequestContextHolder) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<optional>true</optional>
</dependency>
<!-- Jakarta Servlet -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,28 @@
package xtools.boot.idempotent.web;
import org.springframework.context.annotation.Import;
import xtools.boot.core.utils.ModuleLoadUtils;
import xtools.boot.idempotent.web.selector.BootIdempotentWebImportSelector;
/**
* <p>Title : BootIdempotentWebConfiguration</p>
* <p>Description : BootIdempotentWebConfiguration</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 5.0.0
* @date : 2026/07/29 10:00
*/
@Import(BootIdempotentWebImportSelector.class)
public class BootIdempotentWebConfiguration {
/**
* 构造方法
*/
public BootIdempotentWebConfiguration() {
ModuleLoadUtils.loadSuccess(BootIdempotentWebConfiguration.class);
}
}
@@ -0,0 +1,78 @@
package xtools.boot.idempotent.web.annotation;
import xtools.boot.idempotent.web.enums.RepeatSubmitKeyType;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* <p>Title : RepeatSubmit</p>
* <p>Description : RepeatSubmit</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 5.0.0
* @date : 2026/07/28 09:30
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/**
* Redis Key 前缀
*
* @return 前缀
*/
String prefix() default "repeat-submit";
/**
* 防重复提交时间间隔(秒)
*
* @return 时间间隔
*/
long interval() default 5;
/**
* 重复提交提示信息
*
* @return 提示信息
*/
String message() default "请勿重复提交";
/**
* Key 生成策略
*
* @return Key 生成策略
*/
RepeatSubmitKeyType keyType() default RepeatSubmitKeyType.DEFAULT;
/**
* Token 请求头名称
*
* @return 请求头名称
*/
String tokenHeader() default "Bearer Authorization";
/**
* 是否将请求参数(查询参数/表单)参与 Key 计算
*
* @return 是否参与
*/
boolean includeParams() default true;
/**
* 是否在方法执行成功后立即删除 Key(放行下一次提交).
* false: Key 保留至 expire 过期,实现时间窗口内的幂等(默认)
* true: 方法成功后立即释放,仅防止并发重复提交
*
* @return 是否立即删除 Key
*/
boolean delKey() default false;
}
@@ -0,0 +1,148 @@
package xtools.boot.idempotent.web.aspect;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import xtools.boot.api.exection.BizWarning;
import xtools.boot.idempotent.base.service.IdempotentService;
import xtools.boot.idempotent.web.annotation.RepeatSubmit;
import xtools.core.StringUtils;
import xtools.core.encrypt.Md5Utils;
import xtools.web.HeaderUtils;
import java.util.StringJoiner;
import static xtools.base.config.Constants.CP_COLON;
import static xtools.base.config.Constants.CP_EMPTY;
/**
* <p>Title : RepeatSubmitAspect</p>
* <p>Description : RepeatSubmitAspect</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 5.0.0
* @date : 2026/07/28 09:30
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class RepeatSubmitAspect {
/**
* 幂等服务
*/
private final IdempotentService idempotentService;
/**
* 防重复提交拦截
*
* @param joinPoint 连接点
* @param repeatSubmit 防重复提交注解
* @return 方法执行结果
* @throws Throwable 方法执行异常
*/
@Around("@annotation(repeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return joinPoint.proceed();
}
// 生成幂等 Key
String key = buildKey(attributes.getRequest(), repeatSubmit);
boolean acquired = idempotentService.tryAcquire(key, repeatSubmit.interval());
if (!acquired) {
throw new BizWarning(repeatSubmit.message());
}
// 执行业务
Object result = joinPoint.proceed();
if (repeatSubmit.delKey()) {
// 成功后立即释放
idempotentService.release(key);
}
return result;
}
/**
* 构建幂等 Key
*
* @param request 请求
* @param repeatSubmit 注解
* @return Key
*/
private String buildKey(HttpServletRequest request, RepeatSubmit repeatSubmit) {
StringJoiner joiner = new StringJoiner(CP_COLON);
joiner.add(repeatSubmit.prefix());
switch (repeatSubmit.keyType()) {
case TOKEN -> joiner.add(token(request, repeatSubmit.tokenHeader()));
case IP -> joiner.add(clientIp(request));
case URI -> {
}
default -> joiner.add(identity(request, repeatSubmit.tokenHeader()));
}
joiner.add(HeaderUtils.getUri(request));
if (repeatSubmit.includeParams()) {
String paramsDigest = paramsDigest(request);
if (StringUtils.isNotBlank(paramsDigest)) {
joiner.add(paramsDigest);
}
}
return joiner.toString();
}
/**
* 客户端标识:Token 优先,无 Token 则使用 IP
*
* @param request 请求
* @param headerName Token 头名称
* @return 客户端标识
*/
private String identity(HttpServletRequest request, String headerName) {
String token = token(request, headerName);
return StringUtils.isNotBlank(token) ? token : clientIp(request);
}
/**
* 获取 Token(去除 Bearer 前缀)
*
* @param request 请求
* @param headerName 头名称
* @return Token
*/
private String token(HttpServletRequest request, String headerName) {
String header = HeaderUtils.getHeader(request, headerName);
return StringUtils.isBlank(header) ? CP_EMPTY : header.trim();
}
/**
* 获取客户端 IP
*
* @param request 请求
* @return IP
*/
private String clientIp(HttpServletRequest request) {
String ip = HeaderUtils.getIp(request);
return ip == null ? CP_EMPTY : ip;
}
/**
* 请求参数摘要(MD5)
*
* @param request 请求
* @return 摘要
*/
private String paramsDigest(HttpServletRequest request) {
String params = HeaderUtils.getParamsToString(request);
return StringUtils.isBlank(params) ? null : Md5Utils.encryptToString(params);
}
}
@@ -0,0 +1,36 @@
package xtools.boot.idempotent.web.enums;
/**
* <p>Title : RepeatSubmitKeyType</p>
* <p>Description : RepeatSubmitKeyType</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 5.0.0
* @date : 2026/07/28 09:30
*/
public enum RepeatSubmitKeyType {
/**
* Token + URI
*/
TOKEN,
/**
* 客户端 IP + URI
*/
IP,
/**
* URI
*/
URI,
/**
* 默认:Token(无则 IP) + URI
*/
DEFAULT,
}
@@ -0,0 +1,36 @@
package xtools.boot.idempotent.web.selector;
import org.jspecify.annotations.NonNull;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
/**
* <p>Title : BootIdempotentWebImportSelector</p>
* <p>Description : BootIdempotentWebImportSelector</p>
* <p>DevelopTools : Idea_x64_v2026.1</p>
* <p>DevelopSystem : macOS Sequoia 15.7.5</p>
* <p>Company : org.xujun</p>
*
* @author : XuJun
* @version : 5.0.0
* @date : 2026/07/29 10:00
*/
public class BootIdempotentWebImportSelector implements ImportBeanDefinitionRegistrar {
/**
* 根据给定的注释元数据,根据需要注册bean
*
* @param importingClassMetadata AnnotationMetadata
* @param registry BeanDefinitionRegistry
*/
@Override
public void registerBeanDefinitions(@NonNull AnnotationMetadata importingClassMetadata, @NonNull BeanDefinitionRegistry registry) {
// 构建扫描对象
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry, true);
// 扫描 Controller 包装层包
scanner.scan("xtools.boot.idempotent.web");
}
}