login(@RequestBody @Valid LoginForm request) {
+ AuthenticationToken authenticationToken = passwordAuthService.login(request.getUsername(), request.getPassword());
+ return Result.success(authenticationToken);
+ }
+
+}
diff --git a/src/main/java/com/youlai/boot/auth/password/enums/CaptchaTypeEnum.java b/src/main/java/com/youlai/boot/auth/password/enums/CaptchaTypeEnum.java
new file mode 100644
index 00000000..fdcc70f5
--- /dev/null
+++ b/src/main/java/com/youlai/boot/auth/password/enums/CaptchaTypeEnum.java
@@ -0,0 +1,22 @@
+package com.youlai.boot.auth.password.enums;
+
+/**
+ * 验证码类型
+ *
+ * @author Ray.Hao
+ * @since 2.5.1
+ */
+public enum CaptchaTypeEnum {
+
+ /** 圆圈干扰验证码 */
+ CIRCLE,
+
+ /** Gif 动画验证码 */
+ GIF,
+
+ /** 干扰线验证码 */
+ LINE,
+
+ /** 扭曲干扰验证码 */
+ SHEAR
+}
diff --git a/src/main/java/com/youlai/boot/auth/exception/CaptchaException.java b/src/main/java/com/youlai/boot/auth/password/exception/CaptchaException.java
similarity index 90%
rename from src/main/java/com/youlai/boot/auth/exception/CaptchaException.java
rename to src/main/java/com/youlai/boot/auth/password/exception/CaptchaException.java
index 78702000..9c5c6b6d 100644
--- a/src/main/java/com/youlai/boot/auth/exception/CaptchaException.java
+++ b/src/main/java/com/youlai/boot/auth/password/exception/CaptchaException.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.exception;
+package com.youlai.boot.auth.password.exception;
import com.youlai.boot.common.result.ResultCode;
import lombok.Getter;
diff --git a/src/main/java/com/youlai/boot/auth/security/filter/CaptchaValidationFilter.java b/src/main/java/com/youlai/boot/auth/password/filter/CaptchaValidationFilter.java
similarity index 93%
rename from src/main/java/com/youlai/boot/auth/security/filter/CaptchaValidationFilter.java
rename to src/main/java/com/youlai/boot/auth/password/filter/CaptchaValidationFilter.java
index 67423451..201ab338 100644
--- a/src/main/java/com/youlai/boot/auth/security/filter/CaptchaValidationFilter.java
+++ b/src/main/java/com/youlai/boot/auth/password/filter/CaptchaValidationFilter.java
@@ -1,13 +1,13 @@
-package com.youlai.boot.auth.security.filter;
+package com.youlai.boot.auth.password.filter;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
+import com.youlai.boot.auth.password.exception.CaptchaException;
+import com.youlai.boot.auth.password.service.CaptchaService;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.framework.web.util.ResponseWriter;
-import com.youlai.boot.auth.exception.CaptchaException;
-import com.youlai.boot.auth.service.CaptchaService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
@@ -30,6 +30,10 @@ import java.nio.charset.StandardCharsets;
/**
* 图形验证码校验过滤器
+ *
+ * 仅拦截账号密码登录请求:先校验验证码,校验通过后把请求体包装成可重复读取的 Request 再放行。
+ *
+ * @author Ray.Hao
*/
public class CaptchaValidationFilter extends OncePerRequestFilter {
diff --git a/src/main/java/com/youlai/boot/auth/model/req/LoginReq.java b/src/main/java/com/youlai/boot/auth/password/model/form/LoginForm.java
similarity index 84%
rename from src/main/java/com/youlai/boot/auth/model/req/LoginReq.java
rename to src/main/java/com/youlai/boot/auth/password/model/form/LoginForm.java
index bb8c9002..c60c1289 100644
--- a/src/main/java/com/youlai/boot/auth/model/req/LoginReq.java
+++ b/src/main/java/com/youlai/boot/auth/password/model/form/LoginForm.java
@@ -1,15 +1,17 @@
-package com.youlai.boot.auth.model.req;
+package com.youlai.boot.auth.password.model.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
- * 登录请求参数
+ * 账号密码登录请求参数
+ *
+ * @author Ray.Hao
*/
@Schema(description = "登录请求参数")
@Data
-public class LoginReq {
+public class LoginForm {
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
@NotBlank(message = "用户名不能为空")
diff --git a/src/main/java/com/youlai/boot/auth/model/resp/CaptchaInfo.java b/src/main/java/com/youlai/boot/auth/password/model/vo/CaptchaVO.java
similarity index 82%
rename from src/main/java/com/youlai/boot/auth/model/resp/CaptchaInfo.java
rename to src/main/java/com/youlai/boot/auth/password/model/vo/CaptchaVO.java
index 8897c982..e4189e4f 100644
--- a/src/main/java/com/youlai/boot/auth/model/resp/CaptchaInfo.java
+++ b/src/main/java/com/youlai/boot/auth/password/model/vo/CaptchaVO.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.model.resp;
+package com.youlai.boot.auth.password.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
@@ -8,13 +8,15 @@ import lombok.NoArgsConstructor;
/**
* 验证码信息
+ *
+ * @author Ray.Hao
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "验证码信息")
-public class CaptchaInfo {
+public class CaptchaVO {
@Schema(description = "验证码缓存ID")
private String captchaId;
diff --git a/src/main/java/com/youlai/boot/auth/service/CaptchaService.java b/src/main/java/com/youlai/boot/auth/password/service/CaptchaService.java
similarity index 89%
rename from src/main/java/com/youlai/boot/auth/service/CaptchaService.java
rename to src/main/java/com/youlai/boot/auth/password/service/CaptchaService.java
index 4ce21b58..ec188074 100644
--- a/src/main/java/com/youlai/boot/auth/service/CaptchaService.java
+++ b/src/main/java/com/youlai/boot/auth/password/service/CaptchaService.java
@@ -1,16 +1,16 @@
-package com.youlai.boot.auth.service;
+package com.youlai.boot.auth.password.service;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
+import com.youlai.boot.auth.password.config.CaptchaProperties;
+import com.youlai.boot.auth.password.enums.CaptchaTypeEnum;
+import com.youlai.boot.auth.password.exception.CaptchaException;
+import com.youlai.boot.auth.password.model.vo.CaptchaVO;
import com.youlai.boot.common.constant.RedisConstants;
-import com.youlai.boot.auth.enums.CaptchaTypeEnum;
import com.youlai.boot.common.result.ResultCode;
-import com.youlai.boot.config.property.CaptchaProperties;
-import com.youlai.boot.auth.exception.CaptchaException;
-import com.youlai.boot.auth.model.resp.CaptchaInfo;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@@ -35,8 +35,10 @@ public class CaptchaService {
/**
* 生成验证码
+ *
+ * @return 验证码ID与图片Base64
*/
- public CaptchaInfo generate() {
+ public CaptchaVO generate() {
String captchaType = captchaProperties.getType();
int width = captchaProperties.getWidth();
int height = captchaProperties.getHeight();
@@ -71,7 +73,7 @@ public class CaptchaService {
TimeUnit.SECONDS
);
- return CaptchaInfo.builder()
+ return CaptchaVO.builder()
.captchaId(captchaId)
.captchaBase64(imageBase64Data)
.build();
diff --git a/src/main/java/com/youlai/boot/auth/password/service/PasswordAuthService.java b/src/main/java/com/youlai/boot/auth/password/service/PasswordAuthService.java
new file mode 100644
index 00000000..e969bbec
--- /dev/null
+++ b/src/main/java/com/youlai/boot/auth/password/service/PasswordAuthService.java
@@ -0,0 +1,63 @@
+package com.youlai.boot.auth.password.service;
+
+import com.youlai.boot.auth.password.model.vo.CaptchaVO;
+import com.youlai.boot.framework.security.model.AuthenticationToken;
+import com.youlai.boot.framework.security.token.TokenManager;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Service;
+
+/**
+ * 账号密码登录服务
+ *
+ * @author Ray.Hao
+ * @since 4.8.0
+ */
+@Service
+@RequiredArgsConstructor
+public class PasswordAuthService {
+
+ private final AuthenticationManager authenticationManager;
+ private final TokenManager tokenManager;
+ private final CaptchaService captchaService;
+
+ /**
+ * 用户名密码登录
+ *
+ * @param username 用户名
+ * @param password 密码
+ * @return 访问令牌
+ */
+ public AuthenticationToken login(String username, String password) {
+ // 1. 创建用于密码认证的令牌(未认证)
+ UsernamePasswordAuthenticationToken authenticationToken =
+ new UsernamePasswordAuthenticationToken(username.trim(), password);
+
+ // 2. 执行认证(认证中)
+ // 说明:这里的认证流程由 Spring Security 提供的 AuthenticationManager 执行。
+ // 默认情况下会委托给 DaoAuthenticationProvider:
+ // 1) retrieveUser(...):内部通过 UserDetailsService.loadUserByUsername(...) 获取用户信息(本项目为 SecurityUserDetailsService 实现)
+ // 2) additionalAuthenticationChecks(...):对比请求密码与用户存储密码(由 PasswordEncoder 完成匹配)
+ // 认证通过后返回已认证的 Authentication(principal 为 SecurityUserDetails,authorities 为角色/权限集合)。
+ Authentication authentication = authenticationManager.authenticate(authenticationToken);
+
+ // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
+ AuthenticationToken authenticationTokenResponse =
+ tokenManager.generateToken(authentication);
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ return authenticationTokenResponse;
+ }
+
+ /**
+ * 获取验证码
+ *
+ * @return 验证码ID与图片Base64
+ */
+ public CaptchaVO getCaptcha() {
+ return captchaService.generate();
+ }
+
+}
diff --git a/src/main/java/com/youlai/boot/auth/controller/QrCodeAuthController.java b/src/main/java/com/youlai/boot/auth/qrcode/controller/QrCodeAuthController.java
similarity index 76%
rename from src/main/java/com/youlai/boot/auth/controller/QrCodeAuthController.java
rename to src/main/java/com/youlai/boot/auth/qrcode/controller/QrCodeAuthController.java
index 221ef724..773fbebe 100644
--- a/src/main/java/com/youlai/boot/auth/controller/QrCodeAuthController.java
+++ b/src/main/java/com/youlai/boot/auth/qrcode/controller/QrCodeAuthController.java
@@ -1,14 +1,14 @@
-package com.youlai.boot.auth.controller;
+package com.youlai.boot.auth.qrcode.controller;
-import com.youlai.boot.auth.model.req.QrCodeTicketReq;
-import com.youlai.boot.auth.model.resp.QrCodeGenerateResp;
-import com.youlai.boot.auth.model.resp.QrCodeStatusResp;
-import com.youlai.boot.auth.service.QrCodeLoginService;
-import com.youlai.boot.common.util.IPUtils;
-import com.youlai.boot.common.annotation.RateLimit;
+import com.youlai.boot.auth.qrcode.model.form.QrCodeTicketForm;
+import com.youlai.boot.auth.qrcode.model.vo.QrCodeGenerateVO;
+import com.youlai.boot.auth.qrcode.model.vo.QrCodeStatusVO;
+import com.youlai.boot.auth.qrcode.service.QrCodeLoginService;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.framework.security.util.SecurityUtils;
+import com.youlai.boot.framework.web.ratelimit.RateLimit;
+import com.youlai.boot.framework.web.util.IPUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
@@ -44,7 +44,7 @@ public class QrCodeAuthController {
@Operation(summary = "[扫码]生成扫码登录票据")
@PostMapping("/generate")
@RateLimit(limit = 30, window = 60)
- public Result generate(HttpServletRequest request) {
+ public Result generate(HttpServletRequest request) {
return Result.success(qrCodeLoginService.generate(IPUtils.getIpAddr(request)));
}
@@ -54,7 +54,7 @@ public class QrCodeAuthController {
@Operation(summary = "[扫码]查询扫码状态")
@GetMapping("/status")
@RateLimit(limit = 60, window = 60)
- public Result status(@RequestParam String ticket) {
+ public Result status(@RequestParam String ticket) {
return Result.success(qrCodeLoginService.status(ticket));
}
@@ -63,7 +63,7 @@ public class QrCodeAuthController {
*/
@Operation(summary = "[扫码]APP 标记已扫码")
@PostMapping("/scan")
- public Result scan(@RequestBody @Valid QrCodeTicketReq form) {
+ public Result scan(@RequestBody @Valid QrCodeTicketForm form) {
return Result.success(qrCodeLoginService.scan(form.getTicket(), SecurityUtils.getUserId()));
}
@@ -72,7 +72,7 @@ public class QrCodeAuthController {
*/
@Operation(summary = "[扫码]APP 确认登录")
@PostMapping("/confirm")
- public Result confirm(@RequestBody @Valid QrCodeTicketReq form) {
+ public Result confirm(@RequestBody @Valid QrCodeTicketForm form) {
return Result.success(qrCodeLoginService.confirm(form.getTicket(), SecurityUtils.getUserId()));
}
@@ -81,7 +81,7 @@ public class QrCodeAuthController {
*/
@Operation(summary = "[扫码]APP 取消登录")
@PostMapping("/cancel")
- public Result cancel(@RequestBody @Valid QrCodeTicketReq form) {
+ public Result cancel(@RequestBody @Valid QrCodeTicketForm form) {
return Result.success(qrCodeLoginService.cancel(form.getTicket(), SecurityUtils.getUserId()));
}
@@ -90,7 +90,7 @@ public class QrCodeAuthController {
*/
@Operation(summary = "[扫码]PC 端换取会话令牌")
@PostMapping("/login")
- public Result login(@RequestBody @Valid QrCodeTicketReq form) {
+ public Result login(@RequestBody @Valid QrCodeTicketForm form) {
return Result.success(qrCodeLoginService.login(form.getTicket()));
}
}
diff --git a/src/main/java/com/youlai/boot/auth/enums/QrCodeLoginStatusEnum.java b/src/main/java/com/youlai/boot/auth/qrcode/enums/QrCodeLoginStatusEnum.java
similarity index 92%
rename from src/main/java/com/youlai/boot/auth/enums/QrCodeLoginStatusEnum.java
rename to src/main/java/com/youlai/boot/auth/qrcode/enums/QrCodeLoginStatusEnum.java
index 181bca5d..94c6d37e 100644
--- a/src/main/java/com/youlai/boot/auth/enums/QrCodeLoginStatusEnum.java
+++ b/src/main/java/com/youlai/boot/auth/qrcode/enums/QrCodeLoginStatusEnum.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.enums;
+package com.youlai.boot.auth.qrcode.enums;
/**
* 扫码登录票据状态
diff --git a/src/main/java/com/youlai/boot/auth/model/dto/QrCodeLoginContext.java b/src/main/java/com/youlai/boot/auth/qrcode/model/context/QrCodeLoginContext.java
similarity index 83%
rename from src/main/java/com/youlai/boot/auth/model/dto/QrCodeLoginContext.java
rename to src/main/java/com/youlai/boot/auth/qrcode/model/context/QrCodeLoginContext.java
index 357b67d6..95885e0d 100644
--- a/src/main/java/com/youlai/boot/auth/model/dto/QrCodeLoginContext.java
+++ b/src/main/java/com/youlai/boot/auth/qrcode/model/context/QrCodeLoginContext.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.model.dto;
+package com.youlai.boot.auth.qrcode.model.context;
import lombok.Data;
@@ -14,7 +14,7 @@ public class QrCodeLoginContext {
/** 票据,UUID 无连字符 */
private String ticket;
- /** 状态枚举名,取 {@link com.youlai.boot.auth.enums.QrCodeLoginStatusEnum#name()} */
+ /** 状态枚举名,取 {@link com.youlai.boot.auth.qrcode.enums.QrCodeLoginStatusEnum#name()} */
private String status;
/** 扫码用户 ID,scan 时写入 */
diff --git a/src/main/java/com/youlai/boot/auth/model/req/QrCodeTicketReq.java b/src/main/java/com/youlai/boot/auth/qrcode/model/form/QrCodeTicketForm.java
similarity index 77%
rename from src/main/java/com/youlai/boot/auth/model/req/QrCodeTicketReq.java
rename to src/main/java/com/youlai/boot/auth/qrcode/model/form/QrCodeTicketForm.java
index 9e8e86d1..a847b159 100644
--- a/src/main/java/com/youlai/boot/auth/model/req/QrCodeTicketReq.java
+++ b/src/main/java/com/youlai/boot/auth/qrcode/model/form/QrCodeTicketForm.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.model.req;
+package com.youlai.boot.auth.qrcode.model.form;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@@ -10,7 +10,7 @@ import lombok.Data;
* @since 4.5.0
*/
@Data
-public class QrCodeTicketReq {
+public class QrCodeTicketForm {
@NotBlank(message = "票据不能为空")
private String ticket;
diff --git a/src/main/java/com/youlai/boot/auth/model/resp/QrCodeGenerateResp.java b/src/main/java/com/youlai/boot/auth/qrcode/model/vo/QrCodeGenerateVO.java
similarity index 83%
rename from src/main/java/com/youlai/boot/auth/model/resp/QrCodeGenerateResp.java
rename to src/main/java/com/youlai/boot/auth/qrcode/model/vo/QrCodeGenerateVO.java
index dc830b15..5982b0dd 100644
--- a/src/main/java/com/youlai/boot/auth/model/resp/QrCodeGenerateResp.java
+++ b/src/main/java/com/youlai/boot/auth/qrcode/model/vo/QrCodeGenerateVO.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.model.resp;
+package com.youlai.boot.auth.qrcode.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
@@ -13,7 +13,7 @@ import lombok.Data;
@Data
@Builder
@Schema(description = "扫码票据生成结果")
-public class QrCodeGenerateResp {
+public class QrCodeGenerateVO {
@Schema(description = "票据")
private String ticket;
diff --git a/src/main/java/com/youlai/boot/auth/model/resp/QrCodeStatusResp.java b/src/main/java/com/youlai/boot/auth/qrcode/model/vo/QrCodeStatusVO.java
similarity index 90%
rename from src/main/java/com/youlai/boot/auth/model/resp/QrCodeStatusResp.java
rename to src/main/java/com/youlai/boot/auth/qrcode/model/vo/QrCodeStatusVO.java
index b2ea3a2d..d3ef62b7 100644
--- a/src/main/java/com/youlai/boot/auth/model/resp/QrCodeStatusResp.java
+++ b/src/main/java/com/youlai/boot/auth/qrcode/model/vo/QrCodeStatusVO.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.model.resp;
+package com.youlai.boot.auth.qrcode.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
@@ -13,7 +13,7 @@ import lombok.Data;
@Data
@Builder
@Schema(description = "扫码登录状态")
-public class QrCodeStatusResp {
+public class QrCodeStatusVO {
@Schema(description = "票据")
private String ticket;
diff --git a/src/main/java/com/youlai/boot/auth/service/QrCodeLoginService.java b/src/main/java/com/youlai/boot/auth/qrcode/service/QrCodeLoginService.java
similarity index 92%
rename from src/main/java/com/youlai/boot/auth/service/QrCodeLoginService.java
rename to src/main/java/com/youlai/boot/auth/qrcode/service/QrCodeLoginService.java
index cd807d75..66ed0e86 100644
--- a/src/main/java/com/youlai/boot/auth/service/QrCodeLoginService.java
+++ b/src/main/java/com/youlai/boot/auth/qrcode/service/QrCodeLoginService.java
@@ -1,14 +1,14 @@
-package com.youlai.boot.auth.service;
+package com.youlai.boot.auth.qrcode.service;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
-import com.youlai.boot.auth.enums.QrCodeLoginStatusEnum;
-import com.youlai.boot.auth.model.dto.QrCodeLoginContext;
-import com.youlai.boot.auth.model.resp.QrCodeGenerateResp;
-import com.youlai.boot.auth.model.resp.QrCodeStatusResp;
+import com.youlai.boot.auth.qrcode.enums.QrCodeLoginStatusEnum;
+import com.youlai.boot.auth.qrcode.model.context.QrCodeLoginContext;
+import com.youlai.boot.auth.qrcode.model.vo.QrCodeGenerateVO;
+import com.youlai.boot.auth.qrcode.model.vo.QrCodeStatusVO;
import com.youlai.boot.common.constant.RedisConstants;
-import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.exception.TokenInvalidException;
+import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.framework.security.model.SecurityUser;
import com.youlai.boot.framework.security.model.SecurityUserDetails;
@@ -54,7 +54,7 @@ public class QrCodeLoginService {
/**
* 生成扫码票据,写入 Redis 后返回 ticket 和有效期
*/
- public QrCodeGenerateResp generate(String clientIp) {
+ public QrCodeGenerateVO generate(String clientIp) {
String ticket = IdUtil.fastSimpleUUID();
QrCodeLoginContext ctx = new QrCodeLoginContext();
ctx.setTicket(ticket);
@@ -62,7 +62,7 @@ public class QrCodeLoginService {
ctx.setCreatedAt(System.currentTimeMillis());
ctx.setClientIp(clientIp);
save(ctx, DEFAULT_EXPIRE_SECONDS);
- return QrCodeGenerateResp.builder()
+ return QrCodeGenerateVO.builder()
.ticket(ticket)
.expireSeconds(DEFAULT_EXPIRE_SECONDS)
.build();
@@ -71,7 +71,7 @@ public class QrCodeLoginService {
/**
* PC 端轮询扫码状态,未登录调用
*/
- public QrCodeStatusResp status(String ticket) {
+ public QrCodeStatusVO status(String ticket) {
QrCodeLoginContext ctx = loadOrThrow(ticket);
return toResp(ctx, remainingSeconds(ticket));
}
@@ -79,7 +79,7 @@ public class QrCodeLoginService {
/**
* APP 端标记已扫码,需已登录。WAITING → SCANNED
*/
- public QrCodeStatusResp scan(String ticket, Long userId) {
+ public QrCodeStatusVO scan(String ticket, Long userId) {
QrCodeLoginContext ctx = loadOrThrow(ticket);
requireStatus(ctx, QrCodeLoginStatusEnum.WAITING);
fillUserInfo(ctx, userId);
@@ -92,7 +92,7 @@ public class QrCodeLoginService {
/**
* APP 端确认登录,需扫码本人操作。SCANNED → CONFIRMED
*/
- public QrCodeStatusResp confirm(String ticket, Long userId) {
+ public QrCodeStatusVO confirm(String ticket, Long userId) {
QrCodeLoginContext ctx = loadOrThrow(ticket);
requireStatus(ctx, QrCodeLoginStatusEnum.SCANNED);
requireSameUser(ctx, userId);
@@ -105,7 +105,7 @@ public class QrCodeLoginService {
/**
* APP 端取消登录,仅流程中的票据可取消
*/
- public QrCodeStatusResp cancel(String ticket, Long userId) {
+ public QrCodeStatusVO cancel(String ticket, Long userId) {
QrCodeLoginContext ctx = loadOrThrow(ticket);
QrCodeLoginStatusEnum current = QrCodeLoginStatusEnum.valueOf(ctx.getStatus());
// 只有还在流程中的票据(等待扫码 / 已扫码 / 已确认)允许取消
@@ -223,7 +223,7 @@ public class QrCodeLoginService {
/**
* 上下文转前端 VO,昵称脱敏、用户信息仅在扫码后暴露
*/
- private QrCodeStatusResp toResp(QrCodeLoginContext ctx, int expireSeconds) {
+ private QrCodeStatusVO toResp(QrCodeLoginContext ctx, int expireSeconds) {
QrCodeLoginStatusEnum status = QrCodeLoginStatusEnum.valueOf(ctx.getStatus());
String nickname = null;
String avatar = null;
@@ -233,7 +233,7 @@ public class QrCodeLoginService {
nickname = maskNickname(ctx.getNickname());
avatar = ctx.getAvatar();
}
- return QrCodeStatusResp.builder()
+ return QrCodeStatusVO.builder()
.ticket(ctx.getTicket())
.status(status.name())
.nickname(nickname)
diff --git a/src/main/java/com/youlai/boot/auth/controller/AuthController.java b/src/main/java/com/youlai/boot/auth/session/controller/AuthSessionController.java
similarity index 52%
rename from src/main/java/com/youlai/boot/auth/controller/AuthController.java
rename to src/main/java/com/youlai/boot/auth/session/controller/AuthSessionController.java
index f29a684a..960be83d 100644
--- a/src/main/java/com/youlai/boot/auth/controller/AuthController.java
+++ b/src/main/java/com/youlai/boot/auth/session/controller/AuthSessionController.java
@@ -1,64 +1,42 @@
-package com.youlai.boot.auth.controller;
+package com.youlai.boot.auth.session.controller;
-import com.youlai.boot.auth.model.req.LoginReq;
+import com.youlai.boot.auth.session.service.AuthSessionService;
+import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.result.Result;
-import com.youlai.boot.auth.service.AuthService;
-import com.youlai.boot.common.annotation.Log;
-import com.youlai.boot.common.annotation.RateLimit;
-import com.youlai.boot.auth.model.resp.CaptchaInfo;
import com.youlai.boot.framework.security.model.AuthenticationToken;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
- * 认证控制层
+ * 登录态管理接口
+ *
+ * 注销与刷新令牌,各登录方式共用。
*
* @author Ray.Hao
- * @since 0.0.1
+ * @since 4.8.0
*/
@Tag(name = "01.认证中心")
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
-@Slf4j
-public class AuthController {
+public class AuthSessionController {
- private final AuthService authService;
-
- @Operation(summary = "获取验证码")
- @GetMapping("/captcha")
- public Result getCaptcha() {
- CaptchaInfo captcha = authService.getCaptcha();
- return Result.success(captcha);
- }
-
- @Operation(summary = "账号密码登录")
- @PostMapping("/login")
- @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
- @RateLimit
- public Result login(@RequestBody @Valid LoginReq request) {
- AuthenticationToken authenticationToken = authService.login(request.getUsername(), request.getPassword());
- return Result.success(authenticationToken);
- }
+ private final AuthSessionService authSessionService;
@Operation(summary = "退出登录")
@DeleteMapping("/logout")
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGOUT)
public Result logout() {
- authService.logout();
+ authSessionService.logout();
return Result.success();
}
@@ -67,7 +45,7 @@ public class AuthController {
public Result refreshToken(
@Parameter(description = "刷新令牌", example = "xxx.xxx.xxx") @RequestParam String refreshToken
) {
- AuthenticationToken authenticationToken = authService.refreshToken(refreshToken);
+ AuthenticationToken authenticationToken = authSessionService.refreshToken(refreshToken);
return Result.success(authenticationToken);
}
diff --git a/src/main/java/com/youlai/boot/auth/session/service/AuthSessionService.java b/src/main/java/com/youlai/boot/auth/session/service/AuthSessionService.java
new file mode 100644
index 00000000..c5f8860f
--- /dev/null
+++ b/src/main/java/com/youlai/boot/auth/session/service/AuthSessionService.java
@@ -0,0 +1,47 @@
+package com.youlai.boot.auth.session.service;
+
+import cn.hutool.core.util.StrUtil;
+import com.youlai.boot.framework.security.model.AuthenticationToken;
+import com.youlai.boot.framework.security.token.TokenManager;
+import com.youlai.boot.framework.security.util.SecurityUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Service;
+
+/**
+ * 登录态服务
+ *
+ * 承载各登录方式共用的注销与令牌刷新。
+ *
+ * @author Ray.Hao
+ * @since 4.8.0
+ */
+@Service
+@RequiredArgsConstructor
+public class AuthSessionService {
+
+ private final TokenManager tokenManager;
+
+ /**
+ * 注销登录
+ */
+ public void logout() {
+ String token = SecurityUtils.getAccessToken();
+ if (StrUtil.isNotBlank(token)) {
+ tokenManager.invalidateToken(token);
+ // 清除Security上下文
+ SecurityContextHolder.clearContext();
+ }
+ }
+
+ /**
+ * 刷新令牌
+ *
+ * @param refreshToken 刷新令牌
+ * @return 新的访问令牌
+ */
+ public AuthenticationToken refreshToken(String refreshToken) {
+ return tokenManager.refreshToken(refreshToken);
+ }
+
+}
diff --git a/src/main/java/com/youlai/boot/auth/controller/SmsAuthController.java b/src/main/java/com/youlai/boot/auth/sms/controller/SmsAuthController.java
similarity index 82%
rename from src/main/java/com/youlai/boot/auth/controller/SmsAuthController.java
rename to src/main/java/com/youlai/boot/auth/sms/controller/SmsAuthController.java
index 8eba7c72..466e2536 100644
--- a/src/main/java/com/youlai/boot/auth/controller/SmsAuthController.java
+++ b/src/main/java/com/youlai/boot/auth/sms/controller/SmsAuthController.java
@@ -1,17 +1,16 @@
-package com.youlai.boot.auth.controller;
+package com.youlai.boot.auth.sms.controller;
-import com.youlai.boot.auth.service.AuthService;
+import com.youlai.boot.auth.sms.service.SmsAuthService;
+import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.result.Result;
-import com.youlai.boot.common.annotation.Log;
-import com.youlai.boot.common.annotation.RateLimit;
import com.youlai.boot.framework.security.model.AuthenticationToken;
+import com.youlai.boot.framework.web.ratelimit.RateLimit;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -27,10 +26,9 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/auth/sms")
@RequiredArgsConstructor
-@Slf4j
public class SmsAuthController {
- private final AuthService authService;
+ private final SmsAuthService smsAuthService;
@Operation(summary = "[短信]发送登录短信验证码")
@PostMapping("/code")
@@ -38,7 +36,7 @@ public class SmsAuthController {
public Result sendCode(
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile
) {
- authService.sendSmsCode(mobile);
+ smsAuthService.sendSmsCode(mobile);
return Result.success();
}
@@ -49,7 +47,7 @@ public class SmsAuthController {
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile,
@Parameter(description = "验证码", example = "123456") @RequestParam String code
) {
- AuthenticationToken loginResult = authService.loginBySms(mobile, code);
+ AuthenticationToken loginResult = smsAuthService.loginBySms(mobile, code);
return Result.success(loginResult);
}
diff --git a/src/main/java/com/youlai/boot/auth/exception/SmsCaptchaException.java b/src/main/java/com/youlai/boot/auth/sms/exception/SmsCaptchaException.java
similarity index 86%
rename from src/main/java/com/youlai/boot/auth/exception/SmsCaptchaException.java
rename to src/main/java/com/youlai/boot/auth/sms/exception/SmsCaptchaException.java
index a49e50f8..809153de 100644
--- a/src/main/java/com/youlai/boot/auth/exception/SmsCaptchaException.java
+++ b/src/main/java/com/youlai/boot/auth/sms/exception/SmsCaptchaException.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.exception;
+package com.youlai.boot.auth.sms.exception;
import org.springframework.security.core.AuthenticationException;
diff --git a/src/main/java/com/youlai/boot/auth/security/provider/SmsAuthenticationProvider.java b/src/main/java/com/youlai/boot/auth/sms/provider/SmsAuthenticationProvider.java
similarity index 95%
rename from src/main/java/com/youlai/boot/auth/security/provider/SmsAuthenticationProvider.java
rename to src/main/java/com/youlai/boot/auth/sms/provider/SmsAuthenticationProvider.java
index 3423a033..dc5d9592 100644
--- a/src/main/java/com/youlai/boot/auth/security/provider/SmsAuthenticationProvider.java
+++ b/src/main/java/com/youlai/boot/auth/sms/provider/SmsAuthenticationProvider.java
@@ -1,13 +1,13 @@
-package com.youlai.boot.auth.security.provider;
+package com.youlai.boot.auth.sms.provider;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
+import com.youlai.boot.auth.sms.exception.SmsCaptchaException;
+import com.youlai.boot.auth.sms.token.SmsAuthenticationToken;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.framework.security.model.SecurityUser;
import com.youlai.boot.framework.security.model.SecurityUserDetails;
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
-import com.youlai.boot.auth.exception.SmsCaptchaException;
-import com.youlai.boot.auth.security.token.SmsAuthenticationToken;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
diff --git a/src/main/java/com/youlai/boot/auth/service/AuthService.java b/src/main/java/com/youlai/boot/auth/sms/service/SmsAuthService.java
similarity index 51%
rename from src/main/java/com/youlai/boot/auth/service/AuthService.java
rename to src/main/java/com/youlai/boot/auth/sms/service/SmsAuthService.java
index acca88df..f032919e 100644
--- a/src/main/java/com/youlai/boot/auth/service/AuthService.java
+++ b/src/main/java/com/youlai/boot/auth/sms/service/SmsAuthService.java
@@ -1,19 +1,16 @@
-package com.youlai.boot.auth.service;
+package com.youlai.boot.auth.sms.service;
import cn.hutool.core.util.StrUtil;
+import com.youlai.boot.auth.sms.token.SmsAuthenticationToken;
import com.youlai.boot.common.constant.RedisConstants;
-import com.youlai.boot.auth.model.resp.CaptchaInfo;
import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.framework.security.token.TokenManager;
-import com.youlai.boot.auth.security.token.SmsAuthenticationToken;
-import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.support.sms.SmsTypeEnum;
-import com.youlai.boot.support.sms.SmsService;
+import com.youlai.boot.support.sms.service.SmsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
-import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@@ -23,49 +20,21 @@ import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
- * 认证服务
+ * 短信验证码登录服务
*
* @author Ray.Hao
- * @since 2.4.0
+ * @since 4.8.0
*/
@Service
@RequiredArgsConstructor
@Slf4j
-public class AuthService {
+public class SmsAuthService {
private final AuthenticationManager authenticationManager;
private final TokenManager tokenManager;
private final SmsService smsService;
private final RedisTemplate redisTemplate;
- private final CaptchaService captchaService;
-
- /**
- * 用户名密码登录
- *
- * @param username 用户名
- * @param password 密码
- * @return 访问令牌
- */
- public AuthenticationToken login(String username, String password) {
- // 1. 创建用于密码认证的令牌(未认证)
- UsernamePasswordAuthenticationToken authenticationToken =
- new UsernamePasswordAuthenticationToken(username.trim(), password);
-
- // 2. 执行认证(认证中)
- // 说明:这里的认证流程由 Spring Security 提供的 AuthenticationManager 执行。
- // 默认情况下会委托给 DaoAuthenticationProvider:
- // 1) retrieveUser(...):内部通过 UserDetailsService.loadUserByUsername(...) 获取用户信息(本项目为 SecurityUserDetailsService 实现)
- // 2) additionalAuthenticationChecks(...):对比请求密码与用户存储密码(由 PasswordEncoder 完成匹配)
- // 认证通过后返回已认证的 Authentication(principal 为 SecurityUserDetails,authorities 为角色/权限集合)。
- Authentication authentication = authenticationManager.authenticate(authenticationToken);
-
- // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
- AuthenticationToken authenticationTokenResponse =
- tokenManager.generateToken(authentication);
- SecurityContextHolder.getContext().setAuthentication(authentication);
- return authenticationTokenResponse;
- }
/**
* 发送登录短信验证码
@@ -111,33 +80,4 @@ public class AuthService {
return authenticationToken;
}
- /**
- * 注销登录
- */
- public void logout() {
- String token = SecurityUtils.getAccessToken();
- if (StrUtil.isNotBlank(token)) {
- tokenManager.invalidateToken(token);
- // 清除Security上下文
- SecurityContextHolder.clearContext();
- }
- }
-
- /**
- * 获取验证码
- */
- public CaptchaInfo getCaptcha() {
- return captchaService.generate();
- }
-
- /**
- * 刷新token
- *
- * @param refreshToken 刷新令牌
- * @return 新的访问令牌
- */
- public AuthenticationToken refreshToken(String refreshToken) {
- return tokenManager.refreshToken(refreshToken);
- }
-
}
diff --git a/src/main/java/com/youlai/boot/auth/security/token/SmsAuthenticationToken.java b/src/main/java/com/youlai/boot/auth/sms/token/SmsAuthenticationToken.java
similarity index 97%
rename from src/main/java/com/youlai/boot/auth/security/token/SmsAuthenticationToken.java
rename to src/main/java/com/youlai/boot/auth/sms/token/SmsAuthenticationToken.java
index e04de05f..83e8ac50 100644
--- a/src/main/java/com/youlai/boot/auth/security/token/SmsAuthenticationToken.java
+++ b/src/main/java/com/youlai/boot/auth/sms/token/SmsAuthenticationToken.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.security.token;
+package com.youlai.boot.auth.sms.token;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
diff --git a/src/main/java/com/youlai/boot/config/WxMaConfig.java b/src/main/java/com/youlai/boot/auth/wxma/config/WxMaConfig.java
similarity index 75%
rename from src/main/java/com/youlai/boot/config/WxMaConfig.java
rename to src/main/java/com/youlai/boot/auth/wxma/config/WxMaConfig.java
index 1093044b..28b80b74 100644
--- a/src/main/java/com/youlai/boot/config/WxMaConfig.java
+++ b/src/main/java/com/youlai/boot/auth/wxma/config/WxMaConfig.java
@@ -1,10 +1,8 @@
-package com.youlai.boot.config;
+package com.youlai.boot.auth.wxma.config;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
-import com.youlai.boot.config.property.WxMaProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -12,12 +10,17 @@ import org.springframework.context.annotation.Configuration;
* 微信小程序配置
*
* @author Ray.Hao
- * @since 2024/01/01
+ * @since 1.0.0
*/
@Configuration
-@EnableConfigurationProperties(WxMaProperties.class)
public class WxMaConfig {
+ /**
+ * 微信小程序客户端,用于调用 jscode2session、手机号解析等接口
+ *
+ * @param properties 小程序 appid 与密钥
+ * @return WxMaService
+ */
@Bean
public WxMaService wxMaService(WxMaProperties properties) {
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
diff --git a/src/main/java/com/youlai/boot/config/property/WxMaProperties.java b/src/main/java/com/youlai/boot/auth/wxma/config/WxMaProperties.java
similarity index 69%
rename from src/main/java/com/youlai/boot/config/property/WxMaProperties.java
rename to src/main/java/com/youlai/boot/auth/wxma/config/WxMaProperties.java
index d4333c17..428c8709 100644
--- a/src/main/java/com/youlai/boot/config/property/WxMaProperties.java
+++ b/src/main/java/com/youlai/boot/auth/wxma/config/WxMaProperties.java
@@ -1,16 +1,20 @@
-package com.youlai.boot.config.property;
+package com.youlai.boot.auth.wxma.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 微信小程序配置属性
+ *
+ * @author Ray.Hao
*/
@Data
@ConfigurationProperties(prefix = "wx.miniapp")
public class WxMaProperties {
+ /** 小程序 appid */
private String appid;
+ /** 小程序 appsecret */
private String secret;
}
diff --git a/src/main/java/com/youlai/boot/auth/controller/WxMaAuthController.java b/src/main/java/com/youlai/boot/auth/wxma/controller/WxMaAuthController.java
similarity index 86%
rename from src/main/java/com/youlai/boot/auth/controller/WxMaAuthController.java
rename to src/main/java/com/youlai/boot/auth/wxma/controller/WxMaAuthController.java
index 1cb64564..8ff8e019 100644
--- a/src/main/java/com/youlai/boot/auth/controller/WxMaAuthController.java
+++ b/src/main/java/com/youlai/boot/auth/wxma/controller/WxMaAuthController.java
@@ -1,28 +1,26 @@
-package com.youlai.boot.auth.controller;
+package com.youlai.boot.auth.wxma.controller;
-import com.youlai.boot.auth.model.req.WxMaBindMobileReq;
-import com.youlai.boot.auth.model.req.WxMaPhoneLoginReq;
-import com.youlai.boot.auth.model.resp.WxMaLoginResp;
-import com.youlai.boot.auth.service.WxMaAuthService;
+import com.youlai.boot.auth.wxma.model.form.WxMaBindMobileForm;
+import com.youlai.boot.auth.wxma.model.form.WxMaPhoneLoginForm;
+import com.youlai.boot.auth.wxma.model.vo.WxMaLoginVO;
+import com.youlai.boot.auth.wxma.service.WxMaAuthService;
import com.youlai.boot.common.annotation.Log;
-import com.youlai.boot.common.annotation.RateLimit;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.framework.security.model.AuthenticationToken;
+import com.youlai.boot.framework.web.ratelimit.RateLimit;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
-import jakarta.validation.Valid;
-
/**
* 微信小程序认证控制层
*
@@ -33,7 +31,6 @@ import jakarta.validation.Valid;
@RestController
@RequestMapping("/api/v1/auth/wxma")
@RequiredArgsConstructor
-@Slf4j
public class WxMaAuthController {
private final WxMaAuthService wxMaAuthService;
@@ -51,11 +48,11 @@ public class WxMaAuthController {
@PostMapping("/silent-login")
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
@RateLimit
- public Result silentLogin(
+ public Result silentLogin(
@Parameter(description = "微信登录凭证(wx.login 获取)", required = true, example = "0xxx")
@RequestParam String code
) {
- WxMaLoginResp result = wxMaAuthService.silentLogin(code);
+ WxMaLoginVO result = wxMaAuthService.silentLogin(code);
return Result.success(result);
}
@@ -70,7 +67,7 @@ public class WxMaAuthController {
@PostMapping("/phone-login")
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
@RateLimit
- public Result phoneLogin(@Valid @RequestBody WxMaPhoneLoginReq req) {
+ public Result phoneLogin(@Valid @RequestBody WxMaPhoneLoginForm req) {
AuthenticationToken result = wxMaAuthService.phoneLogin(req.getLoginCode(), req.getPhoneCode());
return Result.success(result);
}
@@ -86,7 +83,7 @@ public class WxMaAuthController {
@PostMapping("/bind-mobile")
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
@RateLimit
- public Result bindMobile(@Valid @RequestBody WxMaBindMobileReq req) {
+ public Result bindMobile(@Valid @RequestBody WxMaBindMobileForm req) {
AuthenticationToken result = wxMaAuthService.bindMobile(req.getOpenid(), req.getMobile(), req.getSmsCode());
return Result.success(result);
}
diff --git a/src/main/java/com/youlai/boot/common/exception/MobileNotBoundException.java b/src/main/java/com/youlai/boot/auth/wxma/exception/MobileNotBoundException.java
similarity index 93%
rename from src/main/java/com/youlai/boot/common/exception/MobileNotBoundException.java
rename to src/main/java/com/youlai/boot/auth/wxma/exception/MobileNotBoundException.java
index 48c50ac3..68ff1f1f 100644
--- a/src/main/java/com/youlai/boot/common/exception/MobileNotBoundException.java
+++ b/src/main/java/com/youlai/boot/auth/wxma/exception/MobileNotBoundException.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.common.exception;
+package com.youlai.boot.auth.wxma.exception;
import org.springframework.security.core.AuthenticationException;
@@ -13,6 +13,7 @@ import org.springframework.security.core.AuthenticationException;
public class MobileNotBoundException extends AuthenticationException {
private final String openid;
+
private final String sessionKey;
public MobileNotBoundException(String openid, String sessionKey) {
diff --git a/src/main/java/com/youlai/boot/auth/model/req/WxMaBindMobileReq.java b/src/main/java/com/youlai/boot/auth/wxma/model/form/WxMaBindMobileForm.java
similarity index 87%
rename from src/main/java/com/youlai/boot/auth/model/req/WxMaBindMobileReq.java
rename to src/main/java/com/youlai/boot/auth/wxma/model/form/WxMaBindMobileForm.java
index 2dfea5c2..bb7c353f 100644
--- a/src/main/java/com/youlai/boot/auth/model/req/WxMaBindMobileReq.java
+++ b/src/main/java/com/youlai/boot/auth/wxma/model/form/WxMaBindMobileForm.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.model.req;
+package com.youlai.boot.auth.wxma.model.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@@ -6,10 +6,12 @@ import lombok.Data;
/**
* 微信小程序绑定手机号请求
+ *
+ * @author Ray.Hao
*/
@Schema(description = "微信小程序绑定手机号请求")
@Data
-public class WxMaBindMobileReq {
+public class WxMaBindMobileForm {
@NotBlank(message = "openid 不能为空")
@Schema(description = "微信用户唯一标识", example = "oVBkZ0aYgDMDIywRdgPW8-joxXc4")
diff --git a/src/main/java/com/youlai/boot/auth/model/req/WxMaPhoneLoginReq.java b/src/main/java/com/youlai/boot/auth/wxma/model/form/WxMaPhoneLoginForm.java
similarity index 85%
rename from src/main/java/com/youlai/boot/auth/model/req/WxMaPhoneLoginReq.java
rename to src/main/java/com/youlai/boot/auth/wxma/model/form/WxMaPhoneLoginForm.java
index 0fa3041b..f8c568dd 100644
--- a/src/main/java/com/youlai/boot/auth/model/req/WxMaPhoneLoginReq.java
+++ b/src/main/java/com/youlai/boot/auth/wxma/model/form/WxMaPhoneLoginForm.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.model.req;
+package com.youlai.boot.auth.wxma.model.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@@ -6,10 +6,12 @@ import lombok.Data;
/**
* 微信小程序手机号快捷登录请求
+ *
+ * @author Ray.Hao
*/
@Schema(description = "微信小程序手机号快捷登录请求")
@Data
-public class WxMaPhoneLoginReq {
+public class WxMaPhoneLoginForm {
@NotBlank(message = "微信登录凭证不能为空")
@Schema(description = "微信登录凭证(wx.login 获取)", example = "0xxx")
diff --git a/src/main/java/com/youlai/boot/auth/model/resp/WxMaLoginResp.java b/src/main/java/com/youlai/boot/auth/wxma/model/vo/WxMaLoginVO.java
similarity index 90%
rename from src/main/java/com/youlai/boot/auth/model/resp/WxMaLoginResp.java
rename to src/main/java/com/youlai/boot/auth/wxma/model/vo/WxMaLoginVO.java
index 5e168d8a..62552565 100644
--- a/src/main/java/com/youlai/boot/auth/model/resp/WxMaLoginResp.java
+++ b/src/main/java/com/youlai/boot/auth/wxma/model/vo/WxMaLoginVO.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.model.resp;
+package com.youlai.boot.auth.wxma.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
@@ -8,13 +8,15 @@ import lombok.NoArgsConstructor;
/**
* 微信小程序登录响应
+ *
+ * @author Ray.Hao
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "微信小程序登录响应")
-public class WxMaLoginResp {
+public class WxMaLoginVO {
@Schema(description = "是否新用户")
private Boolean isNewUser;
diff --git a/src/main/java/com/youlai/boot/auth/security/provider/WxMaAuthenticationProvider.java b/src/main/java/com/youlai/boot/auth/wxma/provider/WxMaAuthenticationProvider.java
similarity index 91%
rename from src/main/java/com/youlai/boot/auth/security/provider/WxMaAuthenticationProvider.java
rename to src/main/java/com/youlai/boot/auth/wxma/provider/WxMaAuthenticationProvider.java
index 7ebfc92c..10ce36f2 100644
--- a/src/main/java/com/youlai/boot/auth/security/provider/WxMaAuthenticationProvider.java
+++ b/src/main/java/com/youlai/boot/auth/wxma/provider/WxMaAuthenticationProvider.java
@@ -1,14 +1,14 @@
-package com.youlai.boot.auth.security.provider;
+package com.youlai.boot.auth.wxma.provider;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.hutool.core.util.ObjectUtil;
+import com.youlai.boot.auth.wxma.exception.MobileNotBoundException;
+import com.youlai.boot.auth.wxma.token.WxMaAuthenticationToken;
import com.youlai.boot.common.enums.SocialPlatformEnum;
import com.youlai.boot.framework.security.model.SecurityUser;
import com.youlai.boot.framework.security.model.SecurityUserDetails;
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
-import com.youlai.boot.common.exception.MobileNotBoundException;
-import com.youlai.boot.auth.security.token.WxMaAuthenticationToken;
import com.youlai.boot.system.model.entity.UserSocial;
import com.youlai.boot.system.service.UserSocialService;
import lombok.RequiredArgsConstructor;
@@ -22,6 +22,10 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
/**
* 微信小程序认证 Provider
+ *
+ * 用微信登录凭证换取 openid,已绑定手机号的用户返回认证结果,未绑定抛出 MobileNotBoundException。
+ *
+ * @author Ray.Hao
*/
@Slf4j
@RequiredArgsConstructor
diff --git a/src/main/java/com/youlai/boot/auth/service/WxMaAuthService.java b/src/main/java/com/youlai/boot/auth/wxma/service/WxMaAuthService.java
similarity index 94%
rename from src/main/java/com/youlai/boot/auth/service/WxMaAuthService.java
rename to src/main/java/com/youlai/boot/auth/wxma/service/WxMaAuthService.java
index aff402bb..21886847 100644
--- a/src/main/java/com/youlai/boot/auth/service/WxMaAuthService.java
+++ b/src/main/java/com/youlai/boot/auth/wxma/service/WxMaAuthService.java
@@ -1,22 +1,22 @@
-package com.youlai.boot.auth.service;
+package com.youlai.boot.auth.wxma.service;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
-import com.youlai.boot.auth.model.resp.WxMaLoginResp;
+import com.youlai.boot.auth.wxma.exception.MobileNotBoundException;
+import com.youlai.boot.auth.wxma.model.vo.WxMaLoginVO;
+import com.youlai.boot.auth.wxma.token.WxMaAuthenticationToken;
import com.youlai.boot.common.constant.RedisConstants;
+import com.youlai.boot.common.enums.SocialPlatformEnum;
import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.framework.security.model.SecurityUserDetails;
import com.youlai.boot.framework.security.token.TokenManager;
-import com.youlai.boot.common.exception.MobileNotBoundException;
-import com.youlai.boot.auth.security.token.WxMaAuthenticationToken;
-import com.youlai.boot.common.enums.SocialPlatformEnum;
import com.youlai.boot.system.model.entity.SysUser;
-import com.youlai.boot.system.service.UserSocialService;
-import com.youlai.boot.system.service.UserService;
import com.youlai.boot.system.service.UserRoleService;
+import com.youlai.boot.system.service.UserService;
+import com.youlai.boot.system.service.UserSocialService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
@@ -59,14 +59,14 @@ public class WxMaAuthService {
* @param code 微信登录凭证(wx.login 获取)
* @return 登录结果(成功返回 token,需绑定返回 openid)
*/
- public WxMaLoginResp silentLogin(String code) {
+ public WxMaLoginVO silentLogin(String code) {
WxMaAuthenticationToken token = new WxMaAuthenticationToken(code);
try {
Authentication authentication = authenticationManager.authenticate(token);
AuthenticationToken authToken = tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
- return WxMaLoginResp.builder()
+ return WxMaLoginVO.builder()
.isNewUser(false)
.needBindMobile(false)
.accessToken(authToken.getAccessToken())
@@ -75,7 +75,7 @@ public class WxMaAuthService {
.expiresIn(authToken.getExpiresIn())
.build();
} catch (MobileNotBoundException e) {
- return WxMaLoginResp.builder()
+ return WxMaLoginVO.builder()
.isNewUser(true)
.needBindMobile(true)
.openid(e.getOpenid())
@@ -122,9 +122,9 @@ public class WxMaAuthService {
* 绑定成功后自动完成登录。
*
*
- * @param openid 微信用户唯一标识
- * @param mobile 手机号码
- * @param smsCode 短信验证码
+ * @param openid 微信用户唯一标识
+ * @param mobile 手机号码
+ * @param smsCode 短信验证码
* @return 认证令牌
*/
@Transactional(rollbackFor = Exception.class)
diff --git a/src/main/java/com/youlai/boot/auth/security/token/WxMaAuthenticationToken.java b/src/main/java/com/youlai/boot/auth/wxma/token/WxMaAuthenticationToken.java
similarity index 97%
rename from src/main/java/com/youlai/boot/auth/security/token/WxMaAuthenticationToken.java
rename to src/main/java/com/youlai/boot/auth/wxma/token/WxMaAuthenticationToken.java
index 7d21d92c..970ec5d8 100644
--- a/src/main/java/com/youlai/boot/auth/security/token/WxMaAuthenticationToken.java
+++ b/src/main/java/com/youlai/boot/auth/wxma/token/WxMaAuthenticationToken.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.security.token;
+package com.youlai.boot.auth.wxma.token;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
diff --git a/src/main/java/com/youlai/boot/codegen/controller/CodegenController.java b/src/main/java/com/youlai/boot/codegen/controller/CodegenController.java
index ccbd9b70..77a14631 100644
--- a/src/main/java/com/youlai/boot/codegen/controller/CodegenController.java
+++ b/src/main/java/com/youlai/boot/codegen/controller/CodegenController.java
@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.result.PageResult;
import com.youlai.boot.common.result.Result;
-import com.youlai.boot.codegen.config.CodegenProperties;
+import com.youlai.boot.codegen.property.CodegenProperties;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.codegen.service.CodegenService;
@@ -18,6 +18,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.ServletOutputStream;
+import jakarta.validation.Valid;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -48,7 +49,7 @@ public class CodegenController {
@Operation(summary = "获取数据表分页列表")
@GetMapping("/table")
public PageResult getTablePage(
- TableQuery queryParams
+ @Valid TableQuery queryParams
) {
Page result = codegenService.getTablePage(queryParams);
return PageResult.success(result);
@@ -66,14 +67,14 @@ public class CodegenController {
@Operation(summary = "保存代码生成配置")
@PostMapping("/{tableName}/config")
@Log(module = LogModuleEnum.CODEGEN, value = ActionTypeEnum.UPDATE)
- public Result> saveGenConfig(@RequestBody GenConfigForm formData) {
+ public Result saveGenConfig(@RequestBody @Valid GenConfigForm formData) {
genTableService.saveGenConfig(formData);
return Result.success();
}
@Operation(summary = "删除代码生成配置")
@DeleteMapping("/{tableName}/config")
- public Result> deleteGenConfig(
+ public Result deleteGenConfig(
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
) {
genTableService.deleteGenConfig(tableName);
diff --git a/src/main/java/com/youlai/boot/codegen/model/form/GenConfigForm.java b/src/main/java/com/youlai/boot/codegen/model/form/GenConfigForm.java
index 5e584e0d..60fb6076 100644
--- a/src/main/java/com/youlai/boot/codegen/model/form/GenConfigForm.java
+++ b/src/main/java/com/youlai/boot/codegen/model/form/GenConfigForm.java
@@ -3,6 +3,8 @@ package com.youlai.boot.codegen.model.form;
import com.youlai.boot.codegen.enums.FormTypeEnum;
import com.youlai.boot.codegen.enums.QueryTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.util.List;
@@ -21,18 +23,26 @@ public class GenConfigForm {
private Long id;
@Schema(description = "表名",example = "sys_user")
+ @NotBlank(message = "表名不能为空")
private String tableName;
@Schema(description = "业务名",example = "用户")
+ @NotBlank(message = "业务名不能为空")
private String businessName;
@Schema(description = "模块名",example = "system")
+ @NotBlank(message = "模块名不能为空")
+ @Pattern(regexp = "[a-zA-Z][a-zA-Z0-9_]*", message = "模块名只能包含字母、数字和下划线,且以字母开头")
private String moduleName;
@Schema(description = "包名",example = "com.youlai")
+ @NotBlank(message = "包名不能为空")
+ @Pattern(regexp = "([a-zA-Z_][a-zA-Z0-9_]*\\.)*[a-zA-Z_][a-zA-Z0-9_]*", message = "包名格式不正确")
private String packageName;
@Schema(description = "实体名",example = "User")
+ @NotBlank(message = "实体名不能为空")
+ @Pattern(regexp = "[a-zA-Z][a-zA-Z0-9]*", message = "实体名只能包含字母和数字,且以字母开头")
private String entityName;
@Schema(description = "作者",example = "youlaitech")
diff --git a/src/main/java/com/youlai/boot/codegen/config/CodegenProperties.java b/src/main/java/com/youlai/boot/codegen/property/CodegenProperties.java
similarity index 87%
rename from src/main/java/com/youlai/boot/codegen/config/CodegenProperties.java
rename to src/main/java/com/youlai/boot/codegen/property/CodegenProperties.java
index 5f327996..ec229833 100644
--- a/src/main/java/com/youlai/boot/codegen/config/CodegenProperties.java
+++ b/src/main/java/com/youlai/boot/codegen/property/CodegenProperties.java
@@ -1,11 +1,9 @@
-package com.youlai.boot.codegen.config;
+package com.youlai.boot.codegen.property;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.map.MapUtil;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
@@ -16,8 +14,6 @@ import java.util.Map;
* @author Ray
* @since 2.11.0
*/
-@Component
-@EnableConfigurationProperties(CodegenProperties.class)
@ConfigurationProperties(prefix = "codegen")
@Data
public class CodegenProperties {
@@ -95,4 +91,4 @@ public class CodegenProperties {
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/youlai/boot/codegen/service/impl/CodegenServiceImpl.java b/src/main/java/com/youlai/boot/codegen/service/impl/CodegenServiceImpl.java
index 259cbf06..dc09e96d 100644
--- a/src/main/java/com/youlai/boot/codegen/service/impl/CodegenServiceImpl.java
+++ b/src/main/java/com/youlai/boot/codegen/service/impl/CodegenServiceImpl.java
@@ -12,7 +12,7 @@ import cn.hutool.extra.template.TemplateUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.codegen.enums.JavaTypeEnum;
-import com.youlai.boot.codegen.config.CodegenProperties;
+import com.youlai.boot.codegen.property.CodegenProperties;
import com.youlai.boot.codegen.service.GenTableService;
import com.youlai.boot.codegen.service.GenTableColumnService;
import com.youlai.boot.codegen.service.CodegenService;
diff --git a/src/main/java/com/youlai/boot/codegen/service/impl/GenTableServiceImpl.java b/src/main/java/com/youlai/boot/codegen/service/impl/GenTableServiceImpl.java
index 6df5b3d5..287218a4 100644
--- a/src/main/java/com/youlai/boot/codegen/service/impl/GenTableServiceImpl.java
+++ b/src/main/java/com/youlai/boot/codegen/service/impl/GenTableServiceImpl.java
@@ -11,7 +11,7 @@ import com.youlai.boot.codegen.enums.FormTypeEnum;
import com.youlai.boot.codegen.enums.JavaTypeEnum;
import com.youlai.boot.codegen.enums.QueryTypeEnum;
import com.youlai.boot.common.exception.BusinessException;
-import com.youlai.boot.codegen.config.CodegenProperties;
+import com.youlai.boot.codegen.property.CodegenProperties;
import com.youlai.boot.codegen.converter.CodegenConverter;
import com.youlai.boot.codegen.mapper.DatabaseMapper;
import com.youlai.boot.codegen.mapper.GenTableMapper;
@@ -22,6 +22,7 @@ import com.youlai.boot.codegen.model.entity.GenTableColumn;
import com.youlai.boot.codegen.model.form.GenConfigForm;
import com.youlai.boot.codegen.service.GenTableService;
import com.youlai.boot.codegen.service.GenTableColumnService;
+import com.youlai.boot.system.model.dto.MenuCreateDTO;
import com.youlai.boot.system.service.MenuService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
@@ -188,7 +189,13 @@ public class GenTableServiceImpl extends ServiceImpl i
// 如果选择上级菜单且当前环境不是生产环境,则保存菜单
Long parentMenuId = formData.getParentMenuId();
if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) {
- menuService.addMenuForCodegen(parentMenuId, genTable);
+ MenuCreateDTO dto = new MenuCreateDTO(
+ genTable.getBusinessName(),
+ genTable.getEntityName(),
+ genTable.getModuleName(),
+ genTable.getTableName()
+ );
+ menuService.addMenuForCodegen(parentMenuId, dto);
}
List genTableColumns = codegenConverter.toGenTableColumn(formData.getFieldConfigs());
diff --git a/src/main/java/com/youlai/boot/common/annotation/RepeatSubmit.java b/src/main/java/com/youlai/boot/common/annotation/RepeatSubmit.java
deleted file mode 100644
index 053f1320..00000000
--- a/src/main/java/com/youlai/boot/common/annotation/RepeatSubmit.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.youlai.boot.common.annotation;
-
-
-import java.lang.annotation.*;
-
-/**
- * 防止重复提交注解
- *
- * 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。
- *
- * @author Ray.Hao
- * @since 2.3.0
- */
-@Target(ElementType.METHOD)
-@Retention(RetentionPolicy.RUNTIME)
-@Documented
-@Inherited
-public @interface RepeatSubmit {
-
- /**
- * 锁过期时间(秒)
- *
- * 默认5秒内不允许重复提交
- */
- int expire() default 5;
-
-}
diff --git a/src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java b/src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java
deleted file mode 100644
index 256930ee..00000000
--- a/src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.youlai.boot.common.aspect;
-
-import cn.hutool.core.util.StrUtil;
-import cn.hutool.crypto.digest.DigestUtil;
-import cn.hutool.json.JSONUtil;
-import com.youlai.boot.common.constant.RedisConstants;
-import com.youlai.boot.common.constant.SecurityConstants;
-import com.youlai.boot.common.result.ResultCode;
-import com.youlai.boot.common.exception.BusinessException;
-import com.youlai.boot.common.annotation.RepeatSubmit;
-import com.youlai.boot.common.util.IPUtils;
-import jakarta.servlet.http.HttpServletRequest;
-import lombok.RequiredArgsConstructor;
-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.redisson.api.RLock;
-import org.redisson.api.RedissonClient;
-import org.springframework.http.HttpHeaders;
-import org.springframework.stereotype.Component;
-import org.springframework.web.context.request.RequestContextHolder;
-import org.springframework.web.context.request.ServletRequestAttributes;
-
-import java.util.concurrent.TimeUnit;
-
-/**
- * 防重复提交切面
- *
- * @author Ray.Hao
- * @since 4.3.1
- */
-@Aspect
-@Component
-@RequiredArgsConstructor
-public class RepeatSubmitAspect {
-
- private final RedissonClient redissonClient;
-
- @Pointcut("@annotation(repeatSubmit)")
- public void repeatSubmitPointCut(RepeatSubmit repeatSubmit) {
- }
-
- @Around(value = "repeatSubmitPointCut(repeatSubmit)", argNames = "pjp,repeatSubmit")
- public Object handleRepeatSubmit(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
- String lockKey = buildLockKey(pjp);
-
- int expire = repeatSubmit.expire();
- RLock lock = redissonClient.getLock(lockKey);
-
- boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS);
- if (!locked) {
- throw new BusinessException(ResultCode.DUPLICATE_SUBMISSION);
- }
- return pjp.proceed();
- }
-
- /**
- * 生成防重复提交锁的 key
- *
- *
请求体哈希用于区分同一接口的不同提交(如新增菜单与新增目录)
- */
- private String buildLockKey(ProceedingJoinPoint pjp) {
- HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
- String userIdentifier = getUserIdentifier(request);
- String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI());
- String bodyHash = DigestUtil.sha256Hex(JSONUtil.toJsonStr(pjp.getArgs()));
- return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier + ":" + bodyHash);
- }
-
- /**
- * 获取用户唯一标识
- * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识
- * 2. 如果 Token 为空,使用 IP 作为用户唯一标识
- */
- private String getUserIdentifier(HttpServletRequest request) {
- String userIdentifier;
- String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
- if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
- String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
- userIdentifier = DigestUtil.sha256Hex(rawToken);
- } else {
- userIdentifier = IPUtils.getIpAddr(request);
- }
- return userIdentifier;
- }
-
-}
diff --git a/src/main/java/com/youlai/boot/common/base/BaseQuery.java b/src/main/java/com/youlai/boot/common/base/BaseQuery.java
index 7548937d..697d1c72 100644
--- a/src/main/java/com/youlai/boot/common/base/BaseQuery.java
+++ b/src/main/java/com/youlai/boot/common/base/BaseQuery.java
@@ -2,6 +2,8 @@ package com.youlai.boot.common.base;
import com.youlai.boot.common.annotation.ValidField;
import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
import lombok.Data;
import java.io.Serial;
@@ -21,16 +23,20 @@ public class BaseQuery implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "页码", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1")
+ @Min(value = 1, message = "页码不能小于 1")
private Integer pageNum = 1;
@Schema(description = "每页记录数", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "10")
+ @Min(value = 1, message = "每页记录数不能小于 1")
+ @Max(value = 200, message = "每页记录数不能超过 200")
private Integer pageSize = 10;
@Schema(description = "排序字段", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
@ValidField(allowedValues = {"create_time", "update_time"})
private String sortBy;
- @Schema(description = "排序方式(正序:ASC;反序:DESC)", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
+ @Schema(description = "排序方式(正序:asc;反序:desc,不区分大小写)", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
+ @ValidField(allowedValues = {"asc", "desc", "ASC", "DESC"})
private String order;
public boolean isPaged() {
diff --git a/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java b/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java
index fe0441b3..8e927691 100644
--- a/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java
+++ b/src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java
@@ -28,6 +28,7 @@ public enum LogModuleEnum implements IBaseEnum {
LOG(10, "日志管理"),
CODEGEN(11, "代码生成"),
FORM(12, "动态表单"),
+ WORKFLOW(13, "工作流"),
OTHER(99, "其他");
@EnumValue
diff --git a/src/main/java/com/youlai/boot/common/result/PageResult.java b/src/main/java/com/youlai/boot/common/result/PageResult.java
index 72fee0b1..3b453d19 100644
--- a/src/main/java/com/youlai/boot/common/result/PageResult.java
+++ b/src/main/java/com/youlai/boot/common/result/PageResult.java
@@ -25,7 +25,7 @@ public class PageResult implements Serializable {
/**
* 构建分页结果(MyBatis-Plus {@link IPage})。
*
- * data 为当前页记录列表;page 提供分页元信息。
+ * list 为当前页记录列表,total 为符合条件的总记录数。
*/
public static PageResult success(IPage page) {
PageResult result = new PageResult<>();
@@ -47,7 +47,7 @@ public class PageResult implements Serializable {
/**
* 构建列表结果(无分页)。
*
- * page 置为 null,用于与分页返回区分。
+ * total 固定为 0,用于与分页返回区分。
*/
public static PageResult success(List list) {
PageResult result = new PageResult<>();
diff --git a/src/main/java/com/youlai/boot/config/OpenApiConfig.java b/src/main/java/com/youlai/boot/config/OpenApiConfig.java
index 3fdcb10a..2fb197e6 100644
--- a/src/main/java/com/youlai/boot/config/OpenApiConfig.java
+++ b/src/main/java/com/youlai/boot/config/OpenApiConfig.java
@@ -1,7 +1,7 @@
package com.youlai.boot.config;
import cn.hutool.core.util.ArrayUtil;
-import com.youlai.boot.config.property.SecurityProperties;
+import com.youlai.boot.framework.security.config.SecurityProperties;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
@@ -78,12 +78,12 @@ public class OpenApiConfig {
if (openApi.getPaths() != null) {
openApi.getPaths().forEach((path, pathItem) -> {
- // 忽略认证的请求无需携带 Authorization
- String[] ignoreUrls = securityProperties.getIgnoreUrls();
- if (ArrayUtil.isNotEmpty(ignoreUrls)) {
- // Ant 匹配忽略的路径,不添加Authorization
+ // 匿名放行的请求无需携带 Authorization
+ String[] permitAllUrls = securityProperties.getPermitAllUrls();
+ if (ArrayUtil.isNotEmpty(permitAllUrls)) {
+ // Ant 匹配放行路径,不添加Authorization
AntPathMatcher antPathMatcher = new AntPathMatcher();
- if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) {
+ if (Stream.of(permitAllUrls).anyMatch(permitAllUrl -> antPathMatcher.match(permitAllUrl, path))) {
return;
}
}
diff --git a/src/main/java/com/youlai/boot/config/SecurityConfig.java b/src/main/java/com/youlai/boot/config/SecurityConfig.java
index 8a3b450d..5fc58f72 100644
--- a/src/main/java/com/youlai/boot/config/SecurityConfig.java
+++ b/src/main/java/com/youlai/boot/config/SecurityConfig.java
@@ -2,17 +2,17 @@ package com.youlai.boot.config;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.hutool.core.util.ArrayUtil;
-import com.youlai.boot.auth.service.CaptchaService;
-import com.youlai.boot.config.property.SecurityProperties;
+import com.youlai.boot.auth.password.service.CaptchaService;
+import com.youlai.boot.framework.security.config.SecurityProperties;
import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter;
+import com.youlai.boot.framework.security.handler.JsonAccessDeniedHandler;
+import com.youlai.boot.framework.security.handler.JsonAuthenticationEntryPoint;
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
import com.youlai.boot.framework.security.service.SecurityUserDetailsService;
import com.youlai.boot.framework.security.token.TokenManager;
-import com.youlai.boot.auth.security.filter.CaptchaValidationFilter;
-import com.youlai.boot.auth.security.handler.JsonAccessDeniedHandler;
-import com.youlai.boot.auth.security.handler.JsonAuthenticationEntryPoint;
-import com.youlai.boot.auth.security.provider.SmsAuthenticationProvider;
-import com.youlai.boot.auth.security.provider.WxMaAuthenticationProvider;
+import com.youlai.boot.auth.password.filter.CaptchaValidationFilter;
+import com.youlai.boot.auth.sms.provider.SmsAuthenticationProvider;
+import com.youlai.boot.auth.wxma.provider.WxMaAuthenticationProvider;
import com.youlai.boot.system.service.UserSocialService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
@@ -24,7 +24,6 @@ import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
@@ -60,9 +59,9 @@ public class SecurityConfig {
.authorizeHttpRequests(requestMatcherRegistry -> {
// ASYNC 分发(SSE 超时/完成后的异步 dispatch)无 Authorization 头,直接放行
requestMatcherRegistry.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll();
- String[] ignoreUrls = securityProperties.getIgnoreUrls();
- if (ArrayUtil.isNotEmpty(ignoreUrls)) {
- requestMatcherRegistry.requestMatchers(ignoreUrls).permitAll();
+ String[] permitAllUrls = securityProperties.getPermitAllUrls();
+ if (ArrayUtil.isNotEmpty(permitAllUrls)) {
+ requestMatcherRegistry.requestMatchers(permitAllUrls).permitAll();
}
requestMatcherRegistry.anyRequest().authenticated();
}
@@ -86,16 +85,6 @@ public class SecurityConfig {
.build();
}
- @Bean
- public WebSecurityCustomizer webSecurityCustomizer() {
- return (web) -> {
- String[] unsecuredUrls = securityProperties.getUnsecuredUrls();
- if (ArrayUtil.isNotEmpty(unsecuredUrls)) {
- web.ignoring().requestMatchers(unsecuredUrls);
- }
- };
- }
-
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
diff --git a/src/main/java/com/youlai/boot/config/PasswordEncoderConfig.java b/src/main/java/com/youlai/boot/framework/security/config/PasswordEncoderConfig.java
similarity index 91%
rename from src/main/java/com/youlai/boot/config/PasswordEncoderConfig.java
rename to src/main/java/com/youlai/boot/framework/security/config/PasswordEncoderConfig.java
index 3e6e0e0b..6ee0f46d 100644
--- a/src/main/java/com/youlai/boot/config/PasswordEncoderConfig.java
+++ b/src/main/java/com/youlai/boot/framework/security/config/PasswordEncoderConfig.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.config;
+package com.youlai.boot.framework.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/src/main/java/com/youlai/boot/config/property/SecurityProperties.java b/src/main/java/com/youlai/boot/framework/security/config/SecurityProperties.java
similarity index 72%
rename from src/main/java/com/youlai/boot/config/property/SecurityProperties.java
rename to src/main/java/com/youlai/boot/framework/security/config/SecurityProperties.java
index 5e4b3725..96337564 100644
--- a/src/main/java/com/youlai/boot/config/property/SecurityProperties.java
+++ b/src/main/java/com/youlai/boot/framework/security/config/SecurityProperties.java
@@ -1,12 +1,12 @@
-package com.youlai.boot.config.property;
+package com.youlai.boot.framework.security.config;
+import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
/**
@@ -18,44 +18,38 @@ import org.springframework.validation.annotation.Validated;
* @since 2024/4/18
*/
@Data
-@Component
@Validated
@ConfigurationProperties(prefix = "security")
public class SecurityProperties {
/**
- * 会话管理配置
+ * 令牌策略配置
*/
- private SessionConfig session;
+ @Valid
+ @NotNull(message = "令牌策略配置不能为空")
+ private TokenConfig token;
/**
- * 安全白名单路径(完全绕过安全过滤器)
- * 示例值:/api/v1/auth/login/**, /ws/**
+ * 匿名放行路径(仍经过安全过滤器链,仅跳过认证与授权判断,保留安全响应头)
+ *
示例值:/api/v1/auth/login/**, /doc.html
*/
@NotEmpty
- private String[] ignoreUrls;
+ private String[] permitAllUrls;
/**
- * 非安全端点路径(允许匿名访问的API)
- * 示例值:/doc.html, /v3/api-docs/**
- */
- @NotEmpty
- private String[] unsecuredUrls;
-
- /**
- * 会话配置嵌套类
+ * 令牌策略嵌套类
*/
@Data
- public static class SessionConfig {
+ public static class TokenConfig {
/**
- * 认证策略类型
+ * 令牌策略类型
*
* - jwt - 基于JWT的无状态认证
* - redis-token - 基于Redis的有状态认证
*
*/
- @NotNull(message = "会话类型不能为空")
- @Pattern(regexp = "jwt|redis-token", message = "会话类型只能是 jwt 或 redis-token")
+ @NotNull(message = "令牌类型不能为空")
+ @Pattern(regexp = "jwt|redis-token", message = "令牌类型只能是 jwt 或 redis-token")
private String type;
/**
@@ -75,13 +69,15 @@ public class SecurityProperties {
private Integer refreshTokenTimeToLive = 604800;
/**
- * JWT 配置项
+ * JWT 配置项(type 为 jwt 时必填)
*/
+ @Valid
private JwtConfig jwt;
/**
- * Redis令牌配置项
+ * Redis令牌配置项(type 为 redis-token 时使用)
*/
+ @Valid
private RedisTokenConfig redisToken;
}
@@ -95,7 +91,7 @@ public class SecurityProperties {
* HS256算法要求至少32个字符
* 示例:SecretKey012345678901234567890123456789
*/
- @NotNull
+ @NotNull(message = "JWT 密钥不能为空")
private String secretKey;
}
diff --git a/src/main/java/com/youlai/boot/auth/security/handler/JsonAccessDeniedHandler.java b/src/main/java/com/youlai/boot/framework/security/handler/JsonAccessDeniedHandler.java
similarity index 93%
rename from src/main/java/com/youlai/boot/auth/security/handler/JsonAccessDeniedHandler.java
rename to src/main/java/com/youlai/boot/framework/security/handler/JsonAccessDeniedHandler.java
index 20de72db..b80fe234 100644
--- a/src/main/java/com/youlai/boot/auth/security/handler/JsonAccessDeniedHandler.java
+++ b/src/main/java/com/youlai/boot/framework/security/handler/JsonAccessDeniedHandler.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.security.handler;
+package com.youlai.boot.framework.security.handler;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.framework.web.util.ResponseWriter;
diff --git a/src/main/java/com/youlai/boot/auth/security/handler/JsonAuthenticationEntryPoint.java b/src/main/java/com/youlai/boot/framework/security/handler/JsonAuthenticationEntryPoint.java
similarity index 96%
rename from src/main/java/com/youlai/boot/auth/security/handler/JsonAuthenticationEntryPoint.java
rename to src/main/java/com/youlai/boot/framework/security/handler/JsonAuthenticationEntryPoint.java
index b7d11849..791c9126 100644
--- a/src/main/java/com/youlai/boot/auth/security/handler/JsonAuthenticationEntryPoint.java
+++ b/src/main/java/com/youlai/boot/framework/security/handler/JsonAuthenticationEntryPoint.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.security.handler;
+package com.youlai.boot.framework.security.handler;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.framework.web.util.ResponseWriter;
diff --git a/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java b/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java
index 433322e5..f9754989 100644
--- a/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java
+++ b/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java
@@ -13,7 +13,7 @@ import com.youlai.boot.common.constant.JwtClaimConstants;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode;
-import com.youlai.boot.config.property.SecurityProperties;
+import com.youlai.boot.framework.security.config.SecurityProperties;
import com.youlai.boot.common.exception.TokenInvalidException;
import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.framework.security.model.RoleDataScope;
@@ -40,7 +40,7 @@ import java.util.stream.Collectors;
* @author Ray.Hao
* @since 2024/11/15
*/
-@ConditionalOnProperty(value = "security.session.type", havingValue = "jwt")
+@ConditionalOnProperty(value = "security.token.type", havingValue = "jwt")
@Service
public class JwtTokenManager implements TokenManager {
@@ -51,13 +51,13 @@ public class JwtTokenManager implements TokenManager {
public JwtTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) {
this.securityProperties = securityProperties;
this.redisTemplate = redisTemplate;
- this.secretKey = securityProperties.getSession().getJwt().getSecretKey().getBytes();
+ this.secretKey = securityProperties.getToken().getJwt().getSecretKey().getBytes();
}
@Override
public AuthenticationToken generateToken(Authentication authentication) {
- int accessTokenTimeToLive = securityProperties.getSession().getAccessTokenTimeToLive();
- int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive();
+ int accessTokenTimeToLive = securityProperties.getToken().getAccessTokenTimeToLive();
+ int refreshTokenTimeToLive = securityProperties.getToken().getRefreshTokenTimeToLive();
String accessToken = generateToken(authentication, accessTokenTimeToLive);
String refreshToken = generateToken(authentication, refreshTokenTimeToLive, true);
@@ -189,7 +189,7 @@ public class JwtTokenManager implements TokenManager {
throw new TokenInvalidException(ResultCode.REFRESH_TOKEN_INVALID);
}
Authentication authentication = parseToken(refreshToken);
- int accessTokenExpiration = securityProperties.getSession().getAccessTokenTimeToLive();
+ int accessTokenExpiration = securityProperties.getToken().getAccessTokenTimeToLive();
String newAccessToken = generateToken(authentication, accessTokenExpiration);
return AuthenticationToken.builder()
.accessToken(newAccessToken)
diff --git a/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java b/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java
index 07937d01..54ae7a3c 100644
--- a/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java
+++ b/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java
@@ -5,7 +5,7 @@ import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode;
-import com.youlai.boot.config.property.SecurityProperties;
+import com.youlai.boot.framework.security.config.SecurityProperties;
import com.youlai.boot.common.exception.TokenInvalidException;
import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.framework.security.model.SecurityUserDetails;
@@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit;
* @author Ray.Hao
* @since 2024/11/15
*/
-@ConditionalOnProperty(value = "security.session.type", havingValue = "redis-token")
+@ConditionalOnProperty(value = "security.token.type", havingValue = "redis-token")
@Service
public class RedisTokenManager implements TokenManager {
@@ -62,7 +62,7 @@ public class RedisTokenManager implements TokenManager {
return AuthenticationToken.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
- .expiresIn(securityProperties.getSession().getAccessTokenTimeToLive())
+ .expiresIn(securityProperties.getToken().getAccessTokenTimeToLive())
.build();
}
@@ -102,7 +102,7 @@ public class RedisTokenManager implements TokenManager {
String newAccessToken = IdUtil.fastSimpleUUID();
storeAccessToken(newAccessToken, sessionUser);
- int accessTtl = securityProperties.getSession().getAccessTokenTimeToLive();
+ int accessTtl = securityProperties.getToken().getAccessTokenTimeToLive();
return AuthenticationToken.builder()
.accessToken(newAccessToken)
.refreshToken(refreshToken)
@@ -152,16 +152,16 @@ public class RedisTokenManager implements TokenManager {
private void storeTokensInRedis(String accessToken, String refreshToken, SecurityUserDetails sessionUser) {
setRedisValue(formatTokenKey(accessToken), sessionUser,
- securityProperties.getSession().getAccessTokenTimeToLive());
+ securityProperties.getToken().getAccessTokenTimeToLive());
String refreshTokenKey = StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken);
setRedisValue(refreshTokenKey, sessionUser,
- securityProperties.getSession().getRefreshTokenTimeToLive());
+ securityProperties.getToken().getRefreshTokenTimeToLive());
setRedisValue(StrUtil.format(RedisConstants.Auth.USER_REFRESH_TOKEN, sessionUser.getUserId()),
- refreshToken, securityProperties.getSession().getRefreshTokenTimeToLive());
+ refreshToken, securityProperties.getToken().getRefreshTokenTimeToLive());
}
private void handleSingleDeviceLogin(Long userId, String accessToken) {
- Boolean allowMultiLogin = securityProperties.getSession().getRedisToken().getAllowMultiLogin();
+ Boolean allowMultiLogin = securityProperties.getToken().getRedisToken().getAllowMultiLogin();
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userId);
if (!allowMultiLogin) {
Object oldAccessTokenValue = redisTemplate.opsForValue().get(userAccessKey);
@@ -169,14 +169,14 @@ public class RedisTokenManager implements TokenManager {
redisTemplate.delete(formatTokenKey(oldAccessToken));
}
}
- setRedisValue(userAccessKey, accessToken, securityProperties.getSession().getAccessTokenTimeToLive());
+ setRedisValue(userAccessKey, accessToken, securityProperties.getToken().getAccessTokenTimeToLive());
}
private void storeAccessToken(String newAccessToken, SecurityUserDetails sessionUser) {
setRedisValue(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), sessionUser,
- securityProperties.getSession().getAccessTokenTimeToLive());
+ securityProperties.getToken().getAccessTokenTimeToLive());
String userAccessKey = StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, sessionUser.getUserId());
- setRedisValue(userAccessKey, newAccessToken, securityProperties.getSession().getAccessTokenTimeToLive());
+ setRedisValue(userAccessKey, newAccessToken, securityProperties.getToken().getAccessTokenTimeToLive());
}
private String formatTokenKey(String token) {
diff --git a/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java b/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java
index 09219484..ed629549 100644
--- a/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java
+++ b/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java
@@ -3,9 +3,9 @@ package com.youlai.boot.framework.web.filter;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.result.ResultCode;
-import com.youlai.boot.common.util.IPUtils;
-import com.youlai.boot.config.property.RateLimitProperties;
-import com.youlai.boot.framework.web.ratelimit.SlidingWindowScript;
+import com.youlai.boot.framework.web.util.IPUtils;
+import com.youlai.boot.framework.web.ratelimit.RateLimitProperties;
+import com.youlai.boot.framework.web.ratelimit.RedisSlidingWindowRateLimiter;
import com.youlai.boot.framework.web.util.ResponseWriter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
@@ -15,7 +15,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
-import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@@ -51,7 +50,7 @@ import java.io.IOException;
@Slf4j
public class IpRateLimitFilter extends OncePerRequestFilter {
- private final RedisTemplate redisTemplate;
+ private final RedisSlidingWindowRateLimiter rateLimiter;
private final RateLimitProperties rateLimitProperties;
@Override
@@ -70,14 +69,8 @@ public class IpRateLimitFilter extends OncePerRequestFilter {
String key = StrUtil.format(RedisConstants.RateLimit.IP, ip);
long windowMs = ipConfig.getWindow().toMillis();
- // 执行滑动窗口计数(Lua 原子操作);Redis 异常时 Fail-Open 放行
- Long count;
- try {
- count = SlidingWindowScript.execute(redisTemplate, key, windowMs);
- } catch (Exception e) {
- log.warn("IP 限流 Redis 异常,Fail-Open 放行 ip={}", ip, e);
- count = null;
- }
+ // limiter 内部已统一 Fail-Open:Redis 异常返回 null,按未计数放行
+ Long count = rateLimiter.count(key, windowMs);
int limit = ipConfig.getLimit();
int current = count != null ? count.intValue() : 0;
diff --git a/src/main/java/com/youlai/boot/framework/web/filter/RequestLogFilter.java b/src/main/java/com/youlai/boot/framework/web/filter/RequestLogFilter.java
index f9ca070a..fd992b0b 100644
--- a/src/main/java/com/youlai/boot/framework/web/filter/RequestLogFilter.java
+++ b/src/main/java/com/youlai/boot/framework/web/filter/RequestLogFilter.java
@@ -1,9 +1,9 @@
package com.youlai.boot.framework.web.filter;
-import com.youlai.boot.common.util.IPUtils;
+import com.youlai.boot.framework.web.util.IPUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
import org.springframework.web.filter.CommonsRequestLoggingFilter;
/**
@@ -12,7 +12,7 @@ import org.springframework.web.filter.CommonsRequestLoggingFilter;
* @author haoxr
* @since 2023/03/03
*/
-@Configuration
+@Component
@Slf4j
public class RequestLogFilter extends CommonsRequestLoggingFilter {
diff --git a/src/main/java/com/youlai/boot/framework/web/log/OperationLogAspect.java b/src/main/java/com/youlai/boot/framework/web/log/OperationLogAspect.java
index ec9e294d..1cc010cf 100644
--- a/src/main/java/com/youlai/boot/framework/web/log/OperationLogAspect.java
+++ b/src/main/java/com/youlai/boot/framework/web/log/OperationLogAspect.java
@@ -6,7 +6,7 @@ import cn.hutool.http.useragent.UserAgentUtil;
import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
-import com.youlai.boot.common.util.IPUtils;
+import com.youlai.boot.framework.web.util.IPUtils;
import com.youlai.boot.framework.security.util.SecurityUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
diff --git a/src/main/java/com/youlai/boot/common/annotation/RateLimit.java b/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimit.java
similarity index 68%
rename from src/main/java/com/youlai/boot/common/annotation/RateLimit.java
rename to src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimit.java
index e7e73424..8192d1e3 100644
--- a/src/main/java/com/youlai/boot/common/annotation/RateLimit.java
+++ b/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimit.java
@@ -1,9 +1,14 @@
-package com.youlai.boot.common.annotation;
+package com.youlai.boot.framework.web.ratelimit;
-import java.lang.annotation.*;
+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;
/**
- * 接口限流
+ * 接口限流注解
+ *
* 标注在 Controller 方法上,基于 Redis 滑动窗口实现
*
* @author Ray.Hao
diff --git a/src/main/java/com/youlai/boot/common/aspect/RateLimitAspect.java b/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimitAspect.java
similarity index 87%
rename from src/main/java/com/youlai/boot/common/aspect/RateLimitAspect.java
rename to src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimitAspect.java
index b11b5377..8f415dcb 100644
--- a/src/main/java/com/youlai/boot/common/aspect/RateLimitAspect.java
+++ b/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimitAspect.java
@@ -1,15 +1,12 @@
-package com.youlai.boot.common.aspect;
+package com.youlai.boot.framework.web.ratelimit;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
-import com.youlai.boot.common.annotation.RateLimit;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants;
-import com.youlai.boot.common.result.ResultCode;
-import com.youlai.boot.common.util.IPUtils;
-import com.youlai.boot.config.property.RateLimitProperties;
import com.youlai.boot.common.exception.RateLimitException;
-import com.youlai.boot.framework.web.ratelimit.SlidingWindowScript;
+import com.youlai.boot.common.result.ResultCode;
+import com.youlai.boot.framework.web.util.IPUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
@@ -17,7 +14,6 @@ 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.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
@@ -27,7 +23,7 @@ import org.springframework.web.context.request.ServletRequestAttributes;
* 接口级限流切面
*
* 对标注了 {@link RateLimit} 的 Controller 方法做 Redis 滑动窗口计数。
- * 超阈值抛出 {@link BusinessException}(A0502),全局异常处理器统一返回 JSON。
+ * 超阈值抛出 {@link RateLimitException}(A0502),全局异常处理器统一返回 JSON。
*
*
* 升级历史
@@ -45,7 +41,7 @@ import org.springframework.web.context.request.ServletRequestAttributes;
@Slf4j
public class RateLimitAspect {
- private final RedisTemplate redisTemplate;
+ private final RedisSlidingWindowRateLimiter rateLimiter;
private final RateLimitProperties rateLimitProperties;
@Around("@annotation(rateLimit)")
@@ -59,7 +55,8 @@ public class RateLimitAspect {
? rateLimit.window() * 1000L
: rateLimitProperties.getDefaultWindow().toMillis();
- Long count = SlidingWindowScript.execute(redisTemplate, key, windowMs);
+ // limiter 内部已统一 Fail-Open:Redis 异常返回 null,按未计数放行
+ Long count = rateLimiter.count(key, windowMs);
int current = count != null ? count.intValue() : 0;
setRateLimitHeaders(request, limit, current, windowMs);
diff --git a/src/main/java/com/youlai/boot/config/property/RateLimitProperties.java b/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimitProperties.java
similarity index 92%
rename from src/main/java/com/youlai/boot/config/property/RateLimitProperties.java
rename to src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimitProperties.java
index 72d03b26..a48abc7a 100644
--- a/src/main/java/com/youlai/boot/config/property/RateLimitProperties.java
+++ b/src/main/java/com/youlai/boot/framework/web/ratelimit/RateLimitProperties.java
@@ -1,8 +1,7 @@
-package com.youlai.boot.config.property;
+package com.youlai.boot.framework.web.ratelimit;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
import java.time.Duration;
@@ -14,7 +13,6 @@ import java.time.Duration;
* @since 4.3.1
*/
@Data
-@Component
@ConfigurationProperties(prefix = "rate-limit")
public class RateLimitProperties {
diff --git a/src/main/java/com/youlai/boot/framework/web/ratelimit/RedisSlidingWindowRateLimiter.java b/src/main/java/com/youlai/boot/framework/web/ratelimit/RedisSlidingWindowRateLimiter.java
new file mode 100644
index 00000000..87530387
--- /dev/null
+++ b/src/main/java/com/youlai/boot/framework/web/ratelimit/RedisSlidingWindowRateLimiter.java
@@ -0,0 +1,76 @@
+package com.youlai.boot.framework.web.ratelimit;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Redis 滑动窗口限流器
+ *
+ * 基于 Redis Sorted Set 实现原子化的滑动窗口计数:
+ *
+ * - ZREMRANGEBYSCORE 清除窗口外旧请求
+ * - ZADD 添加当前请求
+ * - ZCARD 统计窗口内请求数
+ * - PEXPIRE 设置 Key 过期(窗口 + 1s 冗余)
+ *
+ * 一次网络往返完成全部操作,避免多次 Redis 调用之间的竞态条件。
+ *
+ * Redis 异常时返回 null(本次不计数),调用方统一按 Fail-Open 放行,
+ * 保证限流组件故障不影响业务可用性。
+ *
+ * @author Ray.Hao
+ * @since 4.4.0
+ */
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class RedisSlidingWindowRateLimiter {
+
+ private final RedisTemplate redisTemplate;
+
+ private static final DefaultRedisScript SCRIPT;
+
+ static {
+ SCRIPT = new DefaultRedisScript<>();
+ SCRIPT.setScriptText(
+ "local key = KEYS[1] " +
+ "local now = tonumber(ARGV[1]) " +
+ "local window = tonumber(ARGV[2]) " +
+ "local member = ARGV[3] " +
+ "redis.call('ZREMRANGEBYSCORE', key, 0, now - window) " +
+ "redis.call('ZADD', key, now, member) " +
+ "redis.call('PEXPIRE', key, window + 1000) " +
+ "return redis.call('ZCARD', key)"
+ );
+ SCRIPT.setResultType(Long.class);
+ }
+
+ /**
+ * 记录一次请求并返回窗口内计数
+ *
+ * @param key 限流 Key(如 {@code rate_limit:ip:192.168.1.1})
+ * @param windowMs 窗口大小(毫秒)
+ * @return 当前窗口内请求数(包含本次);Redis 异常时返回 null,调用方按 Fail-Open 放行
+ */
+ public Long count(String key, long windowMs) {
+ try {
+ // 传 Long 而非 String,避免 Jackson 序列化器加双引号导致 Lua tonumber() 返回 nil
+ return redisTemplate.execute(
+ SCRIPT,
+ List.of(key),
+ System.currentTimeMillis(),
+ windowMs,
+ UUID.randomUUID().toString()
+ );
+ } catch (Exception e) {
+ log.warn("限流 Redis 异常,本次未计数(Fail-Open) key={}", key, e);
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/youlai/boot/framework/web/repeatsubmit/RepeatSubmit.java b/src/main/java/com/youlai/boot/framework/web/repeatsubmit/RepeatSubmit.java
new file mode 100644
index 00000000..fa798be8
--- /dev/null
+++ b/src/main/java/com/youlai/boot/framework/web/repeatsubmit/RepeatSubmit.java
@@ -0,0 +1,37 @@
+package com.youlai.boot.framework.web.repeatsubmit;
+
+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;
+
+/**
+ * 防重复提交注解
+ *
+ * 标注在写接口方法上,指定时间窗口内同一用户对同一请求只允许提交一次,默认 5 秒。
+ *
+ * @author Ray.Hao
+ * @since 2.3.0
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface RepeatSubmit {
+
+ /**
+ * 防重占位过期时间(秒)
+ *
+ * 默认 5 秒内不允许重复提交,占位到期后自动释放。
+ */
+ int expire() default 5;
+
+ /**
+ * 请求标识 SpEL 表达式(可选)
+ *
+ * 非空时以表达式的值区分不同请求,如 {@code "#form.name"};
+ * 为空时基于方法参数自动生成请求指纹,自动排除 ServletRequest/ServletResponse、
+ * MultipartFile、BindingResult 等不可稳定序列化的参数。
+ */
+ String key() default "";
+}
diff --git a/src/main/java/com/youlai/boot/framework/web/repeatsubmit/RepeatSubmitAspect.java b/src/main/java/com/youlai/boot/framework/web/repeatsubmit/RepeatSubmitAspect.java
new file mode 100644
index 00000000..b5bda566
--- /dev/null
+++ b/src/main/java/com/youlai/boot/framework/web/repeatsubmit/RepeatSubmitAspect.java
@@ -0,0 +1,126 @@
+package com.youlai.boot.framework.web.repeatsubmit;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import cn.hutool.json.JSONUtil;
+import com.youlai.boot.common.constant.RedisConstants;
+import com.youlai.boot.common.constant.SecurityConstants;
+import com.youlai.boot.common.exception.BusinessException;
+import com.youlai.boot.common.result.ResultCode;
+import com.youlai.boot.framework.web.util.IPUtils;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+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.RBucket;
+import org.redisson.api.RedissonClient;
+import org.springframework.context.expression.MethodBasedEvaluationContext;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.http.HttpHeaders;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.lang.reflect.Method;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 防重复提交切面
+ *
+ * 用 Redis 的「不存在才写入 + TTL」原子占位实现:同一请求标识在过期时间内
+ * 第二次提交直接拒绝,占位到期自动释放,无需手动解锁。
+ *
+ * @author Ray.Hao
+ * @since 4.3.1
+ */
+@Aspect
+@Component
+@RequiredArgsConstructor
+public class RepeatSubmitAspect {
+
+ private final RedissonClient redissonClient;
+
+ private static final ExpressionParser SPEL_PARSER = new SpelExpressionParser();
+ private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
+
+ /** 生成请求指纹时排除的参数类型,这些对象序列化结果不稳定或无业务含义 */
+ private static final List> EXCLUDED_PARAM_TYPES =
+ List.of(ServletRequest.class, ServletResponse.class, MultipartFile.class, BindingResult.class);
+
+ @Pointcut("@annotation(repeatSubmit)")
+ public void repeatSubmitPointCut(RepeatSubmit repeatSubmit) {
+ }
+
+ @Around(value = "repeatSubmitPointCut(repeatSubmit)", argNames = "pjp,repeatSubmit")
+ public Object handleRepeatSubmit(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
+ String lockKey = buildLockKey(pjp, repeatSubmit);
+
+ RBucket bucket = redissonClient.getBucket(lockKey);
+ boolean acquired = bucket.setIfAbsent("1", Duration.ofSeconds(repeatSubmit.expire()));
+ if (!acquired) {
+ throw new BusinessException(ResultCode.DUPLICATE_SUBMISSION);
+ }
+ return pjp.proceed();
+ }
+
+ /**
+ * 生成防重占位的 key
+ *
+ * 注解配置了 key 表达式时以表达式的值区分请求;否则对过滤后的业务参数做哈希。
+ */
+ private String buildLockKey(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) {
+ HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
+ String userIdentifier = getUserIdentifier(request);
+ String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI());
+ String requestFingerprint = StrUtil.isNotBlank(repeatSubmit.key())
+ ? DigestUtil.sha256Hex(evaluateKey(pjp, repeatSubmit.key()))
+ : DigestUtil.sha256Hex(JSONUtil.toJsonStr(filterArgs(pjp)));
+ return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier + ":" + requestFingerprint);
+ }
+
+ /** 过滤掉不可稳定序列化的参数后再生成指纹 */
+ private Object[] filterArgs(ProceedingJoinPoint pjp) {
+ return Arrays.stream(pjp.getArgs())
+ .filter(arg -> arg == null || EXCLUDED_PARAM_TYPES.stream().noneMatch(type -> type.isInstance(arg)))
+ .toArray();
+ }
+
+ /** 解析注解上的 SpEL key 表达式,方法参数可直接引用 */
+ private String evaluateKey(ProceedingJoinPoint pjp, String spel) {
+ Method method = ((MethodSignature) pjp.getSignature()).getMethod();
+ MethodBasedEvaluationContext context = new MethodBasedEvaluationContext(
+ pjp.getTarget(), method, pjp.getArgs(), PARAMETER_NAME_DISCOVERER);
+ Object value = SPEL_PARSER.parseExpression(spel).getValue(context);
+ return value != null ? value.toString() : "";
+ }
+
+ /**
+ * 获取用户唯一标识
+ * 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识
+ * 2. 如果 Token 为空,使用 IP 作为用户唯一标识
+ */
+ private String getUserIdentifier(HttpServletRequest request) {
+ String userIdentifier;
+ String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
+ if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
+ String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
+ userIdentifier = DigestUtil.sha256Hex(rawToken);
+ } else {
+ userIdentifier = IPUtils.getIpAddr(request);
+ }
+ return userIdentifier;
+ }
+
+}
diff --git a/src/main/java/com/youlai/boot/common/util/IPUtils.java b/src/main/java/com/youlai/boot/framework/web/util/IPUtils.java
similarity index 98%
rename from src/main/java/com/youlai/boot/common/util/IPUtils.java
rename to src/main/java/com/youlai/boot/framework/web/util/IPUtils.java
index 68bd3e89..b43d2cab 100644
--- a/src/main/java/com/youlai/boot/common/util/IPUtils.java
+++ b/src/main/java/com/youlai/boot/framework/web/util/IPUtils.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.common.util;
+package com.youlai.boot.framework.web.util;
import cn.hutool.core.util.StrUtil;
import jakarta.annotation.PostConstruct;
diff --git a/src/main/java/com/youlai/boot/module/config/WorkflowEnvironmentPostProcessor.java b/src/main/java/com/youlai/boot/module/config/WorkflowEnvironmentPostProcessor.java
new file mode 100644
index 00000000..0abaf945
--- /dev/null
+++ b/src/main/java/com/youlai/boot/module/config/WorkflowEnvironmentPostProcessor.java
@@ -0,0 +1,64 @@
+package com.youlai.boot.module.config;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.boot.EnvironmentPostProcessor;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor;
+import org.springframework.boot.context.properties.bind.Bindable;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.core.Ordered;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.MapPropertySource;
+
+/**
+ * 工作流关闭时排除 Flowable 引擎自动配置
+ *
+ * @author Ray.Hao
+ * @since 4.9.0
+ */
+public class WorkflowEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
+
+ /** 工作流开关属性 */
+ private static final String WORKFLOW_ENABLED = "flowable.enabled";
+
+ /** 自动配置排除列表属性 */
+ private static final String AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude";
+
+ /** Flowable 引擎相关的自动配置类 */
+ private static final List FLOWABLE_AUTOCONFIGURATIONS = List.of(
+ "org.flowable.spring.boot.ProcessEngineAutoConfiguration",
+ "org.flowable.spring.boot.ProcessEngineServicesAutoConfiguration"
+ );
+
+ @Override
+ public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
+ // 与 WorkflowModuleConfig 的条件一致:未配置视为关闭
+ if ("true".equalsIgnoreCase(environment.getProperty(WORKFLOW_ENABLED))) {
+ return;
+ }
+ // 合并已有的排除项,避免覆盖用户自己的 spring.autoconfigure.exclude
+ List excluded = new ArrayList<>(Binder.get(environment)
+ .bind(AUTOCONFIGURE_EXCLUDE, Bindable.listOf(String.class))
+ .orElseGet(List::of));
+ for (String className : FLOWABLE_AUTOCONFIGURATIONS) {
+ if (!excluded.contains(className)) {
+ excluded.add(className);
+ }
+ }
+ Map properties = new HashMap<>();
+ properties.put(AUTOCONFIGURE_EXCLUDE, String.join(",", excluded));
+ environment.getPropertySources().addFirst(new MapPropertySource("workflowDisabled", properties));
+ }
+
+ /**
+ * 晚于 ConfigDataEnvironmentPostProcessor 执行,保证 profile 配置文件已加载
+ */
+ @Override
+ public int getOrder() {
+ return ConfigDataEnvironmentPostProcessor.ORDER + 1;
+ }
+}
diff --git a/src/main/java/com/youlai/boot/module/config/WorkflowModuleConfig.java b/src/main/java/com/youlai/boot/module/config/WorkflowModuleConfig.java
new file mode 100644
index 00000000..d28c306d
--- /dev/null
+++ b/src/main/java/com/youlai/boot/module/config/WorkflowModuleConfig.java
@@ -0,0 +1,19 @@
+package com.youlai.boot.module.config;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 工作流模块装配
+ *
+ * 仅当 flowable.enabled 为 true 时扫描工作流包,未配置视为关闭
+ *
+ * @author Ray.Hao
+ * @since 4.9.0
+ */
+@Configuration
+@ConditionalOnProperty(name = "flowable.enabled", havingValue = "true")
+@ComponentScan(basePackages = "com.youlai.boot.module.workflow")
+public class WorkflowModuleConfig {
+}
diff --git a/src/main/java/com/youlai/boot/module/form/controller/FormController.java b/src/main/java/com/youlai/boot/module/form/controller/FormController.java
index a00bdae0..fb65cff1 100644
--- a/src/main/java/com/youlai/boot/module/form/controller/FormController.java
+++ b/src/main/java/com/youlai/boot/module/form/controller/FormController.java
@@ -2,16 +2,18 @@ package com.youlai.boot.module.form.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.common.annotation.Log;
-import com.youlai.boot.common.annotation.RepeatSubmit;
+import com.youlai.boot.framework.web.repeatsubmit.RepeatSubmit;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.result.PageResult;
import com.youlai.boot.common.result.Result;
-import com.youlai.boot.common.util.IPUtils;
+import com.youlai.boot.common.exception.BusinessException;
+import com.youlai.boot.common.model.Option;
+import com.youlai.boot.framework.web.util.IPUtils;
import com.youlai.boot.module.form.model.form.FormDefinitionForm;
import com.youlai.boot.module.form.model.form.FormMenuForm;
-import com.youlai.boot.module.form.model.query.FormDataQuery;
-import com.youlai.boot.module.form.model.query.FormDefinitionQuery;
+import com.youlai.boot.module.form.model.query.FormDataPageQuery;
+import com.youlai.boot.module.form.model.query.FormDefinitionPageQuery;
import com.youlai.boot.module.form.model.vo.FormDataVO;
import com.youlai.boot.module.form.model.vo.FormDefinitionPageVO;
import com.youlai.boot.module.form.model.vo.FormMenuVO;
@@ -28,13 +30,14 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
+import java.util.List;
import java.util.Map;
/**
* 表单控制层(表单定义、访问菜单、表单数据)
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Tag(name = "12.表单接口")
@RestController
@@ -49,12 +52,18 @@ public class FormController {
@GetMapping
@Log(module = LogModuleEnum.FORM, value = ActionTypeEnum.LIST)
public PageResult getFormDefinitionPage(
- FormDefinitionQuery queryParams
+ @Valid FormDefinitionPageQuery queryParams
) {
Page result = formDefinitionService.getFormDefinitionPage(queryParams);
return PageResult.success(result);
}
+ @Operation(summary = "获取审批表单下拉选项(工作流设计器绑定表单用)")
+ @GetMapping("/options")
+ public Result>> listWorkflowFormOptions() {
+ return Result.success(formDefinitionService.listWorkflowFormOptions());
+ }
+
@Operation(summary = "获取表单定义表单数据")
@GetMapping("/{formId}/form")
public Result getFormDefinitionForm(
@@ -69,7 +78,7 @@ public class FormController {
@PreAuthorize("@ss.hasPerm('form:definition:create')")
@RepeatSubmit
@Log(module = LogModuleEnum.FORM, value = ActionTypeEnum.INSERT)
- public Result> saveFormDefinition(@Valid @RequestBody FormDefinitionForm formData) {
+ public Result saveFormDefinition(@Valid @RequestBody FormDefinitionForm formData) {
boolean result = formDefinitionService.saveFormDefinition(formData);
return Result.judge(result);
}
@@ -78,9 +87,9 @@ public class FormController {
@PutMapping("/{formId}")
@PreAuthorize("@ss.hasPerm('form:definition:update')")
@Log(module = LogModuleEnum.FORM, value = ActionTypeEnum.UPDATE)
- public Result> updateFormDefinition(
+ public Result updateFormDefinition(
@Parameter(description = "表单ID") @PathVariable Long formId,
- @RequestBody FormDefinitionForm formData
+ @RequestBody @Valid FormDefinitionForm formData
) {
boolean result = formDefinitionService.updateFormDefinition(formId, formData);
return Result.judge(result);
@@ -145,7 +154,7 @@ public class FormController {
@PreAuthorize("@ss.hasPerm('form:definition:update')")
@RepeatSubmit
@Log(module = LogModuleEnum.FORM, value = ActionTypeEnum.INSERT)
- public Result> saveFormMenu(
+ public Result saveFormMenu(
@Parameter(description = "表单ID") @PathVariable Long formId,
@Valid @RequestBody FormMenuForm menuForm
) {
@@ -168,7 +177,7 @@ public class FormController {
@Operation(summary = "匿名提交公开表单数据")
@PostMapping("/public/{formKey}/data")
@RepeatSubmit
- public Result> savePublicFormData(
+ public Result savePublicFormData(
@Parameter(description = "表单唯一标识") @PathVariable String formKey,
@RequestBody Map data,
HttpServletRequest request
@@ -186,7 +195,7 @@ public class FormController {
@Log(module = LogModuleEnum.FORM, value = ActionTypeEnum.LIST)
public PageResult getFormDataPage(
@Parameter(description = "表单唯一标识") @PathVariable String formKey,
- FormDataQuery queryParams
+ @Valid FormDataPageQuery queryParams
) {
Page result = formDataService.getFormDataPage(formKey, queryParams);
return PageResult.success(result);
@@ -196,7 +205,7 @@ public class FormController {
@PostMapping("/{formKey}/data")
@RepeatSubmit
@Log(module = LogModuleEnum.FORM, value = ActionTypeEnum.INSERT)
- public Result> saveFormData(
+ public Result saveFormData(
@Parameter(description = "表单唯一标识") @PathVariable String formKey,
@RequestBody Map data
) {
@@ -212,6 +221,10 @@ public class FormController {
@Parameter(description = "数据ID") @PathVariable Long dataId
) {
FormDataVO dataVO = formDataService.getFormDataDetail(dataId);
+ // 直查数据详情时明确提示;工作流回显场景由服务层调用方自行降级
+ if (dataVO == null) {
+ throw new BusinessException("表单数据不存在");
+ }
return Result.success(dataVO);
}
diff --git a/src/main/java/com/youlai/boot/module/form/converter/FormConverter.java b/src/main/java/com/youlai/boot/module/form/converter/FormConverter.java
index 1eb60631..94a5bc22 100644
--- a/src/main/java/com/youlai/boot/module/form/converter/FormConverter.java
+++ b/src/main/java/com/youlai/boot/module/form/converter/FormConverter.java
@@ -10,7 +10,7 @@ import org.mapstruct.Mapper;
* 表单定义 对象转换器
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Mapper(componentModel = "spring")
public interface FormConverter {
diff --git a/src/main/java/com/youlai/boot/module/form/enums/FormCategoryEnum.java b/src/main/java/com/youlai/boot/module/form/enums/FormCategoryEnum.java
new file mode 100644
index 00000000..61e6b97e
--- /dev/null
+++ b/src/main/java/com/youlai/boot/module/form/enums/FormCategoryEnum.java
@@ -0,0 +1,53 @@
+package com.youlai.boot.module.form.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.youlai.boot.common.base.IBaseEnum;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+/**
+ * 表单类型枚举
+ *
+ * 类型驱动消费方行为:设计器绑定流程时仅列出 workflow 类型,
+ * 避免"意见反馈"等通用收集表单混入流程表单候选项
+ *
+ * @author Ray.Hao
+ * @since 4.9.0
+ */
+@Schema(enumAsRef = true)
+@Getter
+public enum FormCategoryEnum implements IBaseEnum {
+
+ /**
+ * 通用表单:公开收集 / 菜单入口,与流程无关
+ */
+ NORMAL("normal", "普通表单"),
+
+ /**
+ * 工作流表单:可被工作流绑定为发起 / 办理表单
+ */
+ WORKFLOW("workflow", "工作流表单");
+
+ @EnumValue
+ private final String value;
+
+ @JsonValue
+ private final String label;
+
+ FormCategoryEnum(String value, String label) {
+ this.value = value;
+ this.label = label;
+ }
+
+ @Override
+ public String getValue() {
+ return this.value;
+ }
+
+ @Override
+ public String getLabel() {
+ return this.label;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/youlai/boot/module/form/enums/FormStatusEnum.java b/src/main/java/com/youlai/boot/module/form/enums/FormStatusEnum.java
index d64cd17b..299e6810 100644
--- a/src/main/java/com/youlai/boot/module/form/enums/FormStatusEnum.java
+++ b/src/main/java/com/youlai/boot/module/form/enums/FormStatusEnum.java
@@ -12,7 +12,7 @@ import lombok.Getter;
* -1 为停用态,其余为正常生命周期
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Schema(enumAsRef = true)
@Getter
diff --git a/src/main/java/com/youlai/boot/module/form/mapper/FormDataMapper.java b/src/main/java/com/youlai/boot/module/form/mapper/FormDataMapper.java
index 051b2249..16b67a0b 100644
--- a/src/main/java/com/youlai/boot/module/form/mapper/FormDataMapper.java
+++ b/src/main/java/com/youlai/boot/module/form/mapper/FormDataMapper.java
@@ -3,7 +3,7 @@ package com.youlai.boot.module.form.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.module.form.model.entity.FormData;
-import com.youlai.boot.module.form.model.query.FormDataQuery;
+import com.youlai.boot.module.form.model.query.FormDataPageQuery;
import com.youlai.boot.module.form.model.vo.FormDataVO;
import org.apache.ibatis.annotations.Mapper;
@@ -11,7 +11,7 @@ import org.apache.ibatis.annotations.Mapper;
* 表单数据 访问层
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Mapper
public interface FormDataMapper extends BaseMapper {
@@ -24,6 +24,6 @@ public interface FormDataMapper extends BaseMapper {
* @param queryParams 查询参数
* @return 表单数据分页列表
*/
- Page getFormDataPage(Page page, Long formId, FormDataQuery queryParams);
+ Page getFormDataPage(Page page, Long formId, FormDataPageQuery queryParams);
}
diff --git a/src/main/java/com/youlai/boot/module/form/mapper/FormDefinitionMapper.java b/src/main/java/com/youlai/boot/module/form/mapper/FormDefinitionMapper.java
index 1284d3d9..39196b94 100644
--- a/src/main/java/com/youlai/boot/module/form/mapper/FormDefinitionMapper.java
+++ b/src/main/java/com/youlai/boot/module/form/mapper/FormDefinitionMapper.java
@@ -3,7 +3,7 @@ package com.youlai.boot.module.form.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.module.form.model.entity.FormDefinition;
-import com.youlai.boot.module.form.model.query.FormDefinitionQuery;
+import com.youlai.boot.module.form.model.query.FormDefinitionPageQuery;
import com.youlai.boot.module.form.model.vo.FormDefinitionPageVO;
import org.apache.ibatis.annotations.Mapper;
@@ -11,7 +11,7 @@ import org.apache.ibatis.annotations.Mapper;
* 表单定义 访问层
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Mapper
public interface FormDefinitionMapper extends BaseMapper {
@@ -23,6 +23,6 @@ public interface FormDefinitionMapper extends BaseMapper {
* @param queryParams 查询参数
* @return 表单定义分页列表
*/
- Page getFormDefinitionPage(Page page, FormDefinitionQuery queryParams);
+ Page getFormDefinitionPage(Page page, FormDefinitionPageQuery queryParams);
}
diff --git a/src/main/java/com/youlai/boot/module/form/mapper/FormSnapshotMapper.java b/src/main/java/com/youlai/boot/module/form/mapper/FormSnapshotMapper.java
index 874ba2f3..a16e2505 100644
--- a/src/main/java/com/youlai/boot/module/form/mapper/FormSnapshotMapper.java
+++ b/src/main/java/com/youlai/boot/module/form/mapper/FormSnapshotMapper.java
@@ -8,7 +8,7 @@ import org.apache.ibatis.annotations.Mapper;
* 表单版本快照访问层
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Mapper
public interface FormSnapshotMapper extends BaseMapper {
diff --git a/src/main/java/com/youlai/boot/module/form/model/entity/FormData.java b/src/main/java/com/youlai/boot/module/form/model/entity/FormData.java
index 2586a0f9..f2e049f9 100644
--- a/src/main/java/com/youlai/boot/module/form/model/entity/FormData.java
+++ b/src/main/java/com/youlai/boot/module/form/model/entity/FormData.java
@@ -11,7 +11,7 @@ import lombok.EqualsAndHashCode;
* 表单数据实体
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@EqualsAndHashCode(callSuper = true)
@TableName("form_data")
diff --git a/src/main/java/com/youlai/boot/module/form/model/entity/FormDefinition.java b/src/main/java/com/youlai/boot/module/form/model/entity/FormDefinition.java
index fce32187..53c17da2 100644
--- a/src/main/java/com/youlai/boot/module/form/model/entity/FormDefinition.java
+++ b/src/main/java/com/youlai/boot/module/form/model/entity/FormDefinition.java
@@ -12,7 +12,7 @@ import lombok.EqualsAndHashCode;
* 表单定义实体
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@EqualsAndHashCode(callSuper = true)
@TableName("form_definition")
@@ -56,6 +56,11 @@ public class FormDefinition extends BaseEntity {
*/
private Integer isPublic;
+ /**
+ * 表单类型(normal通用表单 workflow审批表单)
+ */
+ private String category;
+
/**
* 生成的访问菜单ID(NULL未生成,幂等防重复)
*/
diff --git a/src/main/java/com/youlai/boot/module/form/model/entity/FormSnapshot.java b/src/main/java/com/youlai/boot/module/form/model/entity/FormSnapshot.java
index 46c7dfb5..d84aebf6 100644
--- a/src/main/java/com/youlai/boot/module/form/model/entity/FormSnapshot.java
+++ b/src/main/java/com/youlai/boot/module/form/model/entity/FormSnapshot.java
@@ -13,7 +13,7 @@ import lombok.EqualsAndHashCode;
* 快照只增不改,审计字段仅保留 BaseEntity 公共部分
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@EqualsAndHashCode(callSuper = true)
@TableName("form_snapshot")
diff --git a/src/main/java/com/youlai/boot/module/form/model/form/FormDefinitionForm.java b/src/main/java/com/youlai/boot/module/form/model/form/FormDefinitionForm.java
index 1fff0269..fbe7f06b 100644
--- a/src/main/java/com/youlai/boot/module/form/model/form/FormDefinitionForm.java
+++ b/src/main/java/com/youlai/boot/module/form/model/form/FormDefinitionForm.java
@@ -10,7 +10,7 @@ import lombok.Data;
* 表单定义表单对象
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Schema(description = "表单定义")
@Data
@@ -43,4 +43,8 @@ public class FormDefinitionForm {
@Schema(description = "是否允许匿名公开访问(0否 1是),公开访问总开关")
private Integer isPublic;
+ @Schema(description = "表单类型(normal通用表单 workflow工作流表单,缺省normal)", example = "normal")
+ @Pattern(regexp = "^(normal|workflow)$", message = "表单类型不合法")
+ private String category;
+
}
diff --git a/src/main/java/com/youlai/boot/module/form/model/form/FormMenuForm.java b/src/main/java/com/youlai/boot/module/form/model/form/FormMenuForm.java
index 71619adb..f7c8df4c 100644
--- a/src/main/java/com/youlai/boot/module/form/model/form/FormMenuForm.java
+++ b/src/main/java/com/youlai/boot/module/form/model/form/FormMenuForm.java
@@ -11,7 +11,7 @@ import java.util.List;
* 表单菜单发布表单对象(发布向导"入口配置"步骤提交)
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Schema(description = "表单菜单发布对象")
@Data
diff --git a/src/main/java/com/youlai/boot/module/form/model/query/FormDataQuery.java b/src/main/java/com/youlai/boot/module/form/model/query/FormDataPageQuery.java
similarity index 63%
rename from src/main/java/com/youlai/boot/module/form/model/query/FormDataQuery.java
rename to src/main/java/com/youlai/boot/module/form/model/query/FormDataPageQuery.java
index 6d24bbe2..577a3c37 100644
--- a/src/main/java/com/youlai/boot/module/form/model/query/FormDataQuery.java
+++ b/src/main/java/com/youlai/boot/module/form/model/query/FormDataPageQuery.java
@@ -6,14 +6,14 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
/**
- * 表单数据查询对象
+ * 表单数据分页查询对象
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Data
@EqualsAndHashCode(callSuper = false)
-@Schema(description = "表单数据查询对象")
-public class FormDataQuery extends BaseQuery {
+@Schema(description = "表单数据分页查询对象")
+public class FormDataPageQuery extends BaseQuery {
}
diff --git a/src/main/java/com/youlai/boot/module/form/model/query/FormDefinitionQuery.java b/src/main/java/com/youlai/boot/module/form/model/query/FormDefinitionPageQuery.java
similarity index 62%
rename from src/main/java/com/youlai/boot/module/form/model/query/FormDefinitionQuery.java
rename to src/main/java/com/youlai/boot/module/form/model/query/FormDefinitionPageQuery.java
index cdef485f..4f00d428 100644
--- a/src/main/java/com/youlai/boot/module/form/model/query/FormDefinitionQuery.java
+++ b/src/main/java/com/youlai/boot/module/form/model/query/FormDefinitionPageQuery.java
@@ -6,15 +6,15 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
/**
- * 表单定义查询对象
+ * 表单定义分页查询对象
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Data
@EqualsAndHashCode(callSuper = false)
-@Schema(description = "表单定义查询对象")
-public class FormDefinitionQuery extends BaseQuery {
+@Schema(description = "表单定义分页查询对象")
+public class FormDefinitionPageQuery extends BaseQuery {
@Schema(description = "关键字(表单名称/标识)")
private String keywords;
@@ -22,4 +22,7 @@ public class FormDefinitionQuery extends BaseQuery {
@Schema(description = "状态(0草稿 1已发布 -1已停用)")
private Integer status;
+ @Schema(description = "表单类型(normal通用表单 workflow审批表单)")
+ private String category;
+
}
diff --git a/src/main/java/com/youlai/boot/module/form/model/vo/FormDataVO.java b/src/main/java/com/youlai/boot/module/form/model/vo/FormDataVO.java
index 571d6d35..1496cd0d 100644
--- a/src/main/java/com/youlai/boot/module/form/model/vo/FormDataVO.java
+++ b/src/main/java/com/youlai/boot/module/form/model/vo/FormDataVO.java
@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
* 表单数据VO(分页行与详情回显共用:数据只读,无独立表单对象)
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Schema(description = "表单数据对象")
@Getter
diff --git a/src/main/java/com/youlai/boot/module/form/model/vo/FormDefinitionPageVO.java b/src/main/java/com/youlai/boot/module/form/model/vo/FormDefinitionPageVO.java
index f6100144..2c393075 100644
--- a/src/main/java/com/youlai/boot/module/form/model/vo/FormDefinitionPageVO.java
+++ b/src/main/java/com/youlai/boot/module/form/model/vo/FormDefinitionPageVO.java
@@ -10,7 +10,7 @@ import java.time.LocalDateTime;
* 表单定义分页VO
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Schema(description = "表单定义分页对象")
@Getter
@@ -38,6 +38,9 @@ public class FormDefinitionPageVO {
@Schema(description = "是否允许匿名公开访问(0否 1是)")
private Integer isPublic;
+ @Schema(description = "表单类型(normal通用表单 workflow审批表单)")
+ private String category;
+
@Schema(description = "创建时间")
private LocalDateTime createTime;
diff --git a/src/main/java/com/youlai/boot/module/form/model/vo/FormMenuVO.java b/src/main/java/com/youlai/boot/module/form/model/vo/FormMenuVO.java
index a891b357..b1de85d9 100644
--- a/src/main/java/com/youlai/boot/module/form/model/vo/FormMenuVO.java
+++ b/src/main/java/com/youlai/boot/module/form/model/vo/FormMenuVO.java
@@ -10,7 +10,7 @@ import java.util.List;
* 表单菜单配置VO(发布向导回显已生成的菜单)
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Schema(description = "表单菜单配置对象")
@Getter
diff --git a/src/main/java/com/youlai/boot/module/form/model/vo/FormRenderVO.java b/src/main/java/com/youlai/boot/module/form/model/vo/FormRenderVO.java
index f81905ac..5ef35561 100644
--- a/src/main/java/com/youlai/boot/module/form/model/vo/FormRenderVO.java
+++ b/src/main/java/com/youlai/boot/module/form/model/vo/FormRenderVO.java
@@ -8,7 +8,7 @@ import lombok.Setter;
* 表单渲染VO(渲染端按 formKey 加载的已发布规则)
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
@Schema(description = "表单渲染对象")
@Getter
diff --git a/src/main/java/com/youlai/boot/module/form/service/FormDataService.java b/src/main/java/com/youlai/boot/module/form/service/FormDataService.java
index ac882643..b1f40ecf 100644
--- a/src/main/java/com/youlai/boot/module/form/service/FormDataService.java
+++ b/src/main/java/com/youlai/boot/module/form/service/FormDataService.java
@@ -1,9 +1,7 @@
package com.youlai.boot.module.form.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.baomidou.mybatisplus.extension.service.IService;
-import com.youlai.boot.module.form.model.entity.FormData;
-import com.youlai.boot.module.form.model.query.FormDataQuery;
+import com.youlai.boot.module.form.model.query.FormDataPageQuery;
import com.youlai.boot.module.form.model.vo.FormDataVO;
import java.util.List;
@@ -13,9 +11,9 @@ import java.util.Map;
* 表单数据业务接口
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
-public interface FormDataService extends IService {
+public interface FormDataService {
/**
* 提交表单数据(仅已发布表单可提交,提交时快照表单版本)
@@ -28,6 +26,17 @@ public interface FormDataService extends IService {
*/
boolean saveFormData(String formKey, Map data);
+ /**
+ * 提交表单数据并返回数据ID
+ *
+ * 供工作流模块发起流程时建立 businessKey 关联(form_data.id),校验规则同 {@link #saveFormData}
+ *
+ * @param formKey 表单唯一标识
+ * @param data 表单数据(field -> value 映射)
+ * @return 表单数据ID
+ */
+ Long saveFormDataReturnId(String formKey, Map data);
+
/**
* 匿名提交公开表单数据(公开分享链接入口)
*
@@ -48,7 +57,7 @@ public interface FormDataService extends IService {
* @param queryParams 查询参数
* @return 表单数据分页列表
*/
- Page getFormDataPage(String formKey, FormDataQuery queryParams);
+ Page getFormDataPage(String formKey, FormDataPageQuery queryParams);
/**
* 获取表单数据详情(只读回显)
@@ -58,6 +67,16 @@ public interface FormDataService extends IService {
*/
FormDataVO getFormDataDetail(Long dataId);
+ /**
+ * 按表单标识删除全部提交数据(演示数据重置用)
+ *
+ * 跨模块窄接口:formKey 到 formId 的解析与删除条件封装在 form 模块内部,
+ * 调用方(workflow)不感知表单实体与查询条件
+ *
+ * @param formKey 表单唯一标识
+ */
+ void removeByFormKey(String formKey);
+
/**
* 批量删除表单数据
*
diff --git a/src/main/java/com/youlai/boot/module/form/service/FormDefinitionService.java b/src/main/java/com/youlai/boot/module/form/service/FormDefinitionService.java
index 587b5221..d33af54d 100644
--- a/src/main/java/com/youlai/boot/module/form/service/FormDefinitionService.java
+++ b/src/main/java/com/youlai/boot/module/form/service/FormDefinitionService.java
@@ -1,11 +1,11 @@
package com.youlai.boot.module.form.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.baomidou.mybatisplus.extension.service.IService;
+import com.youlai.boot.common.model.Option;
import com.youlai.boot.module.form.model.entity.FormDefinition;
import com.youlai.boot.module.form.model.form.FormDefinitionForm;
import com.youlai.boot.module.form.model.form.FormMenuForm;
-import com.youlai.boot.module.form.model.query.FormDefinitionQuery;
+import com.youlai.boot.module.form.model.query.FormDefinitionPageQuery;
import com.youlai.boot.module.form.model.vo.FormDefinitionPageVO;
import com.youlai.boot.module.form.model.vo.FormMenuVO;
import com.youlai.boot.module.form.model.vo.FormRenderVO;
@@ -16,9 +16,9 @@ import java.util.List;
* 表单定义业务接口
*
* @author Ray.Hao
- * @since 4.8.0
+ * @since 4.9.0
*/
-public interface FormDefinitionService extends IService {
+public interface FormDefinitionService {
/**
* 表单定义分页列表
@@ -26,7 +26,15 @@ public interface FormDefinitionService extends IService {
* @param queryParams 查询参数
* @return 表单定义分页列表
*/
- Page getFormDefinitionPage(FormDefinitionQuery queryParams);
+ Page getFormDefinitionPage(FormDefinitionPageQuery queryParams);
+
+ /**
+ * 按主键查询表单定义(供模块内回退取规则等场景)
+ *
+ * @param id 表单ID
+ * @return 表单定义实体;不存在返回 null
+ */
+ FormDefinition getFormDefinition(Long id);
/**
* 获取表单定义表单数据(编辑回显)
@@ -119,4 +127,14 @@ public interface FormDefinitionService extends IService {
*/
FormMenuVO getFormMenu(Long formId);
+ /**
+ * 审批表单下拉选项(供工作流设计器绑定表单)
+ *
+ * 仅返回已发布且类型为 workflow 的表单;value 为 formKey(BPMN formKey 引用),
+ * 草稿/停用表单结构未定不应被流程绑定
+ *
+ * @return 审批表单下拉选项列表
+ */
+ List