generate/status/login 不需要登录态(PC 端未登录),由 Security 配置放行; + * scan/confirm/cancel 需要 APP 端登录态,当前用户 ID 从 Security 上下文获取。
+ * + * @author Ray.Hao + * @since 4.5.0 + */ +@Tag(name = "01.认证中心") +@RestController +@RequestMapping("/api/v1/auth/qr-code") +@RequiredArgsConstructor +public class QrCodeAuthController { + + private final QrCodeLoginService qrCodeLoginService; + + /** + * 生成扫码登录票据 + */ + @Operation(summary = "[扫码]生成扫码登录票据") + @PostMapping("/generate") + @RateLimit(limit = 30, window = 60) + public Result
* 一步完成登录,无需绑定流程,自动创建新用户
*/
- @Operation(summary = "手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序")
+ @Operation(summary = "[小程序]手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序")
@PostMapping("/phone-login")
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
@RateLimit
- public Result
* 绑定成功后自动完成登录
*/
- @Operation(summary = "绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录")
+ @Operation(summary = "[小程序]绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录")
@PostMapping("/bind-mobile")
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
@RateLimit
- public Result
+ * 携带 {@link ResultCode} 用于统一错误响应。
+ *
+ * @author Ray.Hao
+ * @since 3.1.0
*/
@Getter
public class CaptchaException extends RuntimeException {
diff --git a/src/main/java/com/youlai/boot/auth/security/exception/SmsCaptchaException.java b/src/main/java/com/youlai/boot/auth/exception/SmsCaptchaException.java
similarity index 85%
rename from src/main/java/com/youlai/boot/auth/security/exception/SmsCaptchaException.java
rename to src/main/java/com/youlai/boot/auth/exception/SmsCaptchaException.java
index d5bfd0ea..a49e50f8 100644
--- a/src/main/java/com/youlai/boot/auth/security/exception/SmsCaptchaException.java
+++ b/src/main/java/com/youlai/boot/auth/exception/SmsCaptchaException.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.security.exception;
+package com.youlai.boot.auth.exception;
import org.springframework.security.core.AuthenticationException;
diff --git a/src/main/java/com/youlai/boot/auth/qrcode/model/QrCodeLoginContext.java b/src/main/java/com/youlai/boot/auth/model/dto/QrCodeLoginContext.java
similarity index 66%
rename from src/main/java/com/youlai/boot/auth/qrcode/model/QrCodeLoginContext.java
rename to src/main/java/com/youlai/boot/auth/model/dto/QrCodeLoginContext.java
index 724c4f2e..357b67d6 100644
--- a/src/main/java/com/youlai/boot/auth/qrcode/model/QrCodeLoginContext.java
+++ b/src/main/java/com/youlai/boot/auth/model/dto/QrCodeLoginContext.java
@@ -1,11 +1,12 @@
-package com.youlai.boot.auth.qrcode.model;
+package com.youlai.boot.auth.model.dto;
import lombok.Data;
/**
- * 扫码登录票据上下文,序列化为 JSON 存入 Redis。
- *
- * 字段说明见 docs/youlai-boot/scan-code-login.md 的 Redis 存储设计。
+ * 扫码登录票据上下文,序列化为 JSON 存入 Redis
+ *
+ * @author Ray.Hao
+ * @since 4.5.0
*/
@Data
public class QrCodeLoginContext {
@@ -13,7 +14,7 @@ public class QrCodeLoginContext {
/** 票据,UUID 无连字符 */
private String ticket;
- /** 状态枚举名,取 {@link QrCodeLoginStatusEnum#name()} */
+ /** 状态枚举名,取 {@link com.youlai.boot.auth.enums.QrCodeLoginStatusEnum#name()} */
private String status;
/** 扫码用户 ID,scan 时写入 */
@@ -34,6 +35,6 @@ public class QrCodeLoginContext {
/** 确认时间戳(毫秒) */
private Long confirmedAt;
- /** generate 时的 PC 端 IP,用于审计 */
+ /** generate 时的 PC 端 IP */
private String clientIp;
}
diff --git a/src/main/java/com/youlai/boot/auth/model/form/LoginForm.java b/src/main/java/com/youlai/boot/auth/model/req/LoginReq.java
similarity index 89%
rename from src/main/java/com/youlai/boot/auth/model/form/LoginForm.java
rename to src/main/java/com/youlai/boot/auth/model/req/LoginReq.java
index a4f7d752..bb8c9002 100644
--- a/src/main/java/com/youlai/boot/auth/model/form/LoginForm.java
+++ b/src/main/java/com/youlai/boot/auth/model/req/LoginReq.java
@@ -1,15 +1,15 @@
-package com.youlai.boot.auth.model.form;
+package com.youlai.boot.auth.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
- * 登录表单
+ * 登录请求参数
*/
@Schema(description = "登录请求参数")
@Data
-public class LoginForm {
+public class LoginReq {
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
@NotBlank(message = "用户名不能为空")
diff --git a/src/main/java/com/youlai/boot/auth/model/req/QrCodeTicketReq.java b/src/main/java/com/youlai/boot/auth/model/req/QrCodeTicketReq.java
new file mode 100644
index 00000000..9e8e86d1
--- /dev/null
+++ b/src/main/java/com/youlai/boot/auth/model/req/QrCodeTicketReq.java
@@ -0,0 +1,17 @@
+package com.youlai.boot.auth.model.req;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+/**
+ * 扫码登录票据请求,用于 scan/confirm/cancel/login 接口
+ *
+ * @author Ray.Hao
+ * @since 4.5.0
+ */
+@Data
+public class QrCodeTicketReq {
+
+ @NotBlank(message = "票据不能为空")
+ private String ticket;
+}
diff --git a/src/main/java/com/youlai/boot/auth/model/form/WxMaBindMobileForm.java b/src/main/java/com/youlai/boot/auth/model/req/WxMaBindMobileReq.java
similarity index 85%
rename from src/main/java/com/youlai/boot/auth/model/form/WxMaBindMobileForm.java
rename to src/main/java/com/youlai/boot/auth/model/req/WxMaBindMobileReq.java
index 118abb39..2dfea5c2 100644
--- a/src/main/java/com/youlai/boot/auth/model/form/WxMaBindMobileForm.java
+++ b/src/main/java/com/youlai/boot/auth/model/req/WxMaBindMobileReq.java
@@ -1,15 +1,15 @@
-package com.youlai.boot.auth.model.form;
+package com.youlai.boot.auth.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
- * 微信小程序绑定手机号表单
+ * 微信小程序绑定手机号请求
*/
@Schema(description = "微信小程序绑定手机号请求")
@Data
-public class WxMaBindMobileForm {
+public class WxMaBindMobileReq {
@NotBlank(message = "openid 不能为空")
@Schema(description = "微信用户唯一标识", example = "oVBkZ0aYgDMDIywRdgPW8-joxXc4")
diff --git a/src/main/java/com/youlai/boot/auth/model/form/WxMaPhoneLoginForm.java b/src/main/java/com/youlai/boot/auth/model/req/WxMaPhoneLoginReq.java
similarity index 82%
rename from src/main/java/com/youlai/boot/auth/model/form/WxMaPhoneLoginForm.java
rename to src/main/java/com/youlai/boot/auth/model/req/WxMaPhoneLoginReq.java
index 731adb1c..0fa3041b 100644
--- a/src/main/java/com/youlai/boot/auth/model/form/WxMaPhoneLoginForm.java
+++ b/src/main/java/com/youlai/boot/auth/model/req/WxMaPhoneLoginReq.java
@@ -1,15 +1,15 @@
-package com.youlai.boot.auth.model.form;
+package com.youlai.boot.auth.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
- * 微信小程序手机号快捷登录表单
+ * 微信小程序手机号快捷登录请求
*/
@Schema(description = "微信小程序手机号快捷登录请求")
@Data
-public class WxMaPhoneLoginForm {
+public class WxMaPhoneLoginReq {
@NotBlank(message = "微信登录凭证不能为空")
@Schema(description = "微信登录凭证(wx.login 获取)", example = "0xxx")
diff --git a/src/main/java/com/youlai/boot/framework/captcha/model/CaptchaInfo.java b/src/main/java/com/youlai/boot/auth/model/resp/CaptchaInfo.java
similarity index 90%
rename from src/main/java/com/youlai/boot/framework/captcha/model/CaptchaInfo.java
rename to src/main/java/com/youlai/boot/auth/model/resp/CaptchaInfo.java
index ad845301..8897c982 100644
--- a/src/main/java/com/youlai/boot/framework/captcha/model/CaptchaInfo.java
+++ b/src/main/java/com/youlai/boot/auth/model/resp/CaptchaInfo.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.captcha.model;
+package com.youlai.boot.auth.model.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
diff --git a/src/main/java/com/youlai/boot/auth/qrcode/model/vo/QrCodeGenerateVO.java b/src/main/java/com/youlai/boot/auth/model/resp/QrCodeGenerateResp.java
similarity index 69%
rename from src/main/java/com/youlai/boot/auth/qrcode/model/vo/QrCodeGenerateVO.java
rename to src/main/java/com/youlai/boot/auth/model/resp/QrCodeGenerateResp.java
index 117ef015..dc830b15 100644
--- a/src/main/java/com/youlai/boot/auth/qrcode/model/vo/QrCodeGenerateVO.java
+++ b/src/main/java/com/youlai/boot/auth/model/resp/QrCodeGenerateResp.java
@@ -1,16 +1,19 @@
-package com.youlai.boot.auth.qrcode.model.vo;
+package com.youlai.boot.auth.model.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
/**
- * generate 接口响应。
+ * generate 接口响应
+ *
+ * @author Ray.Hao
+ * @since 4.5.0
*/
@Data
@Builder
@Schema(description = "扫码票据生成结果")
-public class QrCodeGenerateVO {
+public class QrCodeGenerateResp {
@Schema(description = "票据")
private String ticket;
diff --git a/src/main/java/com/youlai/boot/auth/qrcode/model/vo/QrCodeStatusVO.java b/src/main/java/com/youlai/boot/auth/model/resp/QrCodeStatusResp.java
similarity index 80%
rename from src/main/java/com/youlai/boot/auth/qrcode/model/vo/QrCodeStatusVO.java
rename to src/main/java/com/youlai/boot/auth/model/resp/QrCodeStatusResp.java
index 9b3d649e..b2ea3a2d 100644
--- a/src/main/java/com/youlai/boot/auth/qrcode/model/vo/QrCodeStatusVO.java
+++ b/src/main/java/com/youlai/boot/auth/model/resp/QrCodeStatusResp.java
@@ -1,16 +1,19 @@
-package com.youlai.boot.auth.qrcode.model.vo;
+package com.youlai.boot.auth.model.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
/**
- * status/scan/confirm/cancel 接口响应。
+ * status/scan/confirm/cancel 接口响应
+ *
+ * @author Ray.Hao
+ * @since 4.5.0
*/
@Data
@Builder
@Schema(description = "扫码登录状态")
-public class QrCodeStatusVO {
+public class QrCodeStatusResp {
@Schema(description = "票据")
private String ticket;
diff --git a/src/main/java/com/youlai/boot/auth/model/vo/WxMaLoginVO.java b/src/main/java/com/youlai/boot/auth/model/resp/WxMaLoginResp.java
similarity index 92%
rename from src/main/java/com/youlai/boot/auth/model/vo/WxMaLoginVO.java
rename to src/main/java/com/youlai/boot/auth/model/resp/WxMaLoginResp.java
index 9616a755..5e168d8a 100644
--- a/src/main/java/com/youlai/boot/auth/model/vo/WxMaLoginVO.java
+++ b/src/main/java/com/youlai/boot/auth/model/resp/WxMaLoginResp.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.model.vo;
+package com.youlai.boot.auth.model.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
@@ -14,7 +14,7 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "微信小程序登录响应")
-public class WxMaLoginVO {
+public class WxMaLoginResp {
@Schema(description = "是否新用户")
private Boolean isNewUser;
diff --git a/src/main/java/com/youlai/boot/auth/qrcode/controller/QrCodeLoginController.java b/src/main/java/com/youlai/boot/auth/qrcode/controller/QrCodeLoginController.java
deleted file mode 100644
index 22765993..00000000
--- a/src/main/java/com/youlai/boot/auth/qrcode/controller/QrCodeLoginController.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.youlai.boot.auth.qrcode.controller;
-
-import cn.hutool.core.util.StrUtil;
-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.framework.annotation.RateLimit;
-import com.youlai.boot.common.result.Result;
-import com.youlai.boot.framework.security.model.AuthenticationToken;
-import com.youlai.boot.framework.security.util.SecurityUtils;
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.validation.Valid;
-import lombok.RequiredArgsConstructor;
-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;
-
-/**
- * 扫码登录接口。
- *
- * generate/status/login 不需要登录态(PC 端未登录),由 Security 配置放行;
- * scan/confirm/cancel 需要 APP 端登录态,当前用户 ID 从 Security 上下文获取。
- */
-@Tag(name = "02.扫码登录")
-@RestController
-@RequestMapping("/api/v1/auth/qr-code")
-@RequiredArgsConstructor
-public class QrCodeLoginController {
-
- private final QrCodeLoginService qrCodeLoginService;
-
- @Operation(summary = "生成扫码登录票据")
- @PostMapping("/generate")
- @RateLimit(limit = 30, window = 60)
- public Result
- * WAITING 票据已创建,等待 APP 扫码
- * SCANNED APP 已扫码,等待用户在手机上确认
- * CONFIRMED 用户已在 APP 上确认登录
- * LOGGED_IN PC 已用票据换取会话令牌,票据作废,不可再用
- * CANCELED 用户在 APP 上取消登录
- * EXPIRED 票据超时,由 Redis TTL 自动清理,内存中通常不会出现该值
- */
-public enum QrCodeLoginStatusEnum {
-
- WAITING,
- SCANNED,
- CONFIRMED,
- LOGGED_IN,
- CANCELED,
- EXPIRED
-}
diff --git a/src/main/java/com/youlai/boot/auth/qrcode/model/form/QrCodeTicketForm.java b/src/main/java/com/youlai/boot/auth/qrcode/model/form/QrCodeTicketForm.java
deleted file mode 100644
index 51d151b7..00000000
--- a/src/main/java/com/youlai/boot/auth/qrcode/model/form/QrCodeTicketForm.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.youlai.boot.auth.qrcode.model.form;
-
-import jakarta.validation.constraints.NotBlank;
-import lombok.Data;
-
-/**
- * 扫码登录票据表单,用于 scan/confirm/cancel/login 接口。
- */
-@Data
-public class QrCodeTicketForm {
-
- @NotBlank(message = "票据不能为空")
- private String ticket;
-}
diff --git a/src/main/java/com/youlai/boot/auth/qrcode/service/QrCodeLoginService.java b/src/main/java/com/youlai/boot/auth/qrcode/service/QrCodeLoginService.java
deleted file mode 100644
index 9c8a2012..00000000
--- a/src/main/java/com/youlai/boot/auth/qrcode/service/QrCodeLoginService.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.youlai.boot.auth.qrcode.service;
-
-import com.youlai.boot.auth.qrcode.model.vo.QrCodeGenerateVO;
-import com.youlai.boot.auth.qrcode.model.vo.QrCodeStatusVO;
-import com.youlai.boot.framework.security.model.AuthenticationToken;
-
-/**
- * 扫码登录服务。
- */
-public interface QrCodeLoginService {
-
- /** 生成票据,未登录调用 */
- QrCodeGenerateVO generate(String clientIp);
-
- /** 查询状态,未登录调用 */
- QrCodeStatusVO status(String ticket);
-
- /** APP 标记已扫码,需 APP 端已登录 */
- QrCodeStatusVO scan(String ticket, Long userId);
-
- /** APP 确认登录,需 APP 端已登录 */
- QrCodeStatusVO confirm(String ticket, Long userId);
-
- /** APP 取消登录,需 APP 端已登录 */
- QrCodeStatusVO cancel(String ticket, Long userId);
-
- /** PC 端用票据换取会话令牌,未登录调用 */
- AuthenticationToken login(String ticket);
-}
diff --git a/src/main/java/com/youlai/boot/auth/qrcode/util/QrCodeNicknameMasker.java b/src/main/java/com/youlai/boot/auth/qrcode/util/QrCodeNicknameMasker.java
deleted file mode 100644
index 01fcc760..00000000
--- a/src/main/java/com/youlai/boot/auth/qrcode/util/QrCodeNicknameMasker.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.youlai.boot.auth.qrcode.util;
-
-/**
- * 昵称脱敏:保留首尾各一个字符,中间用 * 替换。
- *
- * 长度 1:原样返回
- * 长度 2:首字 + *
- * 长度 ≥3:首字 + (n-2) 个 * + 末字
- */
-public final class QrCodeNicknameMasker {
-
- private QrCodeNicknameMasker() {
- }
-
- public static String mask(String nickname) {
- if (nickname == null || nickname.isEmpty()) {
- return "";
- }
- int len = nickname.length();
- if (len == 1) {
- return nickname;
- }
- if (len == 2) {
- return nickname.charAt(0) + "*";
- }
- StringBuilder sb = new StringBuilder();
- sb.append(nickname.charAt(0));
- for (int i = 0; i < len - 2; i++) {
- sb.append('*');
- }
- sb.append(nickname.charAt(len - 1));
- return sb.toString();
- }
-}
diff --git a/src/main/java/com/youlai/boot/auth/security/filter/CaptchaValidationFilter.java b/src/main/java/com/youlai/boot/auth/security/filter/CaptchaValidationFilter.java
index db2b4973..34f569ab 100644
--- a/src/main/java/com/youlai/boot/auth/security/filter/CaptchaValidationFilter.java
+++ b/src/main/java/com/youlai/boot/auth/security/filter/CaptchaValidationFilter.java
@@ -6,8 +6,8 @@ import cn.hutool.json.JSONUtil;
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.framework.captcha.exception.CaptchaException;
-import com.youlai.boot.framework.captcha.service.CaptchaService;
+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;
diff --git a/src/main/java/com/youlai/boot/auth/security/provider/SmsAuthenticationProvider.java b/src/main/java/com/youlai/boot/auth/security/provider/SmsAuthenticationProvider.java
index 68626416..3423a033 100644
--- a/src/main/java/com/youlai/boot/auth/security/provider/SmsAuthenticationProvider.java
+++ b/src/main/java/com/youlai/boot/auth/security/provider/SmsAuthenticationProvider.java
@@ -6,8 +6,8 @@ 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.security.exception.SmsCaptchaException;
-import com.youlai.boot.auth.security.model.SmsAuthenticationToken;
+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/security/provider/WxMaAuthenticationProvider.java b/src/main/java/com/youlai/boot/auth/security/provider/WxMaAuthenticationProvider.java
index 3b40f2f5..7ebfc92c 100644
--- a/src/main/java/com/youlai/boot/auth/security/provider/WxMaAuthenticationProvider.java
+++ b/src/main/java/com/youlai/boot/auth/security/provider/WxMaAuthenticationProvider.java
@@ -7,8 +7,8 @@ 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.auth.security.exception.MobileNotBoundException;
-import com.youlai.boot.auth.security.model.WxMaAuthenticationToken;
+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;
diff --git a/src/main/java/com/youlai/boot/auth/security/model/SmsAuthenticationToken.java b/src/main/java/com/youlai/boot/auth/security/token/SmsAuthenticationToken.java
similarity index 97%
rename from src/main/java/com/youlai/boot/auth/security/model/SmsAuthenticationToken.java
rename to src/main/java/com/youlai/boot/auth/security/token/SmsAuthenticationToken.java
index 064e277b..e04de05f 100644
--- a/src/main/java/com/youlai/boot/auth/security/model/SmsAuthenticationToken.java
+++ b/src/main/java/com/youlai/boot/auth/security/token/SmsAuthenticationToken.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.security.model;
+package com.youlai.boot.auth.security.token;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
diff --git a/src/main/java/com/youlai/boot/auth/security/model/WxMaAuthenticationToken.java b/src/main/java/com/youlai/boot/auth/security/token/WxMaAuthenticationToken.java
similarity index 97%
rename from src/main/java/com/youlai/boot/auth/security/model/WxMaAuthenticationToken.java
rename to src/main/java/com/youlai/boot/auth/security/token/WxMaAuthenticationToken.java
index 3f012894..7d21d92c 100644
--- a/src/main/java/com/youlai/boot/auth/security/model/WxMaAuthenticationToken.java
+++ b/src/main/java/com/youlai/boot/auth/security/token/WxMaAuthenticationToken.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.auth.security.model;
+package com.youlai.boot.auth.security.token;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
diff --git a/src/main/java/com/youlai/boot/auth/service/AuthService.java b/src/main/java/com/youlai/boot/auth/service/AuthService.java
index 2247b118..acca88df 100644
--- a/src/main/java/com/youlai/boot/auth/service/AuthService.java
+++ b/src/main/java/com/youlai/boot/auth/service/AuthService.java
@@ -1,56 +1,143 @@
package com.youlai.boot.auth.service;
-import com.youlai.boot.framework.captcha.model.CaptchaInfo;
+import cn.hutool.core.util.StrUtil;
+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 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;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
/**
- * 认证服务接口
+ * 认证服务
*
* @author Ray.Hao
* @since 2.4.0
*/
-public interface AuthService {
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class AuthService {
+
+ private final AuthenticationManager authenticationManager;
+ private final TokenManager tokenManager;
+
+ private final SmsService smsService;
+ private final RedisTemplate
- * 票据上下文以 JSON 形式存入 Redis,TTL 与有效期一致;状态流转的合法性全部由
- * {@link #requireStatus} / {@link #requireSameUser} 在写入前拦截,非法迁移直接抛业务异常,
- * 因此同一 ticket 被多端并发操作时不需要分布式锁——最坏情况是后到的操作在状态校验处失败。
- * PC 端换取会话时直接复用 {@link TokenManager#generateToken(Authentication)},
- * 生成的令牌与账号密码登录走的是同一套会话治理(单/多设备、登出、刷新),前端无差别处理。
+ * 扫码登录服务
+ *
+ * @author Ray.Hao
+ * @since 4.5.0
*/
@Service
@RequiredArgsConstructor
@Slf4j
-public class QrCodeLoginServiceImpl implements QrCodeLoginService {
+public class QrCodeLoginService {
- /** 票据默认有效期(秒),二维码在此时间内未走完流程即由 Redis TTL 自动清理 */
+ /** 票据默认有效期(秒) */
private static final int DEFAULT_EXPIRE_SECONDS = 300;
- /** 状态流转时若 Redis 剩余 TTL 小于该值,补足到此值,避免临界点票据在下一步操作前被清掉 */
+ /** 状态流转时若 Redis 剩余 TTL 小于该值,补足到此值 */
private static final int MIN_REMAIN_SECONDS = 30;
/**
@@ -56,8 +51,10 @@ public class QrCodeLoginServiceImpl implements QrCodeLoginService {
private final TokenManager tokenManager;
private final UserSocialService userSocialService;
- @Override
- public QrCodeGenerateVO generate(String clientIp) {
+ /**
+ * 生成扫码票据,写入 Redis 后返回 ticket 和有效期
+ */
+ public QrCodeGenerateResp generate(String clientIp) {
String ticket = IdUtil.fastSimpleUUID();
QrCodeLoginContext ctx = new QrCodeLoginContext();
ctx.setTicket(ticket);
@@ -65,60 +62,70 @@ public class QrCodeLoginServiceImpl implements QrCodeLoginService {
ctx.setCreatedAt(System.currentTimeMillis());
ctx.setClientIp(clientIp);
save(ctx, DEFAULT_EXPIRE_SECONDS);
- return QrCodeGenerateVO.builder()
+ return QrCodeGenerateResp.builder()
.ticket(ticket)
.expireSeconds(DEFAULT_EXPIRE_SECONDS)
.build();
}
- @Override
- public QrCodeStatusVO status(String ticket) {
+ /**
+ * PC 端轮询扫码状态,未登录调用
+ */
+ public QrCodeStatusResp status(String ticket) {
QrCodeLoginContext ctx = loadOrThrow(ticket);
- return toVO(ctx, remainingSeconds(ticket));
+ return toResp(ctx, remainingSeconds(ticket));
}
- @Override
- public QrCodeStatusVO scan(String ticket, Long userId) {
+ /**
+ * APP 端标记已扫码,需已登录。WAITING → SCANNED
+ */
+ public QrCodeStatusResp scan(String ticket, Long userId) {
QrCodeLoginContext ctx = loadOrThrow(ticket);
requireStatus(ctx, QrCodeLoginStatusEnum.WAITING);
fillUserInfo(ctx, userId);
ctx.setStatus(QrCodeLoginStatusEnum.SCANNED.name());
ctx.setScannedAt(System.currentTimeMillis());
save(ctx, refreshTtl(ticket));
- return toVO(ctx, remainingSeconds(ticket));
+ return toResp(ctx, remainingSeconds(ticket));
}
- @Override
- public QrCodeStatusVO confirm(String ticket, Long userId) {
+ /**
+ * APP 端确认登录,需扫码本人操作。SCANNED → CONFIRMED
+ */
+ public QrCodeStatusResp confirm(String ticket, Long userId) {
QrCodeLoginContext ctx = loadOrThrow(ticket);
requireStatus(ctx, QrCodeLoginStatusEnum.SCANNED);
requireSameUser(ctx, userId);
ctx.setStatus(QrCodeLoginStatusEnum.CONFIRMED.name());
ctx.setConfirmedAt(System.currentTimeMillis());
save(ctx, refreshTtl(ticket));
- return toVO(ctx, remainingSeconds(ticket));
+ return toResp(ctx, remainingSeconds(ticket));
}
- @Override
- public QrCodeStatusVO cancel(String ticket, Long userId) {
+ /**
+ * APP 端取消登录,仅流程中的票据可取消
+ */
+ public QrCodeStatusResp cancel(String ticket, Long userId) {
QrCodeLoginContext ctx = loadOrThrow(ticket);
QrCodeLoginStatusEnum current = QrCodeLoginStatusEnum.valueOf(ctx.getStatus());
- // 只有还在流程中的票据(等待扫码 / 已扫码 / 已确认)允许取消,已登录或已取消的重复操作直接拒绝
+ // 只有还在流程中的票据(等待扫码 / 已扫码 / 已确认)允许取消
if (current != QrCodeLoginStatusEnum.WAITING
&& current != QrCodeLoginStatusEnum.SCANNED
&& current != QrCodeLoginStatusEnum.CONFIRMED) {
throw new TokenInvalidException(ResultCode.QR_CODE_STATUS_ILLEGAL);
}
- // 一旦有人扫过码,取消权就归扫码本人,防止他人替扫码用户取消
+ // 一旦有人扫过码,取消权就归扫码本人
if (current != QrCodeLoginStatusEnum.WAITING && ctx.getUserId() != null) {
requireSameUser(ctx, userId);
}
ctx.setStatus(QrCodeLoginStatusEnum.CANCELED.name());
save(ctx, refreshTtl(ticket));
- return toVO(ctx, remainingSeconds(ticket));
+ return toResp(ctx, remainingSeconds(ticket));
}
- @Override
+ /**
+ * PC 端用票据换取会话令牌,CONFIRMED → LOGGED_IN(一次性)
+ */
public AuthenticationToken login(String ticket) {
QrCodeLoginContext ctx = loadOrThrow(ticket);
requireStatus(ctx, QrCodeLoginStatusEnum.CONFIRMED);
@@ -130,15 +137,15 @@ public class QrCodeLoginServiceImpl implements QrCodeLoginService {
Authentication authentication = new UsernamePasswordAuthenticationToken(
details, null, details.getAuthorities());
AuthenticationToken token = tokenManager.generateToken(authentication);
- // 换取令牌成功后立即把票据置为已使用(一次性),再次 login 会在 requireStatus(CONFIRMED) 处被拒,杜绝重放
+ // 换取令牌成功后立即把票据置为已使用(一次性),再次 login 会在 requireStatus(CONFIRMED) 处被拒
ctx.setStatus(QrCodeLoginStatusEnum.LOGGED_IN.name());
save(ctx, Math.max(remainingSeconds(ticket), MIN_REMAIN_SECONDS));
return token;
}
- // ======================== private ========================
-
- /** 读取票据上下文,票据为空、不存在或已过期都视为 QR_CODE_NOT_FOUND */
+ /**
+ * 读取票据上下文,票据为空、不存在或已过期都视为 QR_CODE_NOT_FOUND
+ */
private QrCodeLoginContext loadOrThrow(String ticket) {
if (StrUtil.isBlank(ticket)) {
throw new TokenInvalidException(ResultCode.QR_CODE_NOT_FOUND);
@@ -159,21 +166,27 @@ public class QrCodeLoginServiceImpl implements QrCodeLoginService {
}
}
- /** 当前状态必须等于期望状态,否则说明流程被跳步或重复操作 */
+ /**
+ * 当前状态必须等于期望状态,否则说明流程被跳步或重复操作
+ */
private void requireStatus(QrCodeLoginContext ctx, QrCodeLoginStatusEnum expected) {
if (!expected.name().equals(ctx.getStatus())) {
throw new TokenInvalidException(ResultCode.QR_CODE_STATUS_ILLEGAL);
}
}
- /** 操作者必须是当初扫码的那个用户,防止 A 扫码 B 确认 */
+ /**
+ * 操作者必须是当初扫码的那个用户,防止 A 扫码 B 确认
+ */
private void requireSameUser(QrCodeLoginContext ctx, Long userId) {
if (ctx.getUserId() == null || !ctx.getUserId().equals(userId)) {
throw new TokenInvalidException(ResultCode.QR_CODE_USER_MISMATCH);
}
}
- /** 扫码时把当前 APP 用户的昵称、头像写进上下文,供 PC 端 status 展示 */
+ /**
+ * 扫码时把当前 APP 用户的昵称、头像写进上下文,供 PC 端 status 展示
+ */
private void fillUserInfo(QrCodeLoginContext ctx, Long userId) {
SecurityUser info = userSocialService.getAuthInfoByUserId(userId);
if (info == null) {
@@ -184,35 +197,43 @@ public class QrCodeLoginServiceImpl implements QrCodeLoginService {
ctx.setAvatar(info.getAvatar());
}
- /** 重写整个上下文并刷新 TTL */
+ /**
+ * 重写整个上下文并刷新 TTL
+ */
private void save(QrCodeLoginContext ctx, int ttl) {
redisTemplate.opsForValue().set(key(ctx.getTicket()), ctx, ttl, TimeUnit.SECONDS);
}
- /** 票据在 Redis 中的剩余秒数,取不到或已过期返回 0 */
+ /**
+ * 票据在 Redis 中的剩余秒数,取不到或已过期返回 0
+ */
private int remainingSeconds(String ticket) {
Long ttl = redisTemplate.getExpire(key(ticket), TimeUnit.SECONDS);
return ttl == null ? 0 : Math.max(ttl.intValue(), 0);
}
- /** 状态流转时写回的 TTL:维持剩余时间,不足 MIN_REMAIN_SECONDS 则补足 */
+ /**
+ * 状态流转时写回的 TTL:维持剩余时间,不足 MIN_REMAIN_SECONDS 则补足
+ */
private int refreshTtl(String ticket) {
int remain = remainingSeconds(ticket);
return remain < MIN_REMAIN_SECONDS ? MIN_REMAIN_SECONDS : remain;
}
- /** 上下文转前端 VO,昵称脱敏、用户信息仅在扫码后暴露 */
- private QrCodeStatusVO toVO(QrCodeLoginContext ctx, int expireSeconds) {
+ /**
+ * 上下文转前端 VO,昵称脱敏、用户信息仅在扫码后暴露
+ */
+ private QrCodeStatusResp toResp(QrCodeLoginContext ctx, int expireSeconds) {
QrCodeLoginStatusEnum status = QrCodeLoginStatusEnum.valueOf(ctx.getStatus());
String nickname = null;
String avatar = null;
// WAITING 阶段谁都能查状态,此时不能泄露用户信息;扫码/确认后才回传脱敏昵称与头像
if (status == QrCodeLoginStatusEnum.SCANNED
|| status == QrCodeLoginStatusEnum.CONFIRMED) {
- nickname = QrCodeNicknameMasker.mask(ctx.getNickname());
+ nickname = maskNickname(ctx.getNickname());
avatar = ctx.getAvatar();
}
- return QrCodeStatusVO.builder()
+ return QrCodeStatusResp.builder()
.ticket(ctx.getTicket())
.status(status.name())
.nickname(nickname)
@@ -221,7 +242,23 @@ public class QrCodeLoginServiceImpl implements QrCodeLoginService {
.build();
}
- /** 拼接票据在 Redis 中的完整 Key */
+ /**
+ * 昵称脱敏:保留首尾各一个字符,中间用 * 替换
+ */
+ private String maskNickname(String nickname) {
+ if (StrUtil.isBlank(nickname)) {
+ return "";
+ }
+ int len = nickname.length();
+ if (len == 1) {
+ return nickname;
+ }
+ return StrUtil.hide(nickname, 1, len - 1);
+ }
+
+ /**
+ * 拼接票据在 Redis 中的完整 Key
+ */
private String key(String ticket) {
return StrUtil.format(RedisConstants.Auth.QR_CODE_LOGIN, ticket);
}
diff --git a/src/main/java/com/youlai/boot/auth/service/WxMaAuthService.java b/src/main/java/com/youlai/boot/auth/service/WxMaAuthService.java
index 2ead8689..aff402bb 100644
--- a/src/main/java/com/youlai/boot/auth/service/WxMaAuthService.java
+++ b/src/main/java/com/youlai/boot/auth/service/WxMaAuthService.java
@@ -1,15 +1,53 @@
package com.youlai.boot.auth.service;
-import com.youlai.boot.auth.model.vo.WxMaLoginVO;
+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.common.constant.RedisConstants;
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 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;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
/**
- * 微信小程序认证服务接口
+ * 微信小程序认证服务
*
* @author Ray.Hao
- * @since 2.4.0
+ * @since 4.0.0
*/
-public interface WxMaAuthService {
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class WxMaAuthService {
+
+ private final WxMaService wxMaService;
+ private final AuthenticationManager authenticationManager;
+ private final TokenManager tokenManager;
+ private final UserService userService;
+ private final UserSocialService userSocialService;
+ private final UserRoleService userRoleService;
+ private final RedisTemplate
* 同时使用微信登录凭证和手机号授权凭证,
* 一步完成用户注册/登录,无需额外绑定流程。
- * 适用于企业认证的小程序(已开通手机号快捷登录权限)。
*
+ * 新用户默认分配 GUEST(访问游客)角色
+ *
- * 新用户默认分配 GUEST(访问游客)角色
- *
+ * value 作为 MyBatis-Plus 枚举值持久化到数据库。
+ *
+ * @author Ray.Hao
+ * @since 2.17.0
*/
@Getter
public enum SocialPlatformEnum implements IBaseEnum
+ * 携带 openid 和 sessionKey 供前端引导用户完成手机号绑定。
+ *
+ * @author Ray.Hao
+ * @since 3.1.0
*/
public class MobileNotBoundException extends AuthenticationException {
diff --git a/src/main/java/com/youlai/boot/framework/web/exception/RateLimitException.java b/src/main/java/com/youlai/boot/common/exception/RateLimitException.java
similarity index 92%
rename from src/main/java/com/youlai/boot/framework/web/exception/RateLimitException.java
rename to src/main/java/com/youlai/boot/common/exception/RateLimitException.java
index 9374b6ba..012bac2d 100644
--- a/src/main/java/com/youlai/boot/framework/web/exception/RateLimitException.java
+++ b/src/main/java/com/youlai/boot/common/exception/RateLimitException.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.web.exception;
+package com.youlai.boot.common.exception;
import com.youlai.boot.common.result.ResultCode;
import lombok.Getter;
diff --git a/src/main/java/com/youlai/boot/framework/security/exception/TokenInvalidException.java b/src/main/java/com/youlai/boot/common/exception/TokenInvalidException.java
similarity index 89%
rename from src/main/java/com/youlai/boot/framework/security/exception/TokenInvalidException.java
rename to src/main/java/com/youlai/boot/common/exception/TokenInvalidException.java
index a4bb93c2..67a5e2c5 100644
--- a/src/main/java/com/youlai/boot/framework/security/exception/TokenInvalidException.java
+++ b/src/main/java/com/youlai/boot/common/exception/TokenInvalidException.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.security.exception;
+package com.youlai.boot.common.exception;
import com.youlai.boot.common.result.ResultCode;
import lombok.Getter;
diff --git a/src/main/java/com/youlai/boot/common/validator/FieldValidator.java b/src/main/java/com/youlai/boot/common/validator/FieldValidator.java
index 9659627f..200e67c8 100644
--- a/src/main/java/com/youlai/boot/common/validator/FieldValidator.java
+++ b/src/main/java/com/youlai/boot/common/validator/FieldValidator.java
@@ -1,6 +1,6 @@
package com.youlai.boot.common.validator;
-import com.youlai.boot.framework.annotation.ValidField;
+import com.youlai.boot.common.annotation.ValidField;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
diff --git a/src/main/java/com/youlai/boot/framework/cache/CaffeineConfig.java b/src/main/java/com/youlai/boot/config/CaffeineConfig.java
similarity index 96%
rename from src/main/java/com/youlai/boot/framework/cache/CaffeineConfig.java
rename to src/main/java/com/youlai/boot/config/CaffeineConfig.java
index 24c494d4..bba14491 100644
--- a/src/main/java/com/youlai/boot/framework/cache/CaffeineConfig.java
+++ b/src/main/java/com/youlai/boot/config/CaffeineConfig.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.cache;
+package com.youlai.boot.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
diff --git a/src/main/java/com/youlai/boot/framework/captcha/config/CaptchaConfig.java b/src/main/java/com/youlai/boot/config/CaptchaConfig.java
similarity index 94%
rename from src/main/java/com/youlai/boot/framework/captcha/config/CaptchaConfig.java
rename to src/main/java/com/youlai/boot/config/CaptchaConfig.java
index 2a890e69..d5a68e1f 100644
--- a/src/main/java/com/youlai/boot/framework/captcha/config/CaptchaConfig.java
+++ b/src/main/java/com/youlai/boot/config/CaptchaConfig.java
@@ -1,8 +1,9 @@
-package com.youlai.boot.framework.captcha.config;
+package com.youlai.boot.config;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator;
+import com.youlai.boot.config.property.CaptchaProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/src/main/java/com/youlai/boot/framework/web/config/CorsConfig.java b/src/main/java/com/youlai/boot/config/CorsConfig.java
similarity index 97%
rename from src/main/java/com/youlai/boot/framework/web/config/CorsConfig.java
rename to src/main/java/com/youlai/boot/config/CorsConfig.java
index 38eb6d03..ee3fc54e 100644
--- a/src/main/java/com/youlai/boot/framework/web/config/CorsConfig.java
+++ b/src/main/java/com/youlai/boot/config/CorsConfig.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.web.config;
+package com.youlai.boot.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
diff --git a/src/main/java/com/youlai/boot/framework/web/config/JacksonConfig.java b/src/main/java/com/youlai/boot/config/JacksonConfig.java
similarity index 97%
rename from src/main/java/com/youlai/boot/framework/web/config/JacksonConfig.java
rename to src/main/java/com/youlai/boot/config/JacksonConfig.java
index 7ca0f637..f9ed47af 100644
--- a/src/main/java/com/youlai/boot/framework/web/config/JacksonConfig.java
+++ b/src/main/java/com/youlai/boot/config/JacksonConfig.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.web.config;
+package com.youlai.boot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/src/main/java/com/youlai/boot/framework/openapi/Knife4jOpenApiCustomizer.java b/src/main/java/com/youlai/boot/config/Knife4jOpenApiCustomizer.java
similarity index 99%
rename from src/main/java/com/youlai/boot/framework/openapi/Knife4jOpenApiCustomizer.java
rename to src/main/java/com/youlai/boot/config/Knife4jOpenApiCustomizer.java
index 6ab80e58..b41e151d 100644
--- a/src/main/java/com/youlai/boot/framework/openapi/Knife4jOpenApiCustomizer.java
+++ b/src/main/java/com/youlai/boot/config/Knife4jOpenApiCustomizer.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.apidoc;
+package com.youlai.boot.config;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
diff --git a/src/main/java/com/youlai/boot/framework/integration/mail/config/MailConfig.java b/src/main/java/com/youlai/boot/config/MailConfig.java
similarity index 93%
rename from src/main/java/com/youlai/boot/framework/integration/mail/config/MailConfig.java
rename to src/main/java/com/youlai/boot/config/MailConfig.java
index cebcd62d..6f980b70 100644
--- a/src/main/java/com/youlai/boot/framework/integration/mail/config/MailConfig.java
+++ b/src/main/java/com/youlai/boot/config/MailConfig.java
@@ -1,5 +1,6 @@
-package com.youlai.boot.framework.integration.mail.config;
+package com.youlai.boot.config;
+import com.youlai.boot.config.property.MailProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/src/main/java/com/youlai/boot/framework/mybatis/config/MybatisConfig.java b/src/main/java/com/youlai/boot/config/MybatisConfig.java
similarity index 96%
rename from src/main/java/com/youlai/boot/framework/mybatis/config/MybatisConfig.java
rename to src/main/java/com/youlai/boot/config/MybatisConfig.java
index b06bb4cb..c337dbb2 100644
--- a/src/main/java/com/youlai/boot/framework/mybatis/config/MybatisConfig.java
+++ b/src/main/java/com/youlai/boot/config/MybatisConfig.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.mybatis.config;
+package com.youlai.boot.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
diff --git a/src/main/java/com/youlai/boot/framework/openapi/OpenApiConfig.java b/src/main/java/com/youlai/boot/config/OpenApiConfig.java
similarity index 97%
rename from src/main/java/com/youlai/boot/framework/openapi/OpenApiConfig.java
rename to src/main/java/com/youlai/boot/config/OpenApiConfig.java
index 066c69ec..5e6c2d54 100644
--- a/src/main/java/com/youlai/boot/framework/openapi/OpenApiConfig.java
+++ b/src/main/java/com/youlai/boot/config/OpenApiConfig.java
@@ -1,7 +1,7 @@
-package com.youlai.boot.framework.apidoc;
+package com.youlai.boot.config;
import cn.hutool.core.util.ArrayUtil;
-import com.youlai.boot.framework.security.config.SecurityProperties;
+import com.youlai.boot.config.property.SecurityProperties;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
diff --git a/src/main/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfig.java b/src/main/java/com/youlai/boot/config/OperationLogExecutorConfig.java
similarity index 95%
rename from src/main/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfig.java
rename to src/main/java/com/youlai/boot/config/OperationLogExecutorConfig.java
index c50edf6d..50988f3c 100644
--- a/src/main/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfig.java
+++ b/src/main/java/com/youlai/boot/config/OperationLogExecutorConfig.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.web.config;
+package com.youlai.boot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/src/main/java/com/youlai/boot/framework/security/config/PasswordEncoderConfig.java b/src/main/java/com/youlai/boot/config/PasswordEncoderConfig.java
similarity index 91%
rename from src/main/java/com/youlai/boot/framework/security/config/PasswordEncoderConfig.java
rename to src/main/java/com/youlai/boot/config/PasswordEncoderConfig.java
index 6ee0f46d..3e6e0e0b 100644
--- a/src/main/java/com/youlai/boot/framework/security/config/PasswordEncoderConfig.java
+++ b/src/main/java/com/youlai/boot/config/PasswordEncoderConfig.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.security.config;
+package com.youlai.boot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/src/main/java/com/youlai/boot/framework/cache/RedisCacheConfig.java b/src/main/java/com/youlai/boot/config/RedisCacheConfig.java
similarity index 98%
rename from src/main/java/com/youlai/boot/framework/cache/RedisCacheConfig.java
rename to src/main/java/com/youlai/boot/config/RedisCacheConfig.java
index 06b30707..9be08f3c 100644
--- a/src/main/java/com/youlai/boot/framework/cache/RedisCacheConfig.java
+++ b/src/main/java/com/youlai/boot/config/RedisCacheConfig.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.cache;
+package com.youlai.boot.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.cache.autoconfigure.CacheProperties;
diff --git a/src/main/java/com/youlai/boot/framework/cache/RedisConfig.java b/src/main/java/com/youlai/boot/config/RedisConfig.java
similarity index 97%
rename from src/main/java/com/youlai/boot/framework/cache/RedisConfig.java
rename to src/main/java/com/youlai/boot/config/RedisConfig.java
index 8014f69f..c821d5ef 100644
--- a/src/main/java/com/youlai/boot/framework/cache/RedisConfig.java
+++ b/src/main/java/com/youlai/boot/config/RedisConfig.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.cache;
+package com.youlai.boot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
diff --git a/src/main/java/com/youlai/boot/auth/security/config/SecurityConfig.java b/src/main/java/com/youlai/boot/config/SecurityConfig.java
similarity index 93%
rename from src/main/java/com/youlai/boot/auth/security/config/SecurityConfig.java
rename to src/main/java/com/youlai/boot/config/SecurityConfig.java
index 597afd12..fd2037d2 100644
--- a/src/main/java/com/youlai/boot/auth/security/config/SecurityConfig.java
+++ b/src/main/java/com/youlai/boot/config/SecurityConfig.java
@@ -1,9 +1,9 @@
-package com.youlai.boot.auth.security.config;
+package com.youlai.boot.config;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.hutool.core.util.ArrayUtil;
-import com.youlai.boot.framework.captcha.service.CaptchaService;
-import com.youlai.boot.framework.security.config.SecurityProperties;
+import com.youlai.boot.auth.service.CaptchaService;
+import com.youlai.boot.config.property.SecurityProperties;
import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter;
import com.youlai.boot.framework.security.port.UserAuthenticationPort;
import com.youlai.boot.framework.security.service.SecurityUserDetailsService;
@@ -33,6 +33,8 @@ import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
+import jakarta.servlet.DispatcherType;
+
/**
* Spring Security 配置类。
*
@@ -59,6 +61,8 @@ public class SecurityConfig {
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(requestMatcherRegistry -> {
+ // ASYNC 分发(SSE 超时/完成后的异步 dispatch)无 Authorization 头,直接放行
+ requestMatcherRegistry.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll();
String[] ignoreUrls = securityProperties.getIgnoreUrls();
if (ArrayUtil.isNotEmpty(ignoreUrls)) {
requestMatcherRegistry.requestMatchers(ignoreUrls).permitAll();
diff --git a/src/main/java/com/youlai/boot/framework/integration/wxma/config/WxMaConfig.java b/src/main/java/com/youlai/boot/config/WxMaConfig.java
similarity index 91%
rename from src/main/java/com/youlai/boot/framework/integration/wxma/config/WxMaConfig.java
rename to src/main/java/com/youlai/boot/config/WxMaConfig.java
index 93df531b..1093044b 100644
--- a/src/main/java/com/youlai/boot/framework/integration/wxma/config/WxMaConfig.java
+++ b/src/main/java/com/youlai/boot/config/WxMaConfig.java
@@ -1,8 +1,9 @@
-package com.youlai.boot.framework.integration.wxma.config;
+package com.youlai.boot.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;
diff --git a/src/main/java/com/youlai/boot/framework/job/XxlJobConfig.java b/src/main/java/com/youlai/boot/config/XxlJobConfig.java
similarity index 97%
rename from src/main/java/com/youlai/boot/framework/job/XxlJobConfig.java
rename to src/main/java/com/youlai/boot/config/XxlJobConfig.java
index 9642ec2b..0852a6cc 100644
--- a/src/main/java/com/youlai/boot/framework/job/XxlJobConfig.java
+++ b/src/main/java/com/youlai/boot/config/XxlJobConfig.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.job;
+package com.youlai.boot.config;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import lombok.extern.slf4j.Slf4j;
diff --git a/src/main/java/com/youlai/boot/framework/integration/sms/config/AliyunSmsProperties.java b/src/main/java/com/youlai/boot/config/property/AliyunSmsProperties.java
similarity index 93%
rename from src/main/java/com/youlai/boot/framework/integration/sms/config/AliyunSmsProperties.java
rename to src/main/java/com/youlai/boot/config/property/AliyunSmsProperties.java
index ad2b3426..317e366b 100644
--- a/src/main/java/com/youlai/boot/framework/integration/sms/config/AliyunSmsProperties.java
+++ b/src/main/java/com/youlai/boot/config/property/AliyunSmsProperties.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.integration.sms.config;
+package com.youlai.boot.config.property;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
diff --git a/src/main/java/com/youlai/boot/framework/captcha/config/CaptchaProperties.java b/src/main/java/com/youlai/boot/config/property/CaptchaProperties.java
similarity index 97%
rename from src/main/java/com/youlai/boot/framework/captcha/config/CaptchaProperties.java
rename to src/main/java/com/youlai/boot/config/property/CaptchaProperties.java
index 1ea87bf2..4067d51e 100644
--- a/src/main/java/com/youlai/boot/framework/captcha/config/CaptchaProperties.java
+++ b/src/main/java/com/youlai/boot/config/property/CaptchaProperties.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.captcha.config;
+package com.youlai.boot.config.property;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
diff --git a/src/main/java/com/youlai/boot/file/config/FileStorageProperties.java b/src/main/java/com/youlai/boot/config/property/FileStorageProperties.java
similarity index 97%
rename from src/main/java/com/youlai/boot/file/config/FileStorageProperties.java
rename to src/main/java/com/youlai/boot/config/property/FileStorageProperties.java
index e00f199e..b732beba 100644
--- a/src/main/java/com/youlai/boot/file/config/FileStorageProperties.java
+++ b/src/main/java/com/youlai/boot/config/property/FileStorageProperties.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.file.config;
+package com.youlai.boot.config.property;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
diff --git a/src/main/java/com/youlai/boot/framework/integration/mail/config/MailProperties.java b/src/main/java/com/youlai/boot/config/property/MailProperties.java
similarity index 94%
rename from src/main/java/com/youlai/boot/framework/integration/mail/config/MailProperties.java
rename to src/main/java/com/youlai/boot/config/property/MailProperties.java
index 27e1ef64..4e7cad90 100644
--- a/src/main/java/com/youlai/boot/framework/integration/mail/config/MailProperties.java
+++ b/src/main/java/com/youlai/boot/config/property/MailProperties.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.integration.mail.config;
+package com.youlai.boot.config.property;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
diff --git a/src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java b/src/main/java/com/youlai/boot/config/property/RateLimitProperties.java
similarity index 96%
rename from src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java
rename to src/main/java/com/youlai/boot/config/property/RateLimitProperties.java
index 72d8c5f4..72d03b26 100644
--- a/src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java
+++ b/src/main/java/com/youlai/boot/config/property/RateLimitProperties.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.web.config;
+package com.youlai.boot.config.property;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
diff --git a/src/main/java/com/youlai/boot/framework/security/config/SecurityProperties.java b/src/main/java/com/youlai/boot/config/property/SecurityProperties.java
similarity index 98%
rename from src/main/java/com/youlai/boot/framework/security/config/SecurityProperties.java
rename to src/main/java/com/youlai/boot/config/property/SecurityProperties.java
index ce5d4604..5e4b3725 100644
--- a/src/main/java/com/youlai/boot/framework/security/config/SecurityProperties.java
+++ b/src/main/java/com/youlai/boot/config/property/SecurityProperties.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.security.config;
+package com.youlai.boot.config.property;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
diff --git a/src/main/java/com/youlai/boot/framework/integration/wxma/config/WxMaProperties.java b/src/main/java/com/youlai/boot/config/property/WxMaProperties.java
similarity index 82%
rename from src/main/java/com/youlai/boot/framework/integration/wxma/config/WxMaProperties.java
rename to src/main/java/com/youlai/boot/config/property/WxMaProperties.java
index 3c7626a1..d4333c17 100644
--- a/src/main/java/com/youlai/boot/framework/integration/wxma/config/WxMaProperties.java
+++ b/src/main/java/com/youlai/boot/config/property/WxMaProperties.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.framework.integration.wxma.config;
+package com.youlai.boot.config.property;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
diff --git a/src/main/java/com/youlai/boot/framework/integration/mail/service/MailService.java b/src/main/java/com/youlai/boot/framework/integration/mail/service/MailService.java
deleted file mode 100644
index dbca6442..00000000
--- a/src/main/java/com/youlai/boot/framework/integration/mail/service/MailService.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package com.youlai.boot.framework.integration.mail.service;
-
-import com.youlai.boot.framework.integration.mail.config.MailProperties;
-import jakarta.mail.MessagingException;
-import jakarta.mail.internet.MimeMessage;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.core.io.FileSystemResource;
-import org.springframework.mail.SimpleMailMessage;
-import org.springframework.mail.javamail.JavaMailSender;
-import org.springframework.mail.javamail.MimeMessageHelper;
-import org.springframework.stereotype.Service;
-
-import java.io.File;
-
-/**
- * 邮件服务
- *
- * @author Ray.Hao
- * @since 2024/8/17
- */
-@Service
-@RequiredArgsConstructor
-@Slf4j
-public class MailService {
-
- private final JavaMailSender mailSender;
-
- private final MailProperties mailProperties;
-
- /**
- * 发送简单文本邮件
- *
- * @param to 收件人地址
- * @param subject 邮件主题
- * @param text 邮件内容
- */
- public void sendMail(String to, String subject, String text) {
- try {
- SimpleMailMessage message = new SimpleMailMessage();
- message.setFrom(mailProperties.getFrom());
- message.setTo(to);
- message.setSubject(subject);
- message.setText(text);
- mailSender.send(message);
- } catch (Exception e) {
- log.error("发送邮件失败{}", e.getMessage());
- }
- }
-
- /**
- * 发送带附件的邮件
- *
- * @param to 收件人地址
- * @param subject 邮件主题
- * @param text 邮件内容
- * @param filePath 附件路径
- */
- public void sendMailWithAttachment(String to, String subject, String text, String filePath) {
- MimeMessage message = mailSender.createMimeMessage();
- try {
- MimeMessageHelper helper = new MimeMessageHelper(message, true);
- helper.setFrom(mailProperties.getFrom());
- helper.setTo(to);
- helper.setSubject(subject);
- helper.setText(text, true); // true 表示支持HTML内容
-
- FileSystemResource file = new FileSystemResource(new File(filePath));
- helper.addAttachment(file.getFilename(), file);
-
- mailSender.send(message);
- } catch (MessagingException e) {
- log.error("发送带附件的邮件失败{}", e.getMessage());
- }
- }
-}
diff --git a/src/main/java/com/youlai/boot/framework/integration/sms/service/SmsService.java b/src/main/java/com/youlai/boot/framework/integration/sms/service/SmsService.java
deleted file mode 100644
index e56ecc0b..00000000
--- a/src/main/java/com/youlai/boot/framework/integration/sms/service/SmsService.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.youlai.boot.framework.integration.sms.service;
-
-import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
-
-import java.util.Map;
-
-/**
- * 短信服务接口层
- *
- * @author Ray.Hao
- * @since 2024/8/17
- */
-public interface SmsService {
-
- /**
- * 发送短信
- *
- * @param mobile 手机号 13388886666
- * @param smsType 短信模板 SMS_194640010,模板内容:您的验证码为:${code},请在5分钟内使用
- * @param templateParams 模板参数 [{"code":"123456"}] ,用于替换短信模板中的变量
- * @return boolean 是否发送成功
- */
- boolean sendSms(String mobile, SmsTypeEnum smsType, Map 定时统计并广播当前在线用户数量
+ * 当 {@code file-storage.type=aliyun} 时生效。
+ * 上传文件按日期分目录存储,URL 格式:{@code https://bucket.endpoint/yyyyMMdd/uuid.suffix}。
+ */
@Component
@ConditionalOnProperty(value = "file-storage.type", havingValue = "aliyun")
@ConfigurationProperties(prefix = "file-storage.aliyun")
@@ -29,9 +35,13 @@ import java.time.LocalDateTime;
@Data
public class AliyunFileServiceImpl implements FileService {
+ /** OSS 地域节点,如 {@code oss-cn-hangzhou.aliyuncs.com} */
private String endpoint;
+ /** AccessKey ID */
private String accessKeyId;
+ /** AccessKey Secret */
private String accessKeySecret;
+ /** 存储空间名称 */
private String bucket;
private OSS aliyunOssClient;
@@ -41,6 +51,9 @@ public class AliyunFileServiceImpl implements FileService {
aliyunOssClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
}
+ /**
+ * 上传文件到阿里云 OSS,按日期分目录,返回 {@code https://bucket.endpoint/path} 格式的 URL。
+ */
@Override
@SneakyThrows
public FileInfo uploadFile(MultipartFile file) {
@@ -63,6 +76,12 @@ public class AliyunFileServiceImpl implements FileService {
return fileInfo;
}
+ /**
+ * 从完整 URL 中提取文件名后调用 OSS 删除。
+ *
+ * @param filePath 文件完整 URL
+ * @return 删除成功返回 true
+ */
@Override
public boolean deleteFile(String filePath) {
Assert.notBlank(filePath, "删除文件路径不能为空");
diff --git a/src/main/java/com/youlai/boot/file/service/impl/LocalFileServiceImpl.java b/src/main/java/com/youlai/boot/support/file/service/impl/LocalFileServiceImpl.java
similarity index 71%
rename from src/main/java/com/youlai/boot/file/service/impl/LocalFileServiceImpl.java
rename to src/main/java/com/youlai/boot/support/file/service/impl/LocalFileServiceImpl.java
index 4905625d..657bb6f3 100644
--- a/src/main/java/com/youlai/boot/file/service/impl/LocalFileServiceImpl.java
+++ b/src/main/java/com/youlai/boot/support/file/service/impl/LocalFileServiceImpl.java
@@ -1,11 +1,11 @@
-package com.youlai.boot.file.service.impl;
+package com.youlai.boot.support.file.service.impl;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
-import com.youlai.boot.file.model.FileInfo;
-import com.youlai.boot.file.service.FileService;
+import com.youlai.boot.support.file.model.FileInfo;
+import com.youlai.boot.support.file.service.FileService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -19,6 +19,13 @@ import java.io.File;
import java.io.InputStream;
import java.time.LocalDateTime;
+/**
+ * 本地文件存储服务。
+ *
+ * 当 {@code file-storage.type=local} 时生效。
+ * 文件写入本地磁盘 {@code file-storage.local.path} 下按日期分目录存储。
+ * URL 为相对于存储根目录的路径,如 {@code /yyyyMMdd/uuid.suffix}。
+ */
@Data
@Slf4j
@Component
@@ -27,14 +34,18 @@ import java.time.LocalDateTime;
@RequiredArgsConstructor
public class LocalFileServiceImpl implements FileService {
+ /** 本地存储根目录,来自 {@code file-storage.local.path} */
@Value("${file-storage.local.path}")
private String path;
+ /**
+ * 上传文件到本地磁盘,按日期分目录,返回相对于存储根目录的路径。
+ */
@Override
public FileInfo uploadFile(MultipartFile file) {
String originalFilename = file.getOriginalFilename();
String suffix = FileUtil.getSuffix(originalFilename);
- String fileName = IdUtil.simpleUUID()+ "." + suffix;;
+ String fileName = IdUtil.simpleUUID() + "." + suffix;
String folder = DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
String filePrefix = path.endsWith(File.separator) ? path : path + File.separator;
try (InputStream inputStream = file.getInputStream()) {
@@ -50,6 +61,12 @@ public class LocalFileServiceImpl implements FileService {
return fileInfo;
}
+ /**
+ * 删除本地文件。路径是目录或不存在时返回 false。
+ *
+ * @param filePath 相对于存储根目录的文件路径
+ * @return 删除成功返回 true
+ */
@Override
public boolean deleteFile(String filePath) {
if (filePath == null || filePath.isEmpty()) {
diff --git a/src/main/java/com/youlai/boot/file/service/impl/MinioFileServiceImpl.java b/src/main/java/com/youlai/boot/support/file/service/impl/MinioFileServiceImpl.java
similarity index 76%
rename from src/main/java/com/youlai/boot/file/service/impl/MinioFileServiceImpl.java
rename to src/main/java/com/youlai/boot/support/file/service/impl/MinioFileServiceImpl.java
index b69a2acb..072a1d81 100644
--- a/src/main/java/com/youlai/boot/file/service/impl/MinioFileServiceImpl.java
+++ b/src/main/java/com/youlai/boot/support/file/service/impl/MinioFileServiceImpl.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.file.service.impl;
+package com.youlai.boot.support.file.service.impl;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
@@ -7,8 +7,8 @@ import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.result.ResultCode;
-import com.youlai.boot.file.model.FileInfo;
-import com.youlai.boot.file.service.FileService;
+import com.youlai.boot.support.file.model.FileInfo;
+import com.youlai.boot.support.file.service.FileService;
import io.minio.*;
import io.minio.http.Method;
import jakarta.annotation.PostConstruct;
@@ -24,6 +24,16 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.time.LocalDateTime;
+/**
+ * MinIO 文件存储服务。
+ *
+ * 当 {@code file-storage.type=minio} 时生效。
+ * 上传文件按日期分目录存储,路径格式:{@code yyyyMMdd/uuid.suffix}。
+ *
+ * 从完整 URL 中提取文件名后调用 MinIO 删除。
+ *
+ * @param filePath 文件完整 URL
+ * @return 删除成功返回 true
+ */
@Override
public boolean deleteFile(String filePath) {
Assert.notBlank(filePath, "删除文件路径不能为空");
@@ -107,17 +133,21 @@ public class MinioFileServiceImpl implements FileService {
}
}
+ /** 生成公开读的存储桶策略 JSON */
private static String publicBucketPolicy(String bucket) {
return "{\"Version\":\"2012-10-17\","
+ "\"Statement\":[{\"Effect\":\"Allow\","
+ "\"Principal\":{\"AWS\":[\"*\"]},"
+ "\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"],"
+ "\"Resource\":[\"arn:aws:s3:::" + bucket + "\"]},"
- + "{\"Effect\":\"Allow\"," + "\"Principal\":{\"AWS\":[\"*\"]},"
+ + "{\"Effect\":\"Allow\",\"" + "\"Principal\":{\"AWS\":[\"*\"]},"
+ "\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"],"
+ "\"Resource\":[\"arn:aws:s3:::" + bucket + "/*\"]}]}";
}
+ /**
+ * 存储桶不存在时自动创建,并设置为公开读策略。
+ */
@SneakyThrows
private void createBucketIfAbsent(String bucket) {
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build())) {
diff --git a/src/main/java/com/youlai/boot/support/mail/EmailService.java b/src/main/java/com/youlai/boot/support/mail/EmailService.java
new file mode 100644
index 00000000..1a60dcae
--- /dev/null
+++ b/src/main/java/com/youlai/boot/support/mail/EmailService.java
@@ -0,0 +1,32 @@
+package com.youlai.boot.support.mail;
+
+/**
+ * 邮件发送服务接口。
+ *
+ * 由各邮件供应商实现(SMTP、SendGrid 等),通过 {@code mail.type} 配置项选择。
+ *
+ * @author Ray.Hao
+ * @since 3.1.0
+ */
+public interface EmailService {
+
+ /**
+ * 发送纯文本邮件。
+ *
+ * @param to 收件人邮箱
+ * @param subject 邮件主题
+ * @param text 邮件正文
+ */
+ void send(String to, String subject, String text);
+
+ /**
+ * 发送带附件的邮件。
+ *
+ * @param to 收件人邮箱
+ * @param subject 邮件主题
+ * @param text 邮件正文(支持 HTML)
+ * @param filePath 附件文件绝对路径
+ */
+ void sendWithAttachment(String to, String subject, String text, String filePath);
+
+}
diff --git a/src/main/java/com/youlai/boot/support/mail/impl/SmtpEmailServiceImpl.java b/src/main/java/com/youlai/boot/support/mail/impl/SmtpEmailServiceImpl.java
new file mode 100644
index 00000000..b76a2450
--- /dev/null
+++ b/src/main/java/com/youlai/boot/support/mail/impl/SmtpEmailServiceImpl.java
@@ -0,0 +1,74 @@
+package com.youlai.boot.support.mail.impl;
+
+import com.youlai.boot.config.property.MailProperties;
+import com.youlai.boot.support.mail.EmailService;
+import jakarta.mail.MessagingException;
+import jakarta.mail.internet.MimeMessage;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+
+/**
+ * SMTP 邮件服务。
+ *
+ * 当 {@code mail.type=smtp}(默认)时生效。基于 Spring {@link JavaMailSender} 发送邮件。
+ *
+ * @author Ray.Hao
+ * @since 3.1.0
+ */
+@Slf4j
+@Service
+@ConditionalOnProperty(value = "mail.type", havingValue = "smtp", matchIfMissing = true)
+@RequiredArgsConstructor
+public class SmtpEmailServiceImpl implements EmailService {
+
+ private final JavaMailSender mailSender;
+ /** 发件人地址等邮件配置,来自 {@code mail.*} */
+ private final MailProperties mailProperties;
+
+ /**
+ * 发送纯文本邮件,发件人取自 {@code mail.from} 配置。
+ */
+ @Override
+ public void send(String to, String subject, String text) {
+ try {
+ SimpleMailMessage msg = new SimpleMailMessage();
+ msg.setFrom(mailProperties.getFrom());
+ msg.setTo(to);
+ msg.setSubject(subject);
+ msg.setText(text);
+ mailSender.send(msg);
+ } catch (Exception e) {
+ log.error("发送邮件失败: {}", e.getMessage());
+ }
+ }
+
+ /**
+ * 发送带附件的 HTML 邮件。
+ */
+ @Override
+ public void sendWithAttachment(String to, String subject, String text, String filePath) {
+ MimeMessage message = mailSender.createMimeMessage();
+ try {
+ MimeMessageHelper helper = new MimeMessageHelper(message, true);
+ helper.setFrom(mailProperties.getFrom());
+ helper.setTo(to);
+ helper.setSubject(subject);
+ helper.setText(text, true);
+
+ FileSystemResource file = new FileSystemResource(new File(filePath));
+ helper.addAttachment(file.getFilename(), file);
+
+ mailSender.send(message);
+ } catch (MessagingException e) {
+ log.error("发送带附件邮件失败: {}", e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/com/youlai/boot/support/sms/SmsService.java b/src/main/java/com/youlai/boot/support/sms/SmsService.java
new file mode 100644
index 00000000..1f229f03
--- /dev/null
+++ b/src/main/java/com/youlai/boot/support/sms/SmsService.java
@@ -0,0 +1,25 @@
+package com.youlai.boot.support.sms;
+
+import java.util.Map;
+
+/**
+ * 短信发送服务接口。
+ *
+ * 由各短信供应商实现(阿里云、腾讯云等),通过 {@code sms.type} 配置项选择。
+ *
+ * @author Ray.Hao
+ * @since 2024/8/17
+ */
+public interface SmsService {
+
+ /**
+ * 发送短信。
+ *
+ * @param mobile 手机号
+ * @param smsType 短信类型,对应 {@code sms.templates.*} 配置中的模板
+ * @param templateParams 模板参数,用于替换短信模板中的变量
+ * @return 是否发送成功
+ */
+ boolean send(String mobile, SmsTypeEnum smsType, Map
+ * 组合 {@link SseSessionManager} 提供 SSE 建连、广播、定向推送、在线用户统计等能力。
+ *
+ * @author Ray.Hao
+ * @since 3.1.0
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class SseService {
+
+ private final SseSessionManager sessionManager;
+
+ /**
+ * 创建 SSE 连接
+ *
+ * @param username 用户名
+ * @return SseEmitter
+ */
+ public SseEmitter createConnection(String username) {
+ SseEmitter emitter = new SseEmitter(SseSessionManager.TIMEOUT);
+ if (username == null || username.isEmpty()) {
+ log.warn("创建SSE连接失败:用户名为空");
+ emitter.completeWithError(new IllegalArgumentException("用户名为空"));
+ return emitter;
+ }
+
+ sessionManager.userConnected(username, emitter);
+
+ // 连接建立后立即发送在线用户数
+ try {
+ emitter.send(SseEmitter.event()
+ .name(SseEventConstants.ONLINE_USERS)
+ .data(sessionManager.getOnlineUserCount()));
+ } catch (IOException e) {
+ log.warn("发送初始在线用户数失败: {}", e.getMessage());
+ }
+
+ log.info("用户[{}]SSE连接已建立,当前在线用户数: {}", username, sessionManager.getOnlineUserCount());
+ sendOnlineCount();
+
+ return emitter;
+ }
+
+ /**
+ * 广播事件到所有在线连接
+ *
+ * @param topic 事件主题
+ * @param data 数据
+ */
+ public void broadcast(String topic, Object data) {
+ sessionManager.broadcast(topic, data);
+ }
+
+ /**
+ * 向指定用户发送事件
+ *
+ * @param username 用户名
+ * @param topic 事件主题
+ * @param data 数据
+ */
+ public void sendToUser(String username, String topic, Object data) {
+ sessionManager.sendToUser(username, topic, data);
+ }
+
+ /**
+ * 广播在线用户数
+ */
+ public void sendOnlineCount() {
+ int count = sessionManager.getOnlineUserCount();
+ sessionManager.broadcast(SseEventConstants.ONLINE_USERS, count);
+ log.debug("已发送在线用户数: {}", count);
+ }
+
+ /**
+ * 发送系统消息(全员广播)
+ *
+ * @param message 消息内容
+ */
+ public void sendSystemMessage(String message) {
+ if (message == null || message.isEmpty()) {
+ return;
+ }
+ Map
- * 维护 SSE 连接的用户会话信息,支持多设备同时登录
+ * 维护 SSE 连接的用户会话信息,支持多设备同时登录。
+ * 职责:建连 / 断开 / 心跳检测 / 定向推送 / 全员广播
+ *
+ * @author Ray.Hao
+ * @since 3.1.0
*/
@Slf4j
@Component
-public class SseSessionRegistry {
+public class SseSessionManager {
+
+ /** SSE 连接超时时间:30 分钟 */
+ public static final long TIMEOUT = 30 * 60 * 1000L;
/** 用户名 -> SseEmitter 集合(支持多设备) */
private final Map
- * 用于返回在线用户的基本信息,包括用户名、会话数量和登录时间。
+ * 用于 SSE 在线用户列表展示,包含用户名、会话数量和最早登录时间。
*
* @author Ray.Hao
* @since 3.0.0
@@ -26,9 +26,9 @@ public class OnlineUserDTO implements Serializable {
/** 用户名 */
private String username;
- /** 会话数量(多设备登录时大于1) */
+ /** 会话数量,多设备登录时大于 1 */
private int sessionCount;
- /** 最早登录时间 */
+ /** 最早登录时间(毫秒时间戳) */
private long loginTime;
}
diff --git a/src/main/java/com/youlai/boot/system/controller/ConfigController.java b/src/main/java/com/youlai/boot/system/controller/ConfigController.java
index d4559240..73984b64 100644
--- a/src/main/java/com/youlai/boot/system/controller/ConfigController.java
+++ b/src/main/java/com/youlai/boot/system/controller/ConfigController.java
@@ -5,7 +5,7 @@ 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.framework.annotation.Log;
+import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.system.model.form.ConfigForm;
import com.youlai.boot.system.model.query.ConfigQuery;
import com.youlai.boot.system.model.vo.ConfigVO;
diff --git a/src/main/java/com/youlai/boot/system/controller/DeptController.java b/src/main/java/com/youlai/boot/system/controller/DeptController.java
index 2475b702..7a54a8fd 100644
--- a/src/main/java/com/youlai/boot/system/controller/DeptController.java
+++ b/src/main/java/com/youlai/boot/system/controller/DeptController.java
@@ -2,13 +2,13 @@ package com.youlai.boot.system.controller;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
-import com.youlai.boot.framework.annotation.RepeatSubmit;
+import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.system.model.form.DeptForm;
import com.youlai.boot.system.model.query.DeptQuery;
import com.youlai.boot.system.model.vo.DeptVO;
-import com.youlai.boot.framework.annotation.Log;
+import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.system.service.DeptService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
diff --git a/src/main/java/com/youlai/boot/system/controller/DictController.java b/src/main/java/com/youlai/boot/system/controller/DictController.java
index c905411e..ef8e0487 100644
--- a/src/main/java/com/youlai/boot/system/controller/DictController.java
+++ b/src/main/java/com/youlai/boot/system/controller/DictController.java
@@ -12,12 +12,14 @@ import com.youlai.boot.system.model.query.DictQuery;
import com.youlai.boot.system.model.vo.DictItemOptionVO;
import com.youlai.boot.system.model.vo.DictItemPageVO;
import com.youlai.boot.system.model.vo.DictPageVO;
-import com.youlai.boot.framework.annotation.RepeatSubmit;
+import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.system.model.form.DictForm;
-import com.youlai.boot.framework.annotation.Log;
+import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.system.service.DictItemService;
import com.youlai.boot.system.service.DictService;
-import com.youlai.boot.message.service.SseService;
+import com.youlai.boot.support.sse.SseService;
+import com.youlai.boot.support.sse.constant.SseTopics;
+import com.youlai.boot.system.model.dto.DictChangeEvent;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -45,6 +47,15 @@ public class DictController {
private final DictItemService dictItemService;
private final SseService sseService;
+ /**
+ * 广播字典变更事件到所有 SSE 在线连接
+ *
+ * @param dictCode 字典编码
+ */
+ private void sendDictChange(String dictCode) {
+ sseService.broadcast(SseTopics.DICT, new DictChangeEvent(dictCode));
+ }
+
//---------------------------------------------------
// 字典相关接口
//---------------------------------------------------
@@ -84,7 +95,7 @@ public class DictController {
boolean result = dictService.saveDict(formData);
// 发送字典更新通知
if (result) {
- sseService.sendDictChange(formData.getDictCode());
+ sendDictChange(formData.getDictCode());
}
return Result.judge(result);
}
@@ -100,7 +111,7 @@ public class DictController {
boolean status = dictService.updateDict(dictId, dictForm);
// 发送字典更新通知
if (status && dictForm.getDictCode() != null) {
- sseService.sendDictChange(dictForm.getDictCode());
+ sendDictChange(dictForm.getDictCode());
}
return Result.judge(status);
}
@@ -119,7 +130,7 @@ public class DictController {
// 发送字典删除通知
for (String dictCode : dictCodes) {
- sseService.sendDictChange(dictCode);
+ sendDictChange(dictCode);
}
return Result.success();
@@ -163,7 +174,7 @@ public class DictController {
// 发送字典更新通知
if (result) {
- sseService.sendDictChange(dictCode);
+ sendDictChange(dictCode);
}
return Result.judge(result);
@@ -195,7 +206,7 @@ public class DictController {
// 发送字典更新通知
if (status) {
- sseService.sendDictChange(dictCode);
+ sendDictChange(dictCode);
}
return Result.judge(status);
@@ -212,7 +223,7 @@ public class DictController {
dictItemService.deleteDictItemByIds(itemIds);
// 发送字典更新通知
- sseService.sendDictChange(dictCode);
+ sendDictChange(dictCode);
return Result.success();
}
diff --git a/src/main/java/com/youlai/boot/file/controller/FileController.java b/src/main/java/com/youlai/boot/system/controller/FileController.java
similarity index 92%
rename from src/main/java/com/youlai/boot/file/controller/FileController.java
rename to src/main/java/com/youlai/boot/system/controller/FileController.java
index 154d981b..200eb44e 100644
--- a/src/main/java/com/youlai/boot/file/controller/FileController.java
+++ b/src/main/java/com/youlai/boot/system/controller/FileController.java
@@ -1,13 +1,13 @@
-package com.youlai.boot.file.controller;
+package com.youlai.boot.system.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.common.result.ResultCode;
-import com.youlai.boot.file.config.FileStorageProperties;
-import com.youlai.boot.file.model.FileInfo;
-import com.youlai.boot.file.service.FileService;
+import com.youlai.boot.config.property.FileStorageProperties;
+import com.youlai.boot.support.file.model.FileInfo;
+import com.youlai.boot.support.file.service.FileService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
diff --git a/src/main/java/com/youlai/boot/system/controller/LogController.java b/src/main/java/com/youlai/boot/system/controller/LogController.java
index 34d70307..c717b80e 100644
--- a/src/main/java/com/youlai/boot/system/controller/LogController.java
+++ b/src/main/java/com/youlai/boot/system/controller/LogController.java
@@ -1,7 +1,7 @@
package com.youlai.boot.system.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.youlai.boot.framework.annotation.Log;
+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.PageResult;
diff --git a/src/main/java/com/youlai/boot/system/controller/MenuController.java b/src/main/java/com/youlai/boot/system/controller/MenuController.java
index 5000344f..f4a614cc 100644
--- a/src/main/java/com/youlai/boot/system/controller/MenuController.java
+++ b/src/main/java/com/youlai/boot/system/controller/MenuController.java
@@ -1,7 +1,7 @@
package com.youlai.boot.system.controller;
-import com.youlai.boot.framework.annotation.Log;
-import com.youlai.boot.framework.annotation.RepeatSubmit;
+import com.youlai.boot.common.annotation.Log;
+import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.model.Option;
diff --git a/src/main/java/com/youlai/boot/system/controller/NoticeController.java b/src/main/java/com/youlai/boot/system/controller/NoticeController.java
index 95e38a98..639f794c 100644
--- a/src/main/java/com/youlai/boot/system/controller/NoticeController.java
+++ b/src/main/java/com/youlai/boot/system/controller/NoticeController.java
@@ -1,7 +1,7 @@
package com.youlai.boot.system.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.youlai.boot.framework.annotation.Log;
+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.PageResult;
diff --git a/src/main/java/com/youlai/boot/system/controller/RoleController.java b/src/main/java/com/youlai/boot/system/controller/RoleController.java
index be215f53..b665896f 100644
--- a/src/main/java/com/youlai/boot/system/controller/RoleController.java
+++ b/src/main/java/com/youlai/boot/system/controller/RoleController.java
@@ -3,14 +3,14 @@ package com.youlai.boot.system.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
-import com.youlai.boot.framework.annotation.RepeatSubmit;
+import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.common.result.PageResult;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.system.model.form.RoleForm;
import com.youlai.boot.system.model.query.RoleQuery;
import com.youlai.boot.system.model.vo.RolePageVO;
-import com.youlai.boot.framework.annotation.Log;
+import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.system.service.RoleService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
diff --git a/src/main/java/com/youlai/boot/message/controller/SseController.java b/src/main/java/com/youlai/boot/system/controller/SseController.java
similarity index 71%
rename from src/main/java/com/youlai/boot/message/controller/SseController.java
rename to src/main/java/com/youlai/boot/system/controller/SseController.java
index 3135a842..9fbd1a3b 100644
--- a/src/main/java/com/youlai/boot/message/controller/SseController.java
+++ b/src/main/java/com/youlai/boot/system/controller/SseController.java
@@ -1,21 +1,27 @@
-package com.youlai.boot.message.controller;
+package com.youlai.boot.system.controller;
-import com.youlai.boot.common.result.Result;
+import com.youlai.boot.support.sse.SseService;
import com.youlai.boot.framework.security.model.SecurityUserDetails;
import com.youlai.boot.framework.security.util.SecurityUtils;
-import com.youlai.boot.message.service.SseService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
+import java.io.IOException;
+
/**
- * SSE 控制器
+ * SSE 连接控制器
+ *
+ * @author Ray.Hao
+ * @since 3.1.0
*/
@Tag(name = "14. SSE连接")
@Slf4j
@@ -28,18 +34,13 @@ public class SseController {
@Operation(summary = "建立SSE连接")
@GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
- public SseEmitter connect() {
+ public SseEmitter connect(HttpServletResponse response) throws IOException {
SecurityUserDetails user = SecurityUtils.getUser().orElse(null);
if (user == null) {
- log.warn("SSE连接失败:未获取到当前用户");
+ response.setStatus(HttpStatus.UNAUTHORIZED.value());
+ response.flushBuffer();
return null;
}
return sseService.createConnection(user.getUsername());
}
-
- @Operation(summary = "获取在线用户数")
- @GetMapping("/online-count")
- public Result 每 3 分钟统计并广播当前在线用户数量
* 当字典数据发生变更时,通过 SSE 广播此事件通知前端清除缓存。
- * 前端收到通知后清除对应字典的本地缓存,下次使用时重新从服务端加载。
*
* @author Ray.Hao
* @since 3.0.0
diff --git a/src/main/java/com/youlai/boot/system/model/entity/Config.java b/src/main/java/com/youlai/boot/system/model/entity/Config.java
index 586901a7..40a8075f 100644
--- a/src/main/java/com/youlai/boot/system/model/entity/Config.java
+++ b/src/main/java/com/youlai/boot/system/model/entity/Config.java
@@ -1,7 +1,7 @@
package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.*;
-import com.youlai.boot.framework.base.BaseEntity;
+import com.youlai.boot.common.base.BaseEntity;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
diff --git a/src/main/java/com/youlai/boot/system/model/entity/Dept.java b/src/main/java/com/youlai/boot/system/model/entity/Dept.java
index 44517148..cb31f54d 100644
--- a/src/main/java/com/youlai/boot/system/model/entity/Dept.java
+++ b/src/main/java/com/youlai/boot/system/model/entity/Dept.java
@@ -3,7 +3,7 @@ package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
-import com.youlai.boot.framework.base.BaseEntity;
+import com.youlai.boot.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
diff --git a/src/main/java/com/youlai/boot/system/model/entity/Dict.java b/src/main/java/com/youlai/boot/system/model/entity/Dict.java
index 42b47bce..1085e4f2 100644
--- a/src/main/java/com/youlai/boot/system/model/entity/Dict.java
+++ b/src/main/java/com/youlai/boot/system/model/entity/Dict.java
@@ -1,7 +1,7 @@
package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.*;
-import com.youlai.boot.framework.base.BaseEntity;
+import com.youlai.boot.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
diff --git a/src/main/java/com/youlai/boot/system/model/entity/DictItem.java b/src/main/java/com/youlai/boot/system/model/entity/DictItem.java
index fe066941..0fc80ee2 100644
--- a/src/main/java/com/youlai/boot/system/model/entity/DictItem.java
+++ b/src/main/java/com/youlai/boot/system/model/entity/DictItem.java
@@ -1,7 +1,7 @@
package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.*;
-import com.youlai.boot.framework.base.BaseEntity;
+import com.youlai.boot.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
diff --git a/src/main/java/com/youlai/boot/system/model/entity/Menu.java b/src/main/java/com/youlai/boot/system/model/entity/Menu.java
index 4f3fdac5..2eb8d833 100644
--- a/src/main/java/com/youlai/boot/system/model/entity/Menu.java
+++ b/src/main/java/com/youlai/boot/system/model/entity/Menu.java
@@ -2,7 +2,7 @@ package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
-import com.youlai.boot.framework.base.BaseEntity;
+import com.youlai.boot.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
diff --git a/src/main/java/com/youlai/boot/system/model/entity/Notice.java b/src/main/java/com/youlai/boot/system/model/entity/Notice.java
index 345bde11..e2a4317b 100644
--- a/src/main/java/com/youlai/boot/system/model/entity/Notice.java
+++ b/src/main/java/com/youlai/boot/system/model/entity/Notice.java
@@ -1,7 +1,7 @@
package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.*;
-import com.youlai.boot.framework.base.BaseEntity;
+import com.youlai.boot.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
diff --git a/src/main/java/com/youlai/boot/system/model/entity/Role.java b/src/main/java/com/youlai/boot/system/model/entity/Role.java
index 130c96f3..d81a3823 100644
--- a/src/main/java/com/youlai/boot/system/model/entity/Role.java
+++ b/src/main/java/com/youlai/boot/system/model/entity/Role.java
@@ -3,7 +3,7 @@ package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
-import com.youlai.boot.framework.base.BaseEntity;
+import com.youlai.boot.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
diff --git a/src/main/java/com/youlai/boot/system/model/entity/SysLog.java b/src/main/java/com/youlai/boot/system/model/entity/SysLog.java
index ae93c980..b7712d7f 100644
--- a/src/main/java/com/youlai/boot/system/model/entity/SysLog.java
+++ b/src/main/java/com/youlai/boot/system/model/entity/SysLog.java
@@ -41,7 +41,7 @@ public class SysLog implements Serializable {
private String title;
/**
- * 自定义日志内容
+ * 自定义日志内容,最长 500 字符
*/
private String content;
@@ -102,7 +102,7 @@ public class SysLog implements Serializable {
private Integer status;
/**
- * 错误信息
+ * 错误信息,切面内截断至 1000 字符
*/
@TableField(value = "error_msg")
private String errorMsg;
diff --git a/src/main/java/com/youlai/boot/system/model/entity/SysUser.java b/src/main/java/com/youlai/boot/system/model/entity/SysUser.java
index c1457e7d..728edbbc 100644
--- a/src/main/java/com/youlai/boot/system/model/entity/SysUser.java
+++ b/src/main/java/com/youlai/boot/system/model/entity/SysUser.java
@@ -1,7 +1,7 @@
package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.*;
-import com.youlai.boot.framework.base.BaseEntity;
+import com.youlai.boot.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
diff --git a/src/main/java/com/youlai/boot/system/model/entity/UserNotice.java b/src/main/java/com/youlai/boot/system/model/entity/UserNotice.java
index d6ebbd73..ea91e5f7 100644
--- a/src/main/java/com/youlai/boot/system/model/entity/UserNotice.java
+++ b/src/main/java/com/youlai/boot/system/model/entity/UserNotice.java
@@ -3,7 +3,7 @@ package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
-import com.youlai.boot.framework.base.BaseEntity;
+import com.youlai.boot.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
diff --git a/src/main/java/com/youlai/boot/system/model/entity/UserSocial.java b/src/main/java/com/youlai/boot/system/model/entity/UserSocial.java
index c88b2717..c5bbd7fc 100644
--- a/src/main/java/com/youlai/boot/system/model/entity/UserSocial.java
+++ b/src/main/java/com/youlai/boot/system/model/entity/UserSocial.java
@@ -1,7 +1,7 @@
package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.TableName;
-import com.youlai.boot.framework.base.BaseEntity;
+import com.youlai.boot.common.base.BaseEntity;
import com.youlai.boot.common.enums.SocialPlatformEnum;
import lombok.Getter;
import lombok.Setter;
diff --git a/src/main/java/com/youlai/boot/system/security/adapter/PermissionAdapter.java b/src/main/java/com/youlai/boot/system/security/port/PermissionAdapter.java
similarity index 89%
rename from src/main/java/com/youlai/boot/system/security/adapter/PermissionAdapter.java
rename to src/main/java/com/youlai/boot/system/security/port/PermissionAdapter.java
index 71cc6422..498f2f23 100644
--- a/src/main/java/com/youlai/boot/system/security/adapter/PermissionAdapter.java
+++ b/src/main/java/com/youlai/boot/system/security/port/PermissionAdapter.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.system.security.adapter;
+package com.youlai.boot.system.security.port;
import com.youlai.boot.framework.security.port.PermissionPort;
import com.youlai.boot.system.service.RoleMenuService;
@@ -12,6 +12,8 @@ import java.util.Set;
*
* 实现 framework 层的 {@link PermissionPort},委托 {@link RoleMenuService} 查询权限集合。
*
+ * @author Ray.Hao
+ * @since 3.1.0
* @see PermissionPort
*/
@Component
diff --git a/src/main/java/com/youlai/boot/system/security/adapter/UserAuthenticationAdapter.java b/src/main/java/com/youlai/boot/system/security/port/UserAuthenticationAdapter.java
similarity index 93%
rename from src/main/java/com/youlai/boot/system/security/adapter/UserAuthenticationAdapter.java
rename to src/main/java/com/youlai/boot/system/security/port/UserAuthenticationAdapter.java
index e12535e9..22555298 100644
--- a/src/main/java/com/youlai/boot/system/security/adapter/UserAuthenticationAdapter.java
+++ b/src/main/java/com/youlai/boot/system/security/port/UserAuthenticationAdapter.java
@@ -1,4 +1,4 @@
-package com.youlai.boot.system.security.adapter;
+package com.youlai.boot.system.security.port;
import com.youlai.boot.common.enums.SocialPlatformEnum;
import com.youlai.boot.framework.security.model.SecurityUser;
@@ -13,6 +13,8 @@ import org.springframework.stereotype.Component;
*
* 实现 framework 层的 {@link UserAuthenticationPort},委托 system 层服务完成查询。
*
+ * @author Ray.Hao
+ * @since 3.1.0
* @see UserAuthenticationPort
*/
@Component
diff --git a/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java
index d4e23147..28bcc7cf 100644
--- a/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java
+++ b/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java
@@ -8,8 +8,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.common.exception.BusinessException;
-import com.youlai.boot.message.dto.OnlineUserDTO;
import com.youlai.boot.framework.security.util.SecurityUtils;
+import com.youlai.boot.support.sse.model.OnlineUserDTO;
import com.youlai.boot.system.converter.NoticeConverter;
import com.youlai.boot.system.enums.NoticePublishStatusEnum;
import com.youlai.boot.system.enums.NoticeTargetEnum;
@@ -26,7 +26,7 @@ import com.youlai.boot.system.model.query.NoticeQuery;
import com.youlai.boot.system.service.NoticeService;
import com.youlai.boot.system.service.UserNoticeService;
import com.youlai.boot.system.service.UserService;
-import com.youlai.boot.message.service.SseService;
+import com.youlai.boot.support.sse.SseService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java
index 84b80775..8444c0c2 100644
--- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java
+++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java
@@ -12,9 +12,9 @@ import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SystemConstants;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.model.Option;
-import com.youlai.boot.framework.integration.mail.service.MailService;
-import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
-import com.youlai.boot.framework.integration.sms.service.SmsService;
+import com.youlai.boot.support.sms.SmsTypeEnum;
+import com.youlai.boot.support.mail.EmailService;
+import com.youlai.boot.support.sms.SmsService;
import com.youlai.boot.framework.security.model.RoleDataScope;
import com.youlai.boot.framework.security.model.SecurityUser;
import com.youlai.boot.framework.security.token.TokenManager;
@@ -66,8 +66,7 @@ public class UserServiceImpl extends ServiceImpl
- * 代码生成、MySQL表生成代码、自动代码生成
- *
- * @author Ray Hao
- * @see 代码生成器配置新
- * @since 2024/4/9
- */
-public class SystemCodeGenerator {
-
- private static final DataSourceConfig.Builder DATA_SOURCE_CONFIG = new DataSourceConfig
- .Builder("jdbc:mysql://localhost:3306/youlai_boot?serverTimezone=Asia/Shanghai", "root", "123456");
-
- /**
- * 执行 run
- */
- public static void main(String[] args) {
- FastAutoGenerator.create(DATA_SOURCE_CONFIG)
- // 全局配置
- .globalConfig((scanner, builder) -> {
- builder.outputDir(System.getProperty("user.dir") + "/src/main/java")
- .author("Ray Hao") // 设置作者
- ;
- })
- // 包配置
- .packageConfig(builder -> {
- builder
- .parent("com.youlai.boot.system")
- .entity("model.entity")
- .mapper("mapper")
- .service("support")
- .serviceImpl("support.impl")
- .controller("controller")
- .pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper"));
- }
- )
- // 注入配置(设置扩展类的模板路径和包路径)
- .injectionConfig(consumer -> {
- List
+ *
+ */
@Component
@ConditionalOnProperty(value = "file-storage.type", havingValue = "minio")
@ConfigurationProperties(prefix = "file-storage.minio")
@@ -32,10 +42,15 @@ import java.time.LocalDateTime;
@Slf4j
public class MinioFileServiceImpl implements FileService {
+ /** MinIO 服务端点,如 {@code http://localhost:9000} */
private String endpoint;
+ /** 访问密钥 */
private String accessKey;
+ /** 秘密密钥 */
private String secretKey;
+ /** 存储桶名称 */
private String bucket;
+ /** 自定义域名(可选),配置后不再使用预签名 URL */
private String domain;
private MinioClient minioClient;
@@ -48,6 +63,9 @@ public class MinioFileServiceImpl implements FileService {
.build();
}
+ /**
+ * 上传文件到 MinIO,按日期分目录,自动创建存储桶,返回文件访问 URL。
+ */
@Override
public FileInfo uploadFile(MultipartFile file) {
createBucketIfAbsent(bucket);
@@ -59,7 +77,7 @@ public class MinioFileServiceImpl implements FileService {
try (InputStream inputStream = file.getInputStream()) {
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket(bucket)
- .object(dateFolder + "/"+ fileName)
+ .object(dateFolder + "/" + fileName)
.contentType(file.getContentType())
.stream(inputStream, inputStream.available(), -1)
.build();
@@ -69,13 +87,13 @@ public class MinioFileServiceImpl implements FileService {
if (StrUtil.isBlank(domain)) {
GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
.bucket(bucket)
- .object(dateFolder + "/"+ fileName)
+ .object(dateFolder + "/" + fileName)
.method(Method.GET)
.build();
fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs);
fileUrl = fileUrl.substring(0, fileUrl.indexOf("?"));
} else {
- fileUrl = domain + "/"+ bucket + "/"+ dateFolder + "/"+ fileName;
+ fileUrl = domain + "/" + bucket + "/" + dateFolder + "/" + fileName;
}
FileInfo fileInfo = new FileInfo();
@@ -88,6 +106,14 @@ public class MinioFileServiceImpl implements FileService {
}
}
+ /**
+ * 删除文件。
+ *