From 07616d9118e15c4de0cb9dd2c4774e66b124a774 Mon Sep 17 00:00:00 2001 From: "Ray.Hao" <1490493387@qq.com> Date: Fri, 31 Jul 2026 00:21:49 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=A1=B9=E7=9B=AE=E5=8C=85?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.en.md | 49 ++- README.md | 41 +- pom.xml | 2 +- sql/youlai-admin.sql | 4 +- .../boot/auth/controller/AuthController.java | 31 +- .../auth/controller/QrCodeAuthController.java | 96 +++++ .../auth/controller/SmsAuthController.java | 56 +++ .../auth/controller/WxMaAuthController.java | 28 +- .../enums/CaptchaTypeEnum.java | 2 +- .../auth/enums/QrCodeLoginStatusEnum.java | 28 ++ .../exception/CaptchaException.java | 9 +- .../exception/SmsCaptchaException.java | 2 +- .../dto}/QrCodeLoginContext.java | 13 +- .../LoginForm.java => req/LoginReq.java} | 6 +- .../boot/auth/model/req/QrCodeTicketReq.java | 17 + .../WxMaBindMobileReq.java} | 6 +- .../WxMaPhoneLoginReq.java} | 6 +- .../model/resp}/CaptchaInfo.java | 2 +- .../resp/QrCodeGenerateResp.java} | 9 +- .../resp/QrCodeStatusResp.java} | 9 +- .../WxMaLoginResp.java} | 4 +- .../controller/QrCodeLoginController.java | 89 ----- .../qrcode/model/QrCodeLoginStatusEnum.java | 21 -- .../qrcode/model/form/QrCodeTicketForm.java | 14 - .../qrcode/service/QrCodeLoginService.java | 29 -- .../qrcode/util/QrCodeNicknameMasker.java | 34 -- .../filter/CaptchaValidationFilter.java | 4 +- .../provider/SmsAuthenticationProvider.java | 4 +- .../provider/WxMaAuthenticationProvider.java | 4 +- .../SmsAuthenticationToken.java | 2 +- .../WxMaAuthenticationToken.java | 2 +- .../youlai/boot/auth/service/AuthService.java | 129 +++++-- .../service/CaptchaService.java | 13 +- .../QrCodeLoginService.java} | 139 ++++--- .../boot/auth/service/WxMaAuthService.java | 228 +++++++++++- .../auth/service/impl/AuthServiceImpl.java | 151 -------- .../service/impl/WxMaAuthServiceImpl.java | 245 ------------ .../codegen/controller/CodegenController.java | 2 +- .../boot/codegen/model/entity/GenTable.java | 2 +- .../codegen/model/entity/GenTableColumn.java | 2 +- .../annotation/DataPermission.java | 2 +- .../{framework => common}/annotation/Log.java | 2 +- .../annotation/RateLimit.java | 2 +- .../annotation/RepeatSubmit.java | 2 +- .../annotation/ValidField.java | 2 +- .../web => common}/aspect/LogAspect.java | 28 +- .../aspect/RateLimitAspect.java | 8 +- .../aspect/RepeatSubmitAspect.java | 4 +- .../base/BaseEntity.java | 2 +- .../youlai/boot/common/base/BaseQuery.java | 2 +- .../boot/common/enums/SocialPlatformEnum.java | 7 +- .../exception/MobileNotBoundException.java | 9 +- .../exception/RateLimitException.java | 2 +- .../exception/TokenInvalidException.java | 2 +- .../boot/common/validator/FieldValidator.java | 2 +- .../cache => config}/CaffeineConfig.java | 2 +- .../captcha => }/config/CaptchaConfig.java | 3 +- .../web => }/config/CorsConfig.java | 2 +- .../web => }/config/JacksonConfig.java | 2 +- .../Knife4jOpenApiCustomizer.java | 2 +- .../mail => }/config/MailConfig.java | 3 +- .../mybatis => }/config/MybatisConfig.java | 2 +- .../openapi => config}/OpenApiConfig.java | 4 +- .../config/OperationLogExecutorConfig.java | 2 +- .../config/PasswordEncoderConfig.java | 2 +- .../cache => config}/RedisCacheConfig.java | 2 +- .../cache => config}/RedisConfig.java | 2 +- .../security => }/config/SecurityConfig.java | 10 +- .../wxma => }/config/WxMaConfig.java | 3 +- .../job => config}/XxlJobConfig.java | 2 +- .../property}/AliyunSmsProperties.java | 2 +- .../property}/CaptchaProperties.java | 2 +- .../property}/FileStorageProperties.java | 2 +- .../property}/MailProperties.java | 2 +- .../property}/RateLimitProperties.java | 2 +- .../property}/SecurityProperties.java | 2 +- .../property}/WxMaProperties.java | 2 +- .../integration/mail/service/MailService.java | 76 ---- .../integration/sms/service/SmsService.java | 24 -- .../job/handler/XxlJobSampleHandler.java | 0 .../interceptor/MyDataPermissionHandler.java | 2 +- .../security/port/PermissionPort.java | 2 +- .../security/port/UserAuthenticationPort.java | 2 +- .../security/token/JwtTokenManager.java | 4 +- .../security/token/RedisTokenManager.java | 4 +- .../web/advice/GlobalExceptionHandler.java | 12 +- .../web/filter/IpRateLimitFilter.java | 2 +- .../boot/message/job/OnlineUserCountTask.java | 29 -- .../boot/message/service/SseService.java | 140 ------- .../youlai/boot/message/topic/SseTopics.java | 19 - .../{ => support}/file/model/FileInfo.java | 3 +- .../file/service/FileService.java | 8 +- .../service/impl/AliyunFileServiceImpl.java | 25 +- .../service/impl/LocalFileServiceImpl.java | 25 +- .../service/impl/MinioFileServiceImpl.java | 44 ++- .../boot/support/mail/EmailService.java | 32 ++ .../mail/impl/SmtpEmailServiceImpl.java | 74 ++++ .../youlai/boot/support/sms/SmsService.java | 25 ++ .../enums => support/sms}/SmsTypeEnum.java | 2 +- .../sms/impl/AliyunSmsServiceImpl.java} | 48 +-- .../youlai/boot/support/sse/SseService.java | 132 +++++++ .../sse/SseSessionManager.java} | 67 ++-- .../sse/constant/SseEventConstants.java | 19 + .../boot/support/sse/constant/SseTopics.java | 25 ++ .../sse/model}/OnlineUserDTO.java | 10 +- .../system/controller/ConfigController.java | 2 +- .../system/controller/DeptController.java | 4 +- .../system/controller/DictController.java | 29 +- .../controller/FileController.java | 8 +- .../boot/system/controller/LogController.java | 2 +- .../system/controller/MenuController.java | 4 +- .../system/controller/NoticeController.java | 2 +- .../system/controller/RoleController.java | 4 +- .../controller/SseController.java | 25 +- .../system/controller/UserController.java | 4 +- .../system/handler/XxlJobSampleHandler.java | 19 - .../boot/system/job/OnlineUserCountTask.java | 27 ++ .../youlai/boot/system/mapper/DeptMapper.java | 2 +- .../youlai/boot/system/mapper/UserMapper.java | 2 +- .../model}/dto/DictChangeEvent.java | 3 +- .../boot/system/model/entity/Config.java | 2 +- .../youlai/boot/system/model/entity/Dept.java | 2 +- .../youlai/boot/system/model/entity/Dict.java | 2 +- .../boot/system/model/entity/DictItem.java | 2 +- .../youlai/boot/system/model/entity/Menu.java | 2 +- .../boot/system/model/entity/Notice.java | 2 +- .../youlai/boot/system/model/entity/Role.java | 2 +- .../boot/system/model/entity/SysLog.java | 4 +- .../boot/system/model/entity/SysUser.java | 2 +- .../boot/system/model/entity/UserNotice.java | 2 +- .../boot/system/model/entity/UserSocial.java | 2 +- .../{adapter => port}/PermissionAdapter.java | 4 +- .../UserAuthenticationAdapter.java | 4 +- .../service/impl/NoticeServiceImpl.java | 4 +- .../system/service/impl/UserServiceImpl.java | 24 +- src/main/resources/application-dev.yml | 10 +- src/main/resources/application-prod.yml | 10 +- .../templates/codegen/backend/entity.java.vm | 2 +- .../service/impl/AuthServiceImplTest.java | 61 --- .../advice/GlobalExceptionHandlerTest.java | 20 - .../OperationLogExecutorConfigTest.java | 28 -- .../boot/generator/SystemCodeGenerator.java | 97 ----- .../filter/TokenAuthenticationFilterTest.java | 48 --- .../security/token/JwtTokenManagerTest.java | 350 ------------------ 144 files changed, 1410 insertions(+), 1935 deletions(-) create mode 100644 src/main/java/com/youlai/boot/auth/controller/QrCodeAuthController.java create mode 100644 src/main/java/com/youlai/boot/auth/controller/SmsAuthController.java rename src/main/java/com/youlai/boot/{framework/captcha => auth}/enums/CaptchaTypeEnum.java (75%) create mode 100644 src/main/java/com/youlai/boot/auth/enums/QrCodeLoginStatusEnum.java rename src/main/java/com/youlai/boot/{framework/captcha => auth}/exception/CaptchaException.java (62%) rename src/main/java/com/youlai/boot/auth/{security => }/exception/SmsCaptchaException.java (85%) rename src/main/java/com/youlai/boot/auth/{qrcode/model => model/dto}/QrCodeLoginContext.java (66%) rename src/main/java/com/youlai/boot/auth/model/{form/LoginForm.java => req/LoginReq.java} (89%) create mode 100644 src/main/java/com/youlai/boot/auth/model/req/QrCodeTicketReq.java rename src/main/java/com/youlai/boot/auth/model/{form/WxMaBindMobileForm.java => req/WxMaBindMobileReq.java} (85%) rename src/main/java/com/youlai/boot/auth/model/{form/WxMaPhoneLoginForm.java => req/WxMaPhoneLoginReq.java} (82%) rename src/main/java/com/youlai/boot/{framework/captcha/model => auth/model/resp}/CaptchaInfo.java (90%) rename src/main/java/com/youlai/boot/auth/{qrcode/model/vo/QrCodeGenerateVO.java => model/resp/QrCodeGenerateResp.java} (69%) rename src/main/java/com/youlai/boot/auth/{qrcode/model/vo/QrCodeStatusVO.java => model/resp/QrCodeStatusResp.java} (80%) rename src/main/java/com/youlai/boot/auth/model/{vo/WxMaLoginVO.java => resp/WxMaLoginResp.java} (92%) delete mode 100644 src/main/java/com/youlai/boot/auth/qrcode/controller/QrCodeLoginController.java delete mode 100644 src/main/java/com/youlai/boot/auth/qrcode/model/QrCodeLoginStatusEnum.java delete mode 100644 src/main/java/com/youlai/boot/auth/qrcode/model/form/QrCodeTicketForm.java delete mode 100644 src/main/java/com/youlai/boot/auth/qrcode/service/QrCodeLoginService.java delete mode 100644 src/main/java/com/youlai/boot/auth/qrcode/util/QrCodeNicknameMasker.java rename src/main/java/com/youlai/boot/auth/security/{model => token}/SmsAuthenticationToken.java (97%) rename src/main/java/com/youlai/boot/auth/security/{model => token}/WxMaAuthenticationToken.java (97%) rename src/main/java/com/youlai/boot/{framework/captcha => auth}/service/CaptchaService.java (92%) rename src/main/java/com/youlai/boot/auth/{qrcode/service/impl/QrCodeLoginServiceImpl.java => service/QrCodeLoginService.java} (70%) delete mode 100644 src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java delete mode 100644 src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java rename src/main/java/com/youlai/boot/{framework => common}/annotation/DataPermission.java (91%) rename src/main/java/com/youlai/boot/{framework => common}/annotation/Log.java (95%) rename src/main/java/com/youlai/boot/{framework => common}/annotation/RateLimit.java (93%) rename src/main/java/com/youlai/boot/{framework => common}/annotation/RepeatSubmit.java (90%) rename src/main/java/com/youlai/boot/{framework => common}/annotation/ValidField.java (93%) rename src/main/java/com/youlai/boot/{framework/web => common}/aspect/LogAspect.java (90%) rename src/main/java/com/youlai/boot/{framework/web => common}/aspect/RateLimitAspect.java (94%) rename src/main/java/com/youlai/boot/{framework/web => common}/aspect/RepeatSubmitAspect.java (97%) rename src/main/java/com/youlai/boot/{framework => common}/base/BaseEntity.java (96%) rename src/main/java/com/youlai/boot/{auth/security => common}/exception/MobileNotBoundException.java (70%) rename src/main/java/com/youlai/boot/{framework/web => common}/exception/RateLimitException.java (92%) rename src/main/java/com/youlai/boot/{framework/security => common}/exception/TokenInvalidException.java (89%) rename src/main/java/com/youlai/boot/{framework/cache => config}/CaffeineConfig.java (96%) rename src/main/java/com/youlai/boot/{framework/captcha => }/config/CaptchaConfig.java (94%) rename src/main/java/com/youlai/boot/{framework/web => }/config/CorsConfig.java (97%) rename src/main/java/com/youlai/boot/{framework/web => }/config/JacksonConfig.java (97%) rename src/main/java/com/youlai/boot/{framework/openapi => config}/Knife4jOpenApiCustomizer.java (99%) rename src/main/java/com/youlai/boot/{framework/integration/mail => }/config/MailConfig.java (93%) rename src/main/java/com/youlai/boot/{framework/mybatis => }/config/MybatisConfig.java (96%) rename src/main/java/com/youlai/boot/{framework/openapi => config}/OpenApiConfig.java (97%) rename src/main/java/com/youlai/boot/{framework/web => }/config/OperationLogExecutorConfig.java (95%) rename src/main/java/com/youlai/boot/{framework/security => }/config/PasswordEncoderConfig.java (91%) rename src/main/java/com/youlai/boot/{framework/cache => config}/RedisCacheConfig.java (98%) rename src/main/java/com/youlai/boot/{framework/cache => config}/RedisConfig.java (97%) rename src/main/java/com/youlai/boot/{auth/security => }/config/SecurityConfig.java (93%) rename src/main/java/com/youlai/boot/{framework/integration/wxma => }/config/WxMaConfig.java (91%) rename src/main/java/com/youlai/boot/{framework/job => config}/XxlJobConfig.java (97%) rename src/main/java/com/youlai/boot/{framework/integration/sms/config => config/property}/AliyunSmsProperties.java (93%) rename src/main/java/com/youlai/boot/{framework/captcha/config => config/property}/CaptchaProperties.java (97%) rename src/main/java/com/youlai/boot/{file/config => config/property}/FileStorageProperties.java (97%) rename src/main/java/com/youlai/boot/{framework/integration/mail/config => config/property}/MailProperties.java (94%) rename src/main/java/com/youlai/boot/{framework/web/config => config/property}/RateLimitProperties.java (96%) rename src/main/java/com/youlai/boot/{framework/security/config => config/property}/SecurityProperties.java (98%) rename src/main/java/com/youlai/boot/{framework/integration/wxma/config => config/property}/WxMaProperties.java (82%) delete mode 100644 src/main/java/com/youlai/boot/framework/integration/mail/service/MailService.java delete mode 100644 src/main/java/com/youlai/boot/framework/integration/sms/service/SmsService.java delete mode 100644 src/main/java/com/youlai/boot/framework/job/handler/XxlJobSampleHandler.java delete mode 100644 src/main/java/com/youlai/boot/message/job/OnlineUserCountTask.java delete mode 100644 src/main/java/com/youlai/boot/message/service/SseService.java delete mode 100644 src/main/java/com/youlai/boot/message/topic/SseTopics.java rename src/main/java/com/youlai/boot/{ => support}/file/model/FileInfo.java (88%) rename src/main/java/com/youlai/boot/{ => support}/file/service/FileService.java (76%) rename src/main/java/com/youlai/boot/{ => support}/file/service/impl/AliyunFileServiceImpl.java (76%) rename src/main/java/com/youlai/boot/{ => support}/file/service/impl/LocalFileServiceImpl.java (71%) rename src/main/java/com/youlai/boot/{ => support}/file/service/impl/MinioFileServiceImpl.java (76%) create mode 100644 src/main/java/com/youlai/boot/support/mail/EmailService.java create mode 100644 src/main/java/com/youlai/boot/support/mail/impl/SmtpEmailServiceImpl.java create mode 100644 src/main/java/com/youlai/boot/support/sms/SmsService.java rename src/main/java/com/youlai/boot/{framework/integration/sms/enums => support/sms}/SmsTypeEnum.java (93%) rename src/main/java/com/youlai/boot/{framework/integration/sms/service/impl/AliyunSmsService.java => support/sms/impl/AliyunSmsServiceImpl.java} (58%) create mode 100644 src/main/java/com/youlai/boot/support/sse/SseService.java rename src/main/java/com/youlai/boot/{message/registry/SseSessionRegistry.java => support/sse/SseSessionManager.java} (81%) create mode 100644 src/main/java/com/youlai/boot/support/sse/constant/SseEventConstants.java create mode 100644 src/main/java/com/youlai/boot/support/sse/constant/SseTopics.java rename src/main/java/com/youlai/boot/{message/dto => support/sse/model}/OnlineUserDTO.java (63%) rename src/main/java/com/youlai/boot/{file => system}/controller/FileController.java (92%) rename src/main/java/com/youlai/boot/{message => system}/controller/SseController.java (71%) create mode 100644 src/main/java/com/youlai/boot/system/job/OnlineUserCountTask.java rename src/main/java/com/youlai/boot/{message => system/model}/dto/DictChangeEvent.java (85%) rename src/main/java/com/youlai/boot/system/security/{adapter => port}/PermissionAdapter.java (89%) rename src/main/java/com/youlai/boot/system/security/{adapter => port}/UserAuthenticationAdapter.java (93%) delete mode 100644 src/test/java/com/youlai/boot/auth/service/impl/AuthServiceImplTest.java delete mode 100644 src/test/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandlerTest.java delete mode 100644 src/test/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfigTest.java delete mode 100644 src/test/java/com/youlai/boot/generator/SystemCodeGenerator.java delete mode 100644 src/test/java/com/youlai/boot/security/filter/TokenAuthenticationFilterTest.java delete mode 100644 src/test/java/com/youlai/boot/security/token/JwtTokenManagerTest.java diff --git a/README.en.md b/README.en.md index e1c82325..03ce7b65 100644 --- a/README.en.md +++ b/README.en.md @@ -192,59 +192,54 @@ Detailed guide: https://www.youlai.tech/docs/server/spring-boot/quick-start.html + ## Directory Structure -``` +``` youlai-boot/ -├── deploy/ +├── docker/ # Docker orchestration -│ └── docker/ # Docker orchestration +├── docs/ # Docs and image assets -├── docs/ # Docs and image assets +├── sql/ # Database init scripts -├── sql/ # Database init scripts +├── src/ # Source code -├── src/ # Source code +│ ├── auth/ # Auth module (login/logout/token/captcha) -│ ├── YouLaiBootApplication.java # Bootstrap class +│ ├── codegen/ # Code generator -│ ├── auth/ # Auth (login/logout/token) +│ ├── common/ # Common module (constants/enums/exceptions/response/utils) -│ ├── codegen/ # Code generator +│ ├── config/ # Configuration (Security/Redis/Captcha/OSS/Email etc.) -│ ├── common/ # Common module (constants/enums/response) +│ ├── framework/ # Infrastructure layer -│ ├── file/ # File service (MinIO/local/OSS) +│ │ ├── mybatis/ # MyBatis-Plus config -│ ├── framework/ # Technical framework layer +│ │ ├── security/ # Spring Security (filters/token/utils) -│ │ ├── apidoc/ # OpenAPI / Knife4j +│ │ └── web/ # Web layer (global exception/CORS/rate limit) -│ │ ├── cache/ # Redis / Caffeine cache +│ ├── support/ # Capability services -│ │ ├── captcha/ # Graphic captcha +│ │ ├── file/ # File storage (MinIO/Alibaba Cloud OSS/local) -│ │ ├── integration/ # SMS / Email / WeChat +│ │ ├── mail/ # Email service -│ │ ├── job/ # XXL-Job scheduled tasks +│ │ ├── sms/ # SMS service -│ │ ├── mybatis/ # MyBatis-Plus config +│ │ └── sse/ # SSE push -│ │ ├── security/ # Security / JWT / Token +│ └── system/ # Business (user/role/menu/dept/dict etc.) -│ │ └── web/ # Global exception / CORS / rate limit +└── pom.xml # Maven dependency management -│ ├── message/ # SSE push - -│ └── system/ # Business (user/role/menu/dept) - -└── pom.xml # Maven dependency management - -``` +``` diff --git a/README.md b/README.md index e0d97ef7..53b2edc2 100644 --- a/README.md +++ b/README.md @@ -107,28 +107,25 @@ ``` youlai-boot/ -├── deploy/ -│ └── docker/ # Docker 部署编排 -├── docs/ # 项目文档与图片资源 -├── sql/ # 数据库初始化脚本 -├── src/ # 源码目录 -│ ├── YouLaiBootApplication.java # 启动类 -│ ├── auth/ # 认证授权(登录/登出/令牌) -│ ├── codegen/ # 代码生成器 -│ ├── common/ # 公共模块(常量/枚举/统一响应) -│ ├── file/ # 文件服务(MinIO/本地/OSS) -│ ├── framework/ # 技术框架层 -│ │ ├── apidoc/ # OpenAPI / Knife4j -│ │ ├── cache/ # Redis / Caffeine 缓存 -│ │ ├── captcha/ # 图形验证码 -│ │ ├── integration/ # 短信 / 邮件 / 微信 -│ │ ├── job/ # XXL-Job 定时任务 -│ │ ├── mybatis/ # MyBatis-Plus 配置 -│ │ ├── security/ # Security / JWT / Token -│ │ └── web/ # 全局异常 / 跨域 / 限流 -│ ├── message/ # SSE 消息推送 -│ └── system/ # 系统业务(用户/角色/菜单/部门) -└── pom.xml # Maven 依赖管理 +├── docker/ # Docker 部署编排 +├── docs/ # 项目文档与图片资源 +├── sql/ # 数据库初始化脚本 +├── src/ # 源码目录 +│ ├── auth/ # 认证模块(登录/登出/令牌/验证码) +│ ├── codegen/ # 代码生成器 +│ ├── common/ # 公共模块(常量/枚举/异常/统一响应/工具类) +│ ├── config/ # 配置类(Security/Redis/Captcha/OSS/邮件等) +│ ├── framework/ # 基础设施层 +│ │ ├── mybatis/ # MyBatis-Plus 配置 +│ │ ├── security/ # Spring Security(过滤器/Token/工具类) +│ │ └── web/ # Web 层(全局异常/跨域/限流) +│ ├── support/ # 能力服务 +│ │ ├── file/ # 文件存储(MinIO/阿里云OSS/本地) +│ │ ├── mail/ # 邮件服务 +│ │ ├── sms/ # 短信服务 +│ │ └── sse/ # SSE 消息推送 +│ └── system/ # 系统业务(用户/角色/菜单/部门/字典等) +└── pom.xml # Maven 依赖管理 ``` ## 生态矩阵 diff --git a/pom.xml b/pom.xml index 85ce5888..7218f784 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.youlai youlai-boot - 4.5.0 + 4.6.0 基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。 diff --git a/sql/youlai-admin.sql b/sql/youlai-admin.sql index 37e3caf6..e9dbce87 100644 --- a/sql/youlai-admin.sql +++ b/sql/youlai-admin.sql @@ -411,7 +411,7 @@ CREATE TABLE `sys_log` ( `module` TINYINT NOT NULL COMMENT '模块,数字枚举,参考 LogModule 枚举', `action_type` TINYINT NOT NULL COMMENT '操作类型,数字枚举,参考 ActionType 枚举', `title` VARCHAR(100) NOT NULL COMMENT '前端显示标题', - `content` TEXT COMMENT '自定义日志内容', + `content` VARCHAR(500) COMMENT '自定义日志内容', `operator_id` BIGINT COMMENT '操作人ID', `operator_name` VARCHAR(50) COMMENT '操作人名称', `request_uri` VARCHAR(255) COMMENT '请求路径', @@ -423,7 +423,7 @@ CREATE TABLE `sys_log` ( `os` VARCHAR(100) COMMENT '操作系统', `browser` VARCHAR(100) COMMENT '浏览器', `status` TINYINT DEFAULT 1 COMMENT '0失败 1成功', - `error_msg` VARCHAR(255) COMMENT '错误信息', + `error_msg` VARCHAR(1000) COMMENT '错误信息', `execution_time` INT COMMENT '执行时间(ms)', `create_time` DATETIME COMMENT '操作时间', PRIMARY KEY (`id`) USING BTREE, diff --git a/src/main/java/com/youlai/boot/auth/controller/AuthController.java b/src/main/java/com/youlai/boot/auth/controller/AuthController.java index 64977a50..f29a684a 100644 --- a/src/main/java/com/youlai/boot/auth/controller/AuthController.java +++ b/src/main/java/com/youlai/boot/auth/controller/AuthController.java @@ -1,13 +1,13 @@ package com.youlai.boot.auth.controller; -import com.youlai.boot.auth.model.form.LoginForm; +import com.youlai.boot.auth.model.req.LoginReq; import com.youlai.boot.common.enums.ActionTypeEnum; import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.result.Result; import com.youlai.boot.auth.service.AuthService; -import com.youlai.boot.framework.annotation.Log; -import com.youlai.boot.framework.annotation.RateLimit; -import com.youlai.boot.framework.captcha.model.CaptchaInfo; +import com.youlai.boot.common.annotation.Log; +import com.youlai.boot.common.annotation.RateLimit; +import com.youlai.boot.auth.model.resp.CaptchaInfo; import com.youlai.boot.framework.security.model.AuthenticationToken; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -49,32 +49,11 @@ public class AuthController { @PostMapping("/login") @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) @RateLimit - public Result login(@RequestBody @Valid LoginForm request) { + public Result login(@RequestBody @Valid LoginReq request) { AuthenticationToken authenticationToken = authService.login(request.getUsername(), request.getPassword()); return Result.success(authenticationToken); } - @Operation(summary = "短信验证码登录") - @PostMapping("/login/sms") - @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) - public Result loginBySms( - @Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile, - @Parameter(description = "验证码", example = "123456") @RequestParam String code - ) { - AuthenticationToken loginResult = authService.loginBySms(mobile, code); - return Result.success(loginResult); - } - - @Operation(summary = "发送登录短信验证码") - @PostMapping("/sms/code") - @RateLimit(limit = 1, window = 60) - public Result sendSmsCode( - @Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile - ) { - authService.sendSmsCode(mobile); - return Result.success(); - } - @Operation(summary = "退出登录") @DeleteMapping("/logout") @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGOUT) diff --git a/src/main/java/com/youlai/boot/auth/controller/QrCodeAuthController.java b/src/main/java/com/youlai/boot/auth/controller/QrCodeAuthController.java new file mode 100644 index 00000000..221ef724 --- /dev/null +++ b/src/main/java/com/youlai/boot/auth/controller/QrCodeAuthController.java @@ -0,0 +1,96 @@ +package com.youlai.boot.auth.controller; + +import com.youlai.boot.auth.model.req.QrCodeTicketReq; +import com.youlai.boot.auth.model.resp.QrCodeGenerateResp; +import com.youlai.boot.auth.model.resp.QrCodeStatusResp; +import com.youlai.boot.auth.service.QrCodeLoginService; +import com.youlai.boot.common.util.IPUtils; +import com.youlai.boot.common.annotation.RateLimit; +import com.youlai.boot.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 上下文获取。

+ * + * @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 generate(HttpServletRequest request) { + return Result.success(qrCodeLoginService.generate(IPUtils.getIpAddr(request))); + } + + /** + * 查询扫码状态 + */ + @Operation(summary = "[扫码]查询扫码状态") + @GetMapping("/status") + @RateLimit(limit = 60, window = 60) + public Result status(@RequestParam String ticket) { + return Result.success(qrCodeLoginService.status(ticket)); + } + + /** + * APP 标记已扫码 + */ + @Operation(summary = "[扫码]APP 标记已扫码") + @PostMapping("/scan") + public Result scan(@RequestBody @Valid QrCodeTicketReq form) { + return Result.success(qrCodeLoginService.scan(form.getTicket(), SecurityUtils.getUserId())); + } + + /** + * APP 确认登录 + */ + @Operation(summary = "[扫码]APP 确认登录") + @PostMapping("/confirm") + public Result confirm(@RequestBody @Valid QrCodeTicketReq form) { + return Result.success(qrCodeLoginService.confirm(form.getTicket(), SecurityUtils.getUserId())); + } + + /** + * APP 取消登录 + */ + @Operation(summary = "[扫码]APP 取消登录") + @PostMapping("/cancel") + public Result cancel(@RequestBody @Valid QrCodeTicketReq form) { + return Result.success(qrCodeLoginService.cancel(form.getTicket(), SecurityUtils.getUserId())); + } + + /** + * PC 端用票据换取会话令牌 + */ + @Operation(summary = "[扫码]PC 端换取会话令牌") + @PostMapping("/login") + public Result login(@RequestBody @Valid QrCodeTicketReq form) { + return Result.success(qrCodeLoginService.login(form.getTicket())); + } +} diff --git a/src/main/java/com/youlai/boot/auth/controller/SmsAuthController.java b/src/main/java/com/youlai/boot/auth/controller/SmsAuthController.java new file mode 100644 index 00000000..8eba7c72 --- /dev/null +++ b/src/main/java/com/youlai/boot/auth/controller/SmsAuthController.java @@ -0,0 +1,56 @@ +package com.youlai.boot.auth.controller; + +import com.youlai.boot.auth.service.AuthService; +import com.youlai.boot.common.enums.ActionTypeEnum; +import com.youlai.boot.common.enums.LogModuleEnum; +import com.youlai.boot.common.result.Result; +import com.youlai.boot.common.annotation.Log; +import com.youlai.boot.common.annotation.RateLimit; +import com.youlai.boot.framework.security.model.AuthenticationToken; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * 短信验证码认证接口 + * + * @author Ray.Hao + * @since 4.5.0 + */ +@Tag(name = "01.认证中心") +@RestController +@RequestMapping("/api/v1/auth/sms") +@RequiredArgsConstructor +@Slf4j +public class SmsAuthController { + + private final AuthService authService; + + @Operation(summary = "[短信]发送登录短信验证码") + @PostMapping("/code") + @RateLimit(limit = 1, window = 60) + public Result sendCode( + @Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile + ) { + authService.sendSmsCode(mobile); + return Result.success(); + } + + @Operation(summary = "[短信]短信验证码登录") + @PostMapping("/login") + @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) + public Result login( + @Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile, + @Parameter(description = "验证码", example = "123456") @RequestParam String code + ) { + AuthenticationToken loginResult = authService.loginBySms(mobile, code); + return Result.success(loginResult); + } + +} diff --git a/src/main/java/com/youlai/boot/auth/controller/WxMaAuthController.java b/src/main/java/com/youlai/boot/auth/controller/WxMaAuthController.java index 44e5ab9b..1cb64564 100644 --- a/src/main/java/com/youlai/boot/auth/controller/WxMaAuthController.java +++ b/src/main/java/com/youlai/boot/auth/controller/WxMaAuthController.java @@ -1,11 +1,11 @@ package com.youlai.boot.auth.controller; -import com.youlai.boot.auth.model.form.WxMaBindMobileForm; -import com.youlai.boot.auth.model.form.WxMaPhoneLoginForm; -import com.youlai.boot.auth.model.vo.WxMaLoginVO; +import com.youlai.boot.auth.model.req.WxMaBindMobileReq; +import com.youlai.boot.auth.model.req.WxMaPhoneLoginReq; +import com.youlai.boot.auth.model.resp.WxMaLoginResp; import com.youlai.boot.auth.service.WxMaAuthService; -import com.youlai.boot.framework.annotation.Log; -import com.youlai.boot.framework.annotation.RateLimit; +import com.youlai.boot.common.annotation.Log; +import com.youlai.boot.common.annotation.RateLimit; import com.youlai.boot.common.enums.ActionTypeEnum; import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.result.Result; @@ -29,9 +29,9 @@ import jakarta.validation.Valid; * @author Ray.Hao * @since 2.4.0 */ -@Tag(name = "13.微信小程序认证") +@Tag(name = "01.认证中心") @RestController -@RequestMapping("/api/v1/wxma/auth") +@RequestMapping("/api/v1/auth/wxma") @RequiredArgsConstructor @Slf4j public class WxMaAuthController { @@ -47,15 +47,15 @@ public class WxMaAuthController { *
  • 未绑定手机号的用户:返回 openid,需调用绑定手机号接口
  • * */ - @Operation(summary = "静默登录", description = "通过微信 code 登录,已绑定用户直接返回 token,未绑定用户返回 openid 需绑定手机号") + @Operation(summary = "[小程序]静默登录", description = "通过微信 code 登录,已绑定用户直接返回 token,未绑定用户返回 openid 需绑定手机号") @PostMapping("/silent-login") @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) @RateLimit - public Result silentLogin( + public Result silentLogin( @Parameter(description = "微信登录凭证(wx.login 获取)", required = true, example = "0xxx") @RequestParam String code ) { - WxMaLoginVO result = wxMaAuthService.silentLogin(code); + WxMaLoginResp result = wxMaAuthService.silentLogin(code); return Result.success(result); } @@ -66,11 +66,11 @@ public class WxMaAuthController { *

    * 一步完成登录,无需绑定流程,自动创建新用户 */ - @Operation(summary = "手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序") + @Operation(summary = "[小程序]手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序") @PostMapping("/phone-login") @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) @RateLimit - public Result phoneLogin(@Valid @RequestBody WxMaPhoneLoginForm req) { + public Result phoneLogin(@Valid @RequestBody WxMaPhoneLoginReq req) { AuthenticationToken result = wxMaAuthService.phoneLogin(req.getLoginCode(), req.getPhoneCode()); return Result.success(result); } @@ -82,11 +82,11 @@ public class WxMaAuthController { *

    * 绑定成功后自动完成登录 */ - @Operation(summary = "绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录") + @Operation(summary = "[小程序]绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录") @PostMapping("/bind-mobile") @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) @RateLimit - public Result bindMobile(@Valid @RequestBody WxMaBindMobileForm req) { + public Result bindMobile(@Valid @RequestBody WxMaBindMobileReq req) { AuthenticationToken result = wxMaAuthService.bindMobile(req.getOpenid(), req.getMobile(), req.getSmsCode()); return Result.success(result); } diff --git a/src/main/java/com/youlai/boot/framework/captcha/enums/CaptchaTypeEnum.java b/src/main/java/com/youlai/boot/auth/enums/CaptchaTypeEnum.java similarity index 75% rename from src/main/java/com/youlai/boot/framework/captcha/enums/CaptchaTypeEnum.java rename to src/main/java/com/youlai/boot/auth/enums/CaptchaTypeEnum.java index 5a603983..ab9972a6 100644 --- a/src/main/java/com/youlai/boot/framework/captcha/enums/CaptchaTypeEnum.java +++ b/src/main/java/com/youlai/boot/auth/enums/CaptchaTypeEnum.java @@ -1,4 +1,4 @@ -package com.youlai.boot.framework.captcha.enums; +package com.youlai.boot.auth.enums; /** * EasyCaptcha 验证码类型 diff --git a/src/main/java/com/youlai/boot/auth/enums/QrCodeLoginStatusEnum.java b/src/main/java/com/youlai/boot/auth/enums/QrCodeLoginStatusEnum.java new file mode 100644 index 00000000..181bca5d --- /dev/null +++ b/src/main/java/com/youlai/boot/auth/enums/QrCodeLoginStatusEnum.java @@ -0,0 +1,28 @@ +package com.youlai.boot.auth.enums; + +/** + * 扫码登录票据状态 + * + * @author Ray.Hao + * @since 4.5.0 + */ +public enum QrCodeLoginStatusEnum { + + /** 票据已创建,等待 APP 扫码 */ + WAITING, + + /** APP 已扫码,等待用户在手机上确认 */ + SCANNED, + + /** 用户已在 APP 上确认登录 */ + CONFIRMED, + + /** PC 已用票据换取会话令牌,票据作废,不可再用 */ + LOGGED_IN, + + /** 用户在 APP 上取消登录 */ + CANCELED, + + /** 票据超时,由 Redis TTL 自动清理 */ + EXPIRED +} diff --git a/src/main/java/com/youlai/boot/framework/captcha/exception/CaptchaException.java b/src/main/java/com/youlai/boot/auth/exception/CaptchaException.java similarity index 62% rename from src/main/java/com/youlai/boot/framework/captcha/exception/CaptchaException.java rename to src/main/java/com/youlai/boot/auth/exception/CaptchaException.java index 61b95be9..78702000 100644 --- a/src/main/java/com/youlai/boot/framework/captcha/exception/CaptchaException.java +++ b/src/main/java/com/youlai/boot/auth/exception/CaptchaException.java @@ -1,10 +1,15 @@ -package com.youlai.boot.framework.captcha.exception; +package com.youlai.boot.auth.exception; import com.youlai.boot.common.result.ResultCode; import lombok.Getter; /** - * 验证码异常 + * 图形验证码校验失败时抛出的异常。 + *

    + * 携带 {@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 generate(HttpServletRequest request) { - return Result.success(qrCodeLoginService.generate(getClientIp(request))); - } - - @Operation(summary = "查询扫码状态") - @GetMapping("/status") - @RateLimit(limit = 60, window = 60) - public Result status(@RequestParam String ticket) { - return Result.success(qrCodeLoginService.status(ticket)); - } - - @Operation(summary = "APP 标记已扫码") - @PostMapping("/scan") - public Result scan(@RequestBody @Valid QrCodeTicketForm form) { - return Result.success(qrCodeLoginService.scan(form.getTicket(), SecurityUtils.getUserId())); - } - - @Operation(summary = "APP 确认登录") - @PostMapping("/confirm") - public Result confirm(@RequestBody @Valid QrCodeTicketForm form) { - return Result.success(qrCodeLoginService.confirm(form.getTicket(), SecurityUtils.getUserId())); - } - - @Operation(summary = "APP 取消登录") - @PostMapping("/cancel") - public Result cancel(@RequestBody @Valid QrCodeTicketForm form) { - return Result.success(qrCodeLoginService.cancel(form.getTicket(), SecurityUtils.getUserId())); - } - - @Operation(summary = "PC 端换取会话令牌") - @PostMapping("/login") - public Result login(@RequestBody @Valid QrCodeTicketForm form) { - return Result.success(qrCodeLoginService.login(form.getTicket())); - } - - /** 从请求头或连接信息中提取客户端 IP,兼容反向代理 */ - private String getClientIp(HttpServletRequest request) { - String ip = request.getHeader("X-Forwarded-For"); - if (StrUtil.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) { - int comma = ip.indexOf(','); - return comma > 0 ? ip.substring(0, comma).trim() : ip.trim(); - } - ip = request.getHeader("X-Real-IP"); - if (StrUtil.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) { - return ip.trim(); - } - return request.getRemoteAddr(); - } -} diff --git a/src/main/java/com/youlai/boot/auth/qrcode/model/QrCodeLoginStatusEnum.java b/src/main/java/com/youlai/boot/auth/qrcode/model/QrCodeLoginStatusEnum.java deleted file mode 100644 index 1f993c6e..00000000 --- a/src/main/java/com/youlai/boot/auth/qrcode/model/QrCodeLoginStatusEnum.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.youlai.boot.auth.qrcode.model; - -/** - * 扫码登录票据状态。 - *

    - * 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 redisTemplate; + private final CaptchaService captchaService; /** - * 账号密码登录 + * 用户名密码登录 * * @param username 用户名 * @param password 密码 - * @return 认证令牌 + * @return 访问令牌 */ - AuthenticationToken login(String username, String password); + public AuthenticationToken login(String username, String password) { + // 1. 创建用于密码认证的令牌(未认证) + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(username.trim(), password); + + // 2. 执行认证(认证中) + // 说明:这里的认证流程由 Spring Security 提供的 AuthenticationManager 执行。 + // 默认情况下会委托给 DaoAuthenticationProvider: + // 1) retrieveUser(...):内部通过 UserDetailsService.loadUserByUsername(...) 获取用户信息(本项目为 SecurityUserDetailsService 实现) + // 2) additionalAuthenticationChecks(...):对比请求密码与用户存储密码(由 PasswordEncoder 完成匹配) + // 认证通过后返回已认证的 Authentication(principal 为 SecurityUserDetails,authorities 为角色/权限集合)。 + Authentication authentication = authenticationManager.authenticate(authenticationToken); + + // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) + AuthenticationToken authenticationTokenResponse = + tokenManager.generateToken(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + return authenticationTokenResponse; + } + + /** + * 发送登录短信验证码 + * + * @param mobile 手机号 + */ + public void sendSmsCode(String mobile) { + + // 随机生成4位验证码 + // String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000)); + // TODO 为了方便测试,验证码固定为 1234,实际开发中在配置了厂商短信服务后,可以使用上面的随机验证码 + String code = "1234"; + + // 发送短信验证码 + Map templateParams = new HashMap<>(); + templateParams.put("code", code); + try { + smsService.send(mobile, SmsTypeEnum.LOGIN, templateParams); + } catch (Exception e) { + log.error("发送短信验证码失败", e); + } + // 缓存验证码至Redis,用于登录校验 + redisTemplate.opsForValue().set(StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile), code, 5, TimeUnit.MINUTES); + } /** * 短信验证码登录 * * @param mobile 手机号 * @param code 验证码 - * @return 认证令牌 + * @return 访问令牌 */ - AuthenticationToken loginBySms(String mobile, String code); + public AuthenticationToken loginBySms(String mobile, String code) { + // 1. 创建用户短信验证码认证的令牌(未认证) + SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(mobile, code); + + // 2. 执行认证(认证中) + Authentication authentication = authenticationManager.authenticate(smsAuthenticationToken); + + // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) + AuthenticationToken authenticationToken = tokenManager.generateToken(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + return authenticationToken; + } /** - * 发送短信验证码 - * - * @param mobile 手机号 + * 注销登录 */ - void sendSmsCode(String mobile); - - /** - * 退出登录 - */ - void logout(); + public void logout() { + String token = SecurityUtils.getAccessToken(); + if (StrUtil.isNotBlank(token)) { + tokenManager.invalidateToken(token); + // 清除Security上下文 + SecurityContextHolder.clearContext(); + } + } /** * 获取验证码 */ - CaptchaInfo getCaptcha(); + public CaptchaInfo getCaptcha() { + return captchaService.generate(); + } /** - * 刷新令牌 + * 刷新token * * @param refreshToken 刷新令牌 - * @return 认证令牌 + * @return 新的访问令牌 */ - AuthenticationToken refreshToken(String refreshToken); + public AuthenticationToken refreshToken(String refreshToken) { + return tokenManager.refreshToken(refreshToken); + } + } diff --git a/src/main/java/com/youlai/boot/framework/captcha/service/CaptchaService.java b/src/main/java/com/youlai/boot/auth/service/CaptchaService.java similarity index 92% rename from src/main/java/com/youlai/boot/framework/captcha/service/CaptchaService.java rename to src/main/java/com/youlai/boot/auth/service/CaptchaService.java index 6e9d9859..4ce21b58 100644 --- a/src/main/java/com/youlai/boot/framework/captcha/service/CaptchaService.java +++ b/src/main/java/com/youlai/boot/auth/service/CaptchaService.java @@ -1,4 +1,4 @@ -package com.youlai.boot.framework.captcha.service; +package com.youlai.boot.auth.service; import cn.hutool.captcha.AbstractCaptcha; import cn.hutool.captcha.CaptchaUtil; @@ -6,11 +6,11 @@ import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.RedisConstants; -import com.youlai.boot.framework.captcha.enums.CaptchaTypeEnum; +import com.youlai.boot.auth.enums.CaptchaTypeEnum; import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.framework.captcha.config.CaptchaProperties; -import com.youlai.boot.framework.captcha.exception.CaptchaException; -import com.youlai.boot.framework.captcha.model.CaptchaInfo; +import com.youlai.boot.config.property.CaptchaProperties; +import com.youlai.boot.auth.exception.CaptchaException; +import com.youlai.boot.auth.model.resp.CaptchaInfo; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @@ -20,6 +20,9 @@ import java.util.concurrent.TimeUnit; /** * 验证码服务 + * + * @author Ray.Hao + * @since 3.1.0 */ @Service @RequiredArgsConstructor diff --git a/src/main/java/com/youlai/boot/auth/qrcode/service/impl/QrCodeLoginServiceImpl.java b/src/main/java/com/youlai/boot/auth/service/QrCodeLoginService.java similarity index 70% rename from src/main/java/com/youlai/boot/auth/qrcode/service/impl/QrCodeLoginServiceImpl.java rename to src/main/java/com/youlai/boot/auth/service/QrCodeLoginService.java index 9a488c60..cd807d75 100644 --- a/src/main/java/com/youlai/boot/auth/qrcode/service/impl/QrCodeLoginServiceImpl.java +++ b/src/main/java/com/youlai/boot/auth/service/QrCodeLoginService.java @@ -1,16 +1,14 @@ -package com.youlai.boot.auth.qrcode.service.impl; +package com.youlai.boot.auth.service; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; -import com.youlai.boot.auth.qrcode.model.QrCodeLoginContext; -import com.youlai.boot.auth.qrcode.model.QrCodeLoginStatusEnum; -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.auth.qrcode.util.QrCodeNicknameMasker; +import com.youlai.boot.auth.enums.QrCodeLoginStatusEnum; +import com.youlai.boot.auth.model.dto.QrCodeLoginContext; +import com.youlai.boot.auth.model.resp.QrCodeGenerateResp; +import com.youlai.boot.auth.model.resp.QrCodeStatusResp; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.framework.security.exception.TokenInvalidException; +import com.youlai.boot.common.exception.TokenInvalidException; import com.youlai.boot.framework.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.SecurityUser; import com.youlai.boot.framework.security.model.SecurityUserDetails; @@ -27,23 +25,20 @@ import tools.jackson.databind.json.JsonMapper; import java.util.concurrent.TimeUnit; /** - * 扫码登录服务实现。 - *

    - * 票据上下文以 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 redisTemplate; /** * 静默登录 @@ -21,21 +59,61 @@ public interface WxMaAuthService { * @param code 微信登录凭证(wx.login 获取) * @return 登录结果(成功返回 token,需绑定返回 openid) */ - WxMaLoginVO silentLogin(String code); + public WxMaLoginResp silentLogin(String code) { + WxMaAuthenticationToken token = new WxMaAuthenticationToken(code); + + try { + Authentication authentication = authenticationManager.authenticate(token); + AuthenticationToken authToken = tokenManager.generateToken(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + return WxMaLoginResp.builder() + .isNewUser(false) + .needBindMobile(false) + .accessToken(authToken.getAccessToken()) + .refreshToken(authToken.getRefreshToken()) + .tokenType(authToken.getTokenType()) + .expiresIn(authToken.getExpiresIn()) + .build(); + } catch (MobileNotBoundException e) { + return WxMaLoginResp.builder() + .isNewUser(true) + .needBindMobile(true) + .openid(e.getOpenid()) + .build(); + } + } /** * 手机号快捷登录 *

    * 同时使用微信登录凭证和手机号授权凭证, * 一步完成用户注册/登录,无需额外绑定流程。 - * 适用于企业认证的小程序(已开通手机号快捷登录权限)。 *

    * * @param loginCode 微信登录凭证(wx.login 获取) * @param phoneCode 手机号授权凭证(getPhoneNumber 事件获取) * @return 认证令牌 */ - AuthenticationToken phoneLogin(String loginCode, String phoneCode); + @Transactional(rollbackFor = Exception.class) + public AuthenticationToken phoneLogin(String loginCode, String phoneCode) { + // 1. 解析微信登录凭证,获取会话信息 + WxMaJscode2SessionResult session = resolveSession(loginCode); + String openid = session.getOpenid(); + + // 2. 解析手机号授权凭证,获取手机号 + String mobile = resolvePhoneNumber(phoneCode); + + log.info("微信小程序手机号快捷登录:openid={}, mobile={}", openid, mobile); + + // 3. 查询或创建用户 + SysUser user = findOrCreateUser(mobile); + + // 4. 绑定微信 openid + bindWechatOpenid(user, session); + + // 5. 生成认证令牌 + return generateAuthToken(mobile); + } /** * 绑定手机号 @@ -49,5 +127,139 @@ public interface WxMaAuthService { * @param smsCode 短信验证码 * @return 认证令牌 */ - AuthenticationToken bindMobile(String openid, String mobile, String smsCode); + @Transactional(rollbackFor = Exception.class) + public AuthenticationToken bindMobile(String openid, String mobile, String smsCode) { + // 1. 验证短信验证码 + validateSmsCode(mobile, smsCode); + + // 2. 查询或创建用户 + SysUser user = findOrCreateUser(mobile); + + // 3. 绑定微信 openid + userSocialService.bindOrUpdate( + user.getId(), + SocialPlatformEnum.WECHAT_MINI, + openid, + null, null, null, null + ); + + log.info("微信小程序绑定手机号成功:mobile={}, openid={}", mobile, openid); + + // 4. 生成认证令牌 + return generateAuthToken(mobile); + } + + // ==================== 私有方法 ==================== + + /** + * 解析微信登录凭证,获取会话信息 + */ + private WxMaJscode2SessionResult resolveSession(String loginCode) { + try { + return wxMaService.jsCode2SessionInfo(loginCode); + } catch (Exception e) { + log.error("获取微信会话信息失败,loginCode={}", loginCode, e); + throw new IllegalArgumentException("微信登录失败:" + e.getMessage()); + } + } + + /** + * 解析手机号授权凭证,获取手机号 + */ + private String resolvePhoneNumber(String phoneCode) { + try { + WxMaPhoneNumberInfo phoneInfo = wxMaService.getUserService().getPhoneNoInfo(phoneCode); + return phoneInfo.getPhoneNumber(); + } catch (Exception e) { + log.error("获取微信手机号失败,phoneCode={}", phoneCode, e); + throw new IllegalArgumentException("获取手机号失败:" + e.getMessage()); + } + } + + /** + * 查询或创建用户 + */ + private SysUser findOrCreateUser(String mobile) { + SysUser user = userService.lambdaQuery() + .eq(SysUser::getMobile, mobile) + .one(); + + if (user == null) { + user = createNewUser(mobile); + log.info("微信小程序登录:创建新用户,mobile={}, userId={}", mobile, user.getId()); + } + + return user; + } + + /** + * 创建新用户 + *

    + * 新用户默认分配 GUEST(访问游客)角色 + *

    + */ + private SysUser createNewUser(String mobile) { + SysUser user = new SysUser(); + user.setMobile(mobile); + user.setUsername("wx_" + IdUtil.fastSimpleUUID().substring(0, 8)); + user.setNickname("微信用户"); + user.setStatus(1); + user.setIsDeleted(0); + user.setCreateTime(LocalDateTime.now()); + user.setUpdateTime(LocalDateTime.now()); + userService.save(user); + + // 分配 GUEST 角色(角色ID=3) + userRoleService.saveUserRoles(user.getId(), Collections.singletonList(3L)); + + return user; + } + + /** + * 绑定微信 openid + */ + private void bindWechatOpenid(SysUser user, WxMaJscode2SessionResult session) { + try { + userSocialService.bindOrUpdate( + user.getId(), + SocialPlatformEnum.WECHAT_MINI, + session.getOpenid(), + session.getUnionid(), + user.getNickname(), + user.getAvatar(), + session.getSessionKey() + ); + } catch (Exception e) { + // 绑定失败不影响登录 + log.warn("绑定微信 openid 失败,userId={}, openid={}", user.getId(), session.getOpenid(), e); + } + } + + /** + * 验证短信验证码 + */ + private void validateSmsCode(String mobile, String smsCode) { + String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile); + String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey); + + if (!StrUtil.equals(smsCode, cachedCode)) { + throw new IllegalArgumentException("验证码错误"); + } + + // 验证成功后删除验证码 + redisTemplate.delete(cacheKey); + } + + /** + * 生成认证令牌 + */ + private AuthenticationToken generateAuthToken(String mobile) { + SecurityUserDetails userDetails = new SecurityUserDetails(userService.getAuthInfoByMobile(mobile)); + Authentication authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities() + ); + AuthenticationToken authToken = tokenManager.generateToken(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + return authToken; + } } diff --git a/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java deleted file mode 100644 index a2bc3c44..00000000 --- a/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.youlai.boot.auth.service.impl; - -import cn.hutool.core.util.StrUtil; -import com.youlai.boot.auth.service.AuthService; -import com.youlai.boot.common.constant.RedisConstants; -import com.youlai.boot.framework.captcha.model.CaptchaInfo; -import com.youlai.boot.framework.captcha.service.CaptchaService; -import com.youlai.boot.framework.security.model.AuthenticationToken; -import com.youlai.boot.framework.security.token.TokenManager; -import com.youlai.boot.auth.security.model.SmsAuthenticationToken; -import com.youlai.boot.framework.security.util.SecurityUtils; -import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum; -import com.youlai.boot.framework.integration.sms.service.SmsService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -/** - * 认证服务实现类 - * - * @author Ray.Hao - * @since 2.4.0 - */ -@Service -@RequiredArgsConstructor -@Slf4j -public class AuthServiceImpl implements AuthService { - - private final AuthenticationManager authenticationManager; - private final TokenManager tokenManager; - - private final SmsService smsService; - private final RedisTemplate redisTemplate; - private final CaptchaService captchaService; - - /** - * 用户名密码登录 - * - * @param username 用户名 - * @param password 密码 - * @return 访问令牌 - */ - @Override - public AuthenticationToken login(String username, String password) { - // 1. 创建用于密码认证的令牌(未认证) - UsernamePasswordAuthenticationToken authenticationToken = - new UsernamePasswordAuthenticationToken(username.trim(), password); - - // 2. 执行认证(认证中) - // 说明:这里的认证流程由 Spring Security 提供的 AuthenticationManager 执行。 - // 默认情况下会委托给 DaoAuthenticationProvider: - // 1) retrieveUser(...):内部通过 UserDetailsService.loadUserByUsername(...) 获取用户信息(本项目为 SecurityUserDetailsService 实现) - // 2) additionalAuthenticationChecks(...):对比请求密码与用户存储密码(由 PasswordEncoder 完成匹配) - // 认证通过后返回已认证的 Authentication(principal 为 SecurityUserDetails,authorities 为角色/权限集合)。 - Authentication authentication = authenticationManager.authenticate(authenticationToken); - - // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) - AuthenticationToken authenticationTokenResponse = - tokenManager.generateToken(authentication); - SecurityContextHolder.getContext().setAuthentication(authentication); - return authenticationTokenResponse; - } - - /** - * 发送登录短信验证码 - * - * @param mobile 手机号 - */ - @Override - public void sendSmsCode(String mobile) { - - // 随机生成4位验证码 - // String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000)); - // TODO 为了方便测试,验证码固定为 1234,实际开发中在配置了厂商短信服务后,可以使用上面的随机验证码 - String code = "1234"; - - // 发送短信验证码 - Map templateParams = new HashMap<>(); - templateParams.put("code", code); - try { - smsService.sendSms(mobile, SmsTypeEnum.LOGIN, templateParams); - } catch (Exception e) { - log.error("发送短信验证码失败", e); - } - // 缓存验证码至Redis,用于登录校验 - redisTemplate.opsForValue().set(StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile), code, 5, TimeUnit.MINUTES); - } - - /** - * 短信验证码登录 - * - * @param mobile 手机号 - * @param code 验证码 - * @return 访问令牌 - */ - @Override - public AuthenticationToken loginBySms(String mobile, String code) { - // 1. 创建用户短信验证码认证的令牌(未认证) - SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(mobile, code); - - // 2. 执行认证(认证中) - Authentication authentication = authenticationManager.authenticate(smsAuthenticationToken); - - // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) - AuthenticationToken authenticationToken = tokenManager.generateToken(authentication); - SecurityContextHolder.getContext().setAuthentication(authentication); - return authenticationToken; - } - - /** - * 注销登录 - */ - @Override - public void logout() { - String token = SecurityUtils.getAccessToken(); - if (StrUtil.isNotBlank(token)) { - tokenManager.invalidateToken(token); - // 清除Security上下文 - SecurityContextHolder.clearContext(); - } - } - - /** - * 获取验证码 - */ - @Override - public CaptchaInfo getCaptcha() { - return captchaService.generate(); - } - - /** - * 刷新token - * - * @param refreshToken 刷新令牌 - * @return 新的访问令牌 - */ - @Override - public AuthenticationToken refreshToken(String refreshToken) { - return tokenManager.refreshToken(refreshToken); - } - -} diff --git a/src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java b/src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java deleted file mode 100644 index 439cb329..00000000 --- a/src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java +++ /dev/null @@ -1,245 +0,0 @@ -package com.youlai.boot.auth.service.impl; - -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.vo.WxMaLoginVO; -import com.youlai.boot.auth.service.WxMaAuthService; -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.auth.security.exception.MobileNotBoundException; -import com.youlai.boot.auth.security.model.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 4.0.0 - */ -@Service -@RequiredArgsConstructor -@Slf4j -public class WxMaAuthServiceImpl implements 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 redisTemplate; - - /** - * 静默登录 - */ - @Override - public WxMaLoginVO silentLogin(String code) { - WxMaAuthenticationToken token = new WxMaAuthenticationToken(code); - - try { - Authentication authentication = authenticationManager.authenticate(token); - AuthenticationToken authToken = tokenManager.generateToken(authentication); - SecurityContextHolder.getContext().setAuthentication(authentication); - return WxMaLoginVO.builder() - .isNewUser(false) - .needBindMobile(false) - .accessToken(authToken.getAccessToken()) - .refreshToken(authToken.getRefreshToken()) - .tokenType(authToken.getTokenType()) - .expiresIn(authToken.getExpiresIn()) - .build(); - } catch (MobileNotBoundException e) { - return WxMaLoginVO.builder() - .isNewUser(true) - .needBindMobile(true) - .openid(e.getOpenid()) - .build(); - } - } - - /** - * 手机号快捷登录 - */ - @Override - @Transactional(rollbackFor = Exception.class) - public AuthenticationToken phoneLogin(String loginCode, String phoneCode) { - // 1. 解析微信登录凭证,获取会话信息 - WxMaJscode2SessionResult session = resolveSession(loginCode); - String openid = session.getOpenid(); - - // 2. 解析手机号授权凭证,获取手机号 - String mobile = resolvePhoneNumber(phoneCode); - - log.info("微信小程序手机号快捷登录:openid={}, mobile={}", openid, mobile); - - // 3. 查询或创建用户 - SysUser user = findOrCreateUser(mobile); - - // 4. 绑定微信 openid - bindWechatOpenid(user, session); - - // 5. 生成认证令牌 - return generateAuthToken(mobile); - } - - /** - * 绑定手机号 - */ - @Override - @Transactional(rollbackFor = Exception.class) - public AuthenticationToken bindMobile(String openid, String mobile, String smsCode) { - // 1. 验证短信验证码 - validateSmsCode(mobile, smsCode); - - // 2. 查询或创建用户 - SysUser user = findOrCreateUser(mobile); - - // 3. 绑定微信 openid - userSocialService.bindOrUpdate( - user.getId(), - SocialPlatformEnum.WECHAT_MINI, - openid, - null, null, null, null - ); - - log.info("微信小程序绑定手机号成功:mobile={}, openid={}", mobile, openid); - - // 4. 生成认证令牌 - return generateAuthToken(mobile); - } - - // ==================== 私有方法 ==================== - - /** - * 解析微信登录凭证,获取会话信息 - */ - private WxMaJscode2SessionResult resolveSession(String loginCode) { - try { - return wxMaService.jsCode2SessionInfo(loginCode); - } catch (Exception e) { - log.error("获取微信会话信息失败,loginCode={}", loginCode, e); - throw new IllegalArgumentException("微信登录失败:" + e.getMessage()); - } - } - - /** - * 解析手机号授权凭证,获取手机号 - */ - private String resolvePhoneNumber(String phoneCode) { - try { - WxMaPhoneNumberInfo phoneInfo = wxMaService.getUserService().getPhoneNoInfo(phoneCode); - return phoneInfo.getPhoneNumber(); - } catch (Exception e) { - log.error("获取微信手机号失败,phoneCode={}", phoneCode, e); - throw new IllegalArgumentException("获取手机号失败:" + e.getMessage()); - } - } - - /** - * 查询或创建用户 - */ - private SysUser findOrCreateUser(String mobile) { - SysUser user = userService.lambdaQuery() - .eq(SysUser::getMobile, mobile) - .one(); - - if (user == null) { - user = createNewUser(mobile); - log.info("微信小程序登录:创建新用户,mobile={}, userId={}", mobile, user.getId()); - } - - return user; - } - - /** - * 创建新用户 - *

    - * 新用户默认分配 GUEST(访问游客)角色 - *

    - */ - private SysUser createNewUser(String mobile) { - SysUser user = new SysUser(); - user.setMobile(mobile); - user.setUsername("wx_" + IdUtil.fastSimpleUUID().substring(0, 8)); - user.setNickname("微信用户"); - user.setStatus(1); - user.setIsDeleted(0); - user.setCreateTime(LocalDateTime.now()); - user.setUpdateTime(LocalDateTime.now()); - userService.save(user); - - // 分配 GUEST 角色(角色ID=3) - userRoleService.saveUserRoles(user.getId(), Collections.singletonList(3L)); - - return user; - } - - /** - * 绑定微信 openid - */ - private void bindWechatOpenid(SysUser user, WxMaJscode2SessionResult session) { - try { - userSocialService.bindOrUpdate( - user.getId(), - SocialPlatformEnum.WECHAT_MINI, - session.getOpenid(), - session.getUnionid(), - user.getNickname(), - user.getAvatar(), - session.getSessionKey() - ); - } catch (Exception e) { - // 绑定失败不影响登录 - log.warn("绑定微信 openid 失败,userId={}, openid={}", user.getId(), session.getOpenid(), e); - } - } - - /** - * 验证短信验证码 - */ - private void validateSmsCode(String mobile, String smsCode) { - String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile); - String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey); - - if (!StrUtil.equals(smsCode, cachedCode)) { - throw new IllegalArgumentException("验证码错误"); - } - - // 验证成功后删除验证码 - redisTemplate.delete(cacheKey); - } - - /** - * 生成认证令牌 - */ - private AuthenticationToken generateAuthToken(String mobile) { - SecurityUserDetails userDetails = new SecurityUserDetails(userService.getAuthInfoByMobile(mobile)); - Authentication authentication = new UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities() - ); - AuthenticationToken authToken = tokenManager.generateToken(authentication); - SecurityContextHolder.getContext().setAuthentication(authentication); - return authToken; - } -} diff --git a/src/main/java/com/youlai/boot/codegen/controller/CodegenController.java b/src/main/java/com/youlai/boot/codegen/controller/CodegenController.java index e9746d53..7e589f1a 100644 --- a/src/main/java/com/youlai/boot/codegen/controller/CodegenController.java +++ b/src/main/java/com/youlai/boot/codegen/controller/CodegenController.java @@ -11,7 +11,7 @@ import com.youlai.boot.codegen.model.form.GenConfigForm; import com.youlai.boot.codegen.model.query.TableQuery; import com.youlai.boot.codegen.model.vo.CodegenPreviewVO; import com.youlai.boot.codegen.model.vo.TablePageVO; -import com.youlai.boot.framework.annotation.Log; +import com.youlai.boot.common.annotation.Log; import com.youlai.boot.codegen.service.GenTableService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; diff --git a/src/main/java/com/youlai/boot/codegen/model/entity/GenTable.java b/src/main/java/com/youlai/boot/codegen/model/entity/GenTable.java index 188bbd26..51f5f289 100644 --- a/src/main/java/com/youlai/boot/codegen/model/entity/GenTable.java +++ b/src/main/java/com/youlai/boot/codegen/model/entity/GenTable.java @@ -2,7 +2,7 @@ package com.youlai.boot.codegen.model.entity; import com.baomidou.mybatisplus.annotation.*; -import com.youlai.boot.framework.base.BaseEntity; +import com.youlai.boot.common.base.BaseEntity; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/com/youlai/boot/codegen/model/entity/GenTableColumn.java b/src/main/java/com/youlai/boot/codegen/model/entity/GenTableColumn.java index fdebd6d4..a2640747 100644 --- a/src/main/java/com/youlai/boot/codegen/model/entity/GenTableColumn.java +++ b/src/main/java/com/youlai/boot/codegen/model/entity/GenTableColumn.java @@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.youlai.boot.framework.base.BaseEntity; +import com.youlai.boot.common.base.BaseEntity; import com.youlai.boot.codegen.enums.FormTypeEnum; import com.youlai.boot.codegen.enums.QueryTypeEnum; import lombok.Getter; diff --git a/src/main/java/com/youlai/boot/framework/annotation/DataPermission.java b/src/main/java/com/youlai/boot/common/annotation/DataPermission.java similarity index 91% rename from src/main/java/com/youlai/boot/framework/annotation/DataPermission.java rename to src/main/java/com/youlai/boot/common/annotation/DataPermission.java index d3fd0b4c..ba266d01 100644 --- a/src/main/java/com/youlai/boot/framework/annotation/DataPermission.java +++ b/src/main/java/com/youlai/boot/common/annotation/DataPermission.java @@ -1,4 +1,4 @@ -package com.youlai.boot.framework.annotation; +package com.youlai.boot.common.annotation; import java.lang.annotation.*; diff --git a/src/main/java/com/youlai/boot/framework/annotation/Log.java b/src/main/java/com/youlai/boot/common/annotation/Log.java similarity index 95% rename from src/main/java/com/youlai/boot/framework/annotation/Log.java rename to src/main/java/com/youlai/boot/common/annotation/Log.java index 16e9ec06..c02d5608 100644 --- a/src/main/java/com/youlai/boot/framework/annotation/Log.java +++ b/src/main/java/com/youlai/boot/common/annotation/Log.java @@ -1,4 +1,4 @@ -package com.youlai.boot.framework.annotation; +package com.youlai.boot.common.annotation; import com.youlai.boot.common.enums.ActionTypeEnum; import com.youlai.boot.common.enums.LogModuleEnum; diff --git a/src/main/java/com/youlai/boot/framework/annotation/RateLimit.java b/src/main/java/com/youlai/boot/common/annotation/RateLimit.java similarity index 93% rename from src/main/java/com/youlai/boot/framework/annotation/RateLimit.java rename to src/main/java/com/youlai/boot/common/annotation/RateLimit.java index 1605d977..e7e73424 100644 --- a/src/main/java/com/youlai/boot/framework/annotation/RateLimit.java +++ b/src/main/java/com/youlai/boot/common/annotation/RateLimit.java @@ -1,4 +1,4 @@ -package com.youlai.boot.framework.annotation; +package com.youlai.boot.common.annotation; import java.lang.annotation.*; diff --git a/src/main/java/com/youlai/boot/framework/annotation/RepeatSubmit.java b/src/main/java/com/youlai/boot/common/annotation/RepeatSubmit.java similarity index 90% rename from src/main/java/com/youlai/boot/framework/annotation/RepeatSubmit.java rename to src/main/java/com/youlai/boot/common/annotation/RepeatSubmit.java index f489a982..053f1320 100644 --- a/src/main/java/com/youlai/boot/framework/annotation/RepeatSubmit.java +++ b/src/main/java/com/youlai/boot/common/annotation/RepeatSubmit.java @@ -1,4 +1,4 @@ -package com.youlai.boot.framework.annotation; +package com.youlai.boot.common.annotation; import java.lang.annotation.*; diff --git a/src/main/java/com/youlai/boot/framework/annotation/ValidField.java b/src/main/java/com/youlai/boot/common/annotation/ValidField.java similarity index 93% rename from src/main/java/com/youlai/boot/framework/annotation/ValidField.java rename to src/main/java/com/youlai/boot/common/annotation/ValidField.java index b7be969d..559b06e1 100644 --- a/src/main/java/com/youlai/boot/framework/annotation/ValidField.java +++ b/src/main/java/com/youlai/boot/common/annotation/ValidField.java @@ -1,4 +1,4 @@ -package com.youlai.boot.framework.annotation; +package com.youlai.boot.common.annotation; import com.youlai.boot.common.validator.FieldValidator; import jakarta.validation.Constraint; diff --git a/src/main/java/com/youlai/boot/framework/web/aspect/LogAspect.java b/src/main/java/com/youlai/boot/common/aspect/LogAspect.java similarity index 90% rename from src/main/java/com/youlai/boot/framework/web/aspect/LogAspect.java rename to src/main/java/com/youlai/boot/common/aspect/LogAspect.java index 00093330..779d8b94 100644 --- a/src/main/java/com/youlai/boot/framework/web/aspect/LogAspect.java +++ b/src/main/java/com/youlai/boot/common/aspect/LogAspect.java @@ -1,9 +1,9 @@ -package com.youlai.boot.framework.web.aspect; +package com.youlai.boot.common.aspect; import cn.hutool.core.util.StrUtil; import cn.hutool.http.useragent.UserAgent; import cn.hutool.http.useragent.UserAgentUtil; -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.util.IPUtils; @@ -37,6 +37,8 @@ import java.util.concurrent.Executor; @Slf4j public class LogAspect { + private static final int MAX_ERROR_MSG_LENGTH = 1000; + private final LogService logService; private final Executor operationLogExecutor; @@ -66,10 +68,6 @@ public class LogAspect { throw e; } finally { long elapsed = System.currentTimeMillis() - start; - if (userId == null) { - userId = SecurityUtils.getUserId(); - username = SecurityUtils.getUsername(); - } try { SysLog entity = buildEntity(logAnnotation, elapsed, failure, userId, username); if (entity != null) { @@ -94,7 +92,6 @@ public class LogAspect { HttpServletRequest req = attrs.getRequest(); UserAgent ua = UserAgentUtil.parse(req.getHeader("User-Agent")); String ip = IPUtils.getIpAddr(req); - IpRegion region = parseRegion(IPUtils.getRegion(ip)); LogModuleEnum module = ann.module(); ActionTypeEnum action = ann.value(); @@ -109,25 +106,34 @@ public class LogAspect { entity.setRequestUri(req.getRequestURI()); entity.setRequestMethod(req.getMethod()); entity.setIp(ip); - entity.setProvince(region.province); - entity.setCity(region.city); entity.setDevice(getDevice(ua)); entity.setOs(getOs(ua)); entity.setBrowser(getBrowser(ua)); entity.setStatus(failure == null ? 1 : 0); - entity.setErrorMsg(failure == null ? null : failure.getMessage()); + entity.setErrorMsg(failure == null ? null : truncate(failure.getMessage(), MAX_ERROR_MSG_LENGTH)); entity.setExecutionTime((int) Math.min(elapsed, Integer.MAX_VALUE)); entity.setCreateTime(LocalDateTime.now()); + + IpRegion region = parseRegion(ip); + entity.setProvince(region.province); + entity.setCity(region.city); + return entity; } - private IpRegion parseRegion(String region) { + private IpRegion parseRegion(String ip) { + String region = IPUtils.getRegion(ip); if (StrUtil.isBlank(region)) return IpRegion.EMPTY; String[] parts = region.split("\\|"); if (parts.length < 4) return IpRegion.EMPTY; return new IpRegion(StrUtil.blankToDefault(parts[2], null), StrUtil.blankToDefault(parts[3], null)); } + private String truncate(String msg, int maxLen) { + if (msg == null) return null; + return msg.length() <= maxLen ? msg : msg.substring(0, maxLen); + } + private String getOs(UserAgent ua) { return Optional.ofNullable(ua).map(UserAgent::getOs).map(os -> os.getName()).orElse(null); } diff --git a/src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java b/src/main/java/com/youlai/boot/common/aspect/RateLimitAspect.java similarity index 94% rename from src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java rename to src/main/java/com/youlai/boot/common/aspect/RateLimitAspect.java index a58fe6e2..b11b5377 100644 --- a/src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java +++ b/src/main/java/com/youlai/boot/common/aspect/RateLimitAspect.java @@ -1,14 +1,14 @@ -package com.youlai.boot.framework.web.aspect; +package com.youlai.boot.common.aspect; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; -import com.youlai.boot.framework.annotation.RateLimit; +import com.youlai.boot.common.annotation.RateLimit; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.util.IPUtils; -import com.youlai.boot.framework.web.config.RateLimitProperties; -import com.youlai.boot.framework.web.exception.RateLimitException; +import com.youlai.boot.config.property.RateLimitProperties; +import com.youlai.boot.common.exception.RateLimitException; import com.youlai.boot.framework.web.ratelimit.SlidingWindowScript; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/src/main/java/com/youlai/boot/framework/web/aspect/RepeatSubmitAspect.java b/src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java similarity index 97% rename from src/main/java/com/youlai/boot/framework/web/aspect/RepeatSubmitAspect.java rename to src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java index dbea1303..976c0f5d 100644 --- a/src/main/java/com/youlai/boot/framework/web/aspect/RepeatSubmitAspect.java +++ b/src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java @@ -1,4 +1,4 @@ -package com.youlai.boot.framework.web.aspect; +package com.youlai.boot.common.aspect; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; @@ -7,7 +7,7 @@ import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.exception.BusinessException; -import com.youlai.boot.framework.annotation.RepeatSubmit; +import com.youlai.boot.common.annotation.RepeatSubmit; import com.youlai.boot.common.util.IPUtils; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/youlai/boot/framework/base/BaseEntity.java b/src/main/java/com/youlai/boot/common/base/BaseEntity.java similarity index 96% rename from src/main/java/com/youlai/boot/framework/base/BaseEntity.java rename to src/main/java/com/youlai/boot/common/base/BaseEntity.java index 7b3fb9b5..67f898f6 100644 --- a/src/main/java/com/youlai/boot/framework/base/BaseEntity.java +++ b/src/main/java/com/youlai/boot/common/base/BaseEntity.java @@ -1,4 +1,4 @@ -package com.youlai.boot.framework.base; +package com.youlai.boot.common.base; import com.baomidou.mybatisplus.annotation.FieldFill; import com.baomidou.mybatisplus.annotation.IdType; diff --git a/src/main/java/com/youlai/boot/common/base/BaseQuery.java b/src/main/java/com/youlai/boot/common/base/BaseQuery.java index 40b6a78e..7548937d 100644 --- a/src/main/java/com/youlai/boot/common/base/BaseQuery.java +++ b/src/main/java/com/youlai/boot/common/base/BaseQuery.java @@ -1,6 +1,6 @@ package com.youlai.boot.common.base; -import com.youlai.boot.framework.annotation.ValidField; +import com.youlai.boot.common.annotation.ValidField; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/src/main/java/com/youlai/boot/common/enums/SocialPlatformEnum.java b/src/main/java/com/youlai/boot/common/enums/SocialPlatformEnum.java index c6e85c13..973b57f4 100644 --- a/src/main/java/com/youlai/boot/common/enums/SocialPlatformEnum.java +++ b/src/main/java/com/youlai/boot/common/enums/SocialPlatformEnum.java @@ -5,7 +5,12 @@ import com.youlai.boot.common.base.IBaseEnum; import lombok.Getter; /** - * 第三方登录平台类型枚举 + * 第三方登录平台类型枚举。 + *

    + * value 作为 MyBatis-Plus 枚举值持久化到数据库。 + * + * @author Ray.Hao + * @since 2.17.0 */ @Getter public enum SocialPlatformEnum implements IBaseEnum { diff --git a/src/main/java/com/youlai/boot/auth/security/exception/MobileNotBoundException.java b/src/main/java/com/youlai/boot/common/exception/MobileNotBoundException.java similarity index 70% rename from src/main/java/com/youlai/boot/auth/security/exception/MobileNotBoundException.java rename to src/main/java/com/youlai/boot/common/exception/MobileNotBoundException.java index 099b9ee3..48c50ac3 100644 --- a/src/main/java/com/youlai/boot/auth/security/exception/MobileNotBoundException.java +++ b/src/main/java/com/youlai/boot/common/exception/MobileNotBoundException.java @@ -1,9 +1,14 @@ -package com.youlai.boot.auth.security.exception; +package com.youlai.boot.common.exception; import org.springframework.security.core.AuthenticationException; /** - * 需要绑定手机号异常(微信小程序登录未绑定手机号时抛出)。 + * 微信小程序登录时手机号未绑定异常。 + *

    + * 携带 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 templateParams); -} diff --git a/src/main/java/com/youlai/boot/framework/job/handler/XxlJobSampleHandler.java b/src/main/java/com/youlai/boot/framework/job/handler/XxlJobSampleHandler.java deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java b/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java index 74d6ad33..bc2a48b9 100644 --- a/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java +++ b/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java @@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; -import com.youlai.boot.framework.annotation.DataPermission; +import com.youlai.boot.common.annotation.DataPermission; import com.youlai.boot.common.enums.DataScopeEnum; import com.youlai.boot.framework.security.model.RoleDataScope; import com.youlai.boot.framework.security.model.SecurityUserDetails; diff --git a/src/main/java/com/youlai/boot/framework/security/port/PermissionPort.java b/src/main/java/com/youlai/boot/framework/security/port/PermissionPort.java index 9417eded..c26f1af8 100644 --- a/src/main/java/com/youlai/boot/framework/security/port/PermissionPort.java +++ b/src/main/java/com/youlai/boot/framework/security/port/PermissionPort.java @@ -8,7 +8,7 @@ import java.util.Set; * 由 system 模块提供适配器实现,framework 层通过此接口获取角色权限集合, * 不直接依赖 system 模块的 {@code RoleMenuService}。 * - * @see com.youlai.boot.system.security.adapter.PermissionAdapter + * @see com.youlai.boot.system.security.port.PermissionAdapter */ public interface PermissionPort { diff --git a/src/main/java/com/youlai/boot/framework/security/port/UserAuthenticationPort.java b/src/main/java/com/youlai/boot/framework/security/port/UserAuthenticationPort.java index 82acea2c..29bdbf22 100644 --- a/src/main/java/com/youlai/boot/framework/security/port/UserAuthenticationPort.java +++ b/src/main/java/com/youlai/boot/framework/security/port/UserAuthenticationPort.java @@ -9,7 +9,7 @@ import com.youlai.boot.framework.security.model.SecurityUser; * 由 system 模块提供适配器实现,framework 层通过此接口获取认证数据, * 不直接依赖 system 模块的 {@code UserService} / {@code UserSocialService}。 * - * @see com.youlai.boot.system.security.adapter.UserAuthenticationAdapter + * @see com.youlai.boot.system.security.port.UserAuthenticationAdapter */ public interface UserAuthenticationPort { diff --git a/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java b/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java index bdd44dab..433322e5 100644 --- a/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java +++ b/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java @@ -13,8 +13,8 @@ import com.youlai.boot.common.constant.JwtClaimConstants; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.framework.security.config.SecurityProperties; -import com.youlai.boot.framework.security.exception.TokenInvalidException; +import com.youlai.boot.config.property.SecurityProperties; +import com.youlai.boot.common.exception.TokenInvalidException; import com.youlai.boot.framework.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.RoleDataScope; import com.youlai.boot.framework.security.model.SecurityUserDetails; diff --git a/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java b/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java index 661a8b46..07937d01 100644 --- a/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java +++ b/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java @@ -5,8 +5,8 @@ import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.framework.security.config.SecurityProperties; -import com.youlai.boot.framework.security.exception.TokenInvalidException; +import com.youlai.boot.config.property.SecurityProperties; +import com.youlai.boot.common.exception.TokenInvalidException; import com.youlai.boot.framework.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.SecurityUserDetails; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; diff --git a/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java b/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java index 89c044ec..751051e6 100644 --- a/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java +++ b/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java @@ -4,15 +4,17 @@ 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.framework.security.exception.TokenInvalidException; -import com.youlai.boot.framework.web.exception.RateLimitException; +import com.youlai.boot.common.exception.TokenInvalidException; +import com.youlai.boot.common.exception.RateLimitException; import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.TypeMismatchException; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.jdbc.BadSqlGrammarException; import org.springframework.security.access.AccessDeniedException; @@ -176,11 +178,15 @@ public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) - public Result handleException(Exception e) throws Exception { + public Result handleException(Exception e, HttpServletRequest request) throws Exception { if (e instanceof AccessDeniedException || e instanceof AuthenticationException) { throw e; } + String accept = request.getHeader("Accept"); + if (accept != null && accept.contains(MediaType.TEXT_EVENT_STREAM_VALUE)) { + throw e; + } log.error("unknown exception", e); return Result.failed(e.getLocalizedMessage()); } diff --git a/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java b/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java index b4e3f0a1..09219484 100644 --- a/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java +++ b/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java @@ -4,7 +4,7 @@ import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.util.IPUtils; -import com.youlai.boot.framework.web.config.RateLimitProperties; +import com.youlai.boot.config.property.RateLimitProperties; import com.youlai.boot.framework.web.ratelimit.SlidingWindowScript; import com.youlai.boot.framework.web.util.ResponseWriter; import jakarta.servlet.FilterChain; diff --git a/src/main/java/com/youlai/boot/message/job/OnlineUserCountTask.java b/src/main/java/com/youlai/boot/message/job/OnlineUserCountTask.java deleted file mode 100644 index 63dc9ac5..00000000 --- a/src/main/java/com/youlai/boot/message/job/OnlineUserCountTask.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.youlai.boot.message.job; - -import com.youlai.boot.message.registry.SseSessionRegistry; -import com.youlai.boot.message.service.SseService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Component; - -/** - * 在线用户统计 - *

    定时统计并广播当前在线用户数量

    - */ -@Component -@Slf4j -@RequiredArgsConstructor -public class OnlineUserCountTask { - - private final SseSessionRegistry sessionRegistry; - private final SseService sseService; - - @Scheduled(cron = "0 */3 * * * ?") - public void execute() { - int onlineCount = sessionRegistry.getOnlineUserCount(); - int connectionCount = sessionRegistry.getTotalConnectionCount(); - log.debug("在线用户数={} 总连接数={}", onlineCount, connectionCount); - sseService.sendOnlineCount(); - } -} diff --git a/src/main/java/com/youlai/boot/message/service/SseService.java b/src/main/java/com/youlai/boot/message/service/SseService.java deleted file mode 100644 index d4b59f26..00000000 --- a/src/main/java/com/youlai/boot/message/service/SseService.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.youlai.boot.message.service; - -import com.youlai.boot.message.dto.DictChangeEvent; -import com.youlai.boot.message.dto.OnlineUserDTO; -import com.youlai.boot.message.registry.SseSessionRegistry; -import com.youlai.boot.message.topic.SseTopics; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -/** - * SSE 服务 - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class SseService { - - /** SSE 连接超时时间:30 分钟 */ - private static final long TIMEOUT = 30 * 60 * 1000L; - - private final SseSessionRegistry sessionRegistry; - - /** - * 创建 SSE 连接 - * - * @param username 用户名 - * @return SseEmitter - */ - public SseEmitter createConnection(String username) { - if (username == null || username.isEmpty()) { - log.warn("创建SSE连接失败:用户名为空"); - return null; - } - - // 创建 SseEmitter,设置超时时间 - SseEmitter emitter = new SseEmitter(TIMEOUT); - - // 注册用户连接 - sessionRegistry.userConnected(username, emitter); - - // 连接建立后立即发送在线用户数 - try { - emitter.send(SseEmitter.event() - .name(SseTopics.ONLINE_COUNT) - .data(sessionRegistry.getOnlineUserCount())); - } catch (IOException e) { - log.warn("发送初始在线用户数失败: {}", e.getMessage()); - } - - log.info("用户[{}]SSE连接已建立,当前在线用户数: {}", username, sessionRegistry.getOnlineUserCount()); - - // 发送在线用户数变更 - sendOnlineCount(); - - return emitter; - } - - /** - * 发送字典变更事件 - * - * @param dictCode 字典编码 - */ - public void sendDictChange(String dictCode) { - if (dictCode == null || dictCode.isEmpty()) { - log.warn("字典编码为空,跳过发送"); - return; - } - - DictChangeEvent event = new DictChangeEvent(dictCode); - sessionRegistry.broadcast(SseTopics.DICT, event); - log.info("已发送字典变更通知: dictCode={}", dictCode); - } - - /** - * 发送在线用户数 - */ - public void sendOnlineCount() { - int count = sessionRegistry.getOnlineUserCount(); - sessionRegistry.broadcast(SseTopics.ONLINE_COUNT, count); - log.debug("已发送在线用户数: {}", count); - } - - /** - * 发送消息给指定用户 - * - * @param username 用户名 - * @param eventName 事件名称 - * @param data 数据 - */ - public void sendToUser(String username, String eventName, Object data) { - if (username == null || username.isEmpty()) { - log.warn("用户名为空,无法发送消息"); - return; - } - sessionRegistry.sendToUser(username, eventName, data); - log.info("已向用户[{}]发送事件[{}]", username, eventName); - } - - /** - * 获取在线用户列表 - * - * @return 在线用户列表 - */ - public List getOnlineUsers() { - return sessionRegistry.getOnlineUsers(); - } - - /** - * 获取在线用户数 - * - * @return 在线用户数 - */ - public int getOnlineUserCount() { - return sessionRegistry.getOnlineUserCount(); - } - - /** - * 发送系统消息 - * - * @param message 消息内容 - */ - public void sendSystemMessage(String message) { - if (message == null || message.isEmpty()) { - return; - } - Map systemMessage = Map.of( - "sender", "系统通知", - "content", message, - "timestamp", System.currentTimeMillis() - ); - sessionRegistry.broadcast(SseTopics.SYSTEM, systemMessage); - log.info("已发送系统消息: {}", message); - } -} diff --git a/src/main/java/com/youlai/boot/message/topic/SseTopics.java b/src/main/java/com/youlai/boot/message/topic/SseTopics.java deleted file mode 100644 index 0b48b51d..00000000 --- a/src/main/java/com/youlai/boot/message/topic/SseTopics.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.youlai.boot.message.topic; - -/** - * SSE 主题常量 - */ -public final class SseTopics { - - private SseTopics() { - } - - /** 字典变更事件 */ - public static final String DICT = "dict"; - - /** 在线用户数事件 */ - public static final String ONLINE_COUNT = "online-count"; - - /** 系统消息事件 */ - public static final String SYSTEM = "system"; -} diff --git a/src/main/java/com/youlai/boot/file/model/FileInfo.java b/src/main/java/com/youlai/boot/support/file/model/FileInfo.java similarity index 88% rename from src/main/java/com/youlai/boot/file/model/FileInfo.java rename to src/main/java/com/youlai/boot/support/file/model/FileInfo.java index 5be4f4ca..37e33cdf 100644 --- a/src/main/java/com/youlai/boot/file/model/FileInfo.java +++ b/src/main/java/com/youlai/boot/support/file/model/FileInfo.java @@ -1,9 +1,8 @@ -package com.youlai.boot.file.model; +package com.youlai.boot.support.file.model; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; - /** * 文件信息对象 * diff --git a/src/main/java/com/youlai/boot/file/service/FileService.java b/src/main/java/com/youlai/boot/support/file/service/FileService.java similarity index 76% rename from src/main/java/com/youlai/boot/file/service/FileService.java rename to src/main/java/com/youlai/boot/support/file/service/FileService.java index 6e5d2804..72cae226 100644 --- a/src/main/java/com/youlai/boot/file/service/FileService.java +++ b/src/main/java/com/youlai/boot/support/file/service/FileService.java @@ -1,10 +1,10 @@ -package com.youlai.boot.file.service; +package com.youlai.boot.support.file.service; -import com.youlai.boot.file.model.FileInfo; +import com.youlai.boot.support.file.model.FileInfo; import org.springframework.web.multipart.MultipartFile; /** - * 对象存储服务接口层 + * 文件存储服务接口 * * @author haoxr * @since 2022/11/19 @@ -13,6 +13,7 @@ public interface FileService { /** * 上传文件 + * * @param file 表单文件对象 * @return 文件信息 */ @@ -26,5 +27,4 @@ public interface FileService { */ boolean deleteFile(String filePath); - } diff --git a/src/main/java/com/youlai/boot/file/service/impl/AliyunFileServiceImpl.java b/src/main/java/com/youlai/boot/support/file/service/impl/AliyunFileServiceImpl.java similarity index 76% rename from src/main/java/com/youlai/boot/file/service/impl/AliyunFileServiceImpl.java rename to src/main/java/com/youlai/boot/support/file/service/impl/AliyunFileServiceImpl.java index 6d4e93e0..bc3b2a31 100644 --- a/src/main/java/com/youlai/boot/file/service/impl/AliyunFileServiceImpl.java +++ b/src/main/java/com/youlai/boot/support/file/service/impl/AliyunFileServiceImpl.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; @@ -8,8 +8,8 @@ import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.model.ObjectMetadata; import com.aliyun.oss.model.PutObjectRequest; -import com.youlai.boot.file.service.FileService; -import com.youlai.boot.file.model.FileInfo; +import com.youlai.boot.support.file.model.FileInfo; +import com.youlai.boot.support.file.service.FileService; import jakarta.annotation.PostConstruct; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -22,6 +22,12 @@ import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; import java.time.LocalDateTime; +/** + * 阿里云 OSS 文件存储服务。 + *

    + * 当 {@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}。 + *

      + *
    • 配置了 {@code file-storage.minio.domain} 时直接用域名拼接 URL
    • + *
    • 未配置则用 MinIO 预签名 URL(去掉 query 参数后的纯路径)
    • + *
    + */ @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 { } } + /** + * 删除文件。 + *

    + * 从完整 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 templateParams); + +} diff --git a/src/main/java/com/youlai/boot/framework/integration/sms/enums/SmsTypeEnum.java b/src/main/java/com/youlai/boot/support/sms/SmsTypeEnum.java similarity index 93% rename from src/main/java/com/youlai/boot/framework/integration/sms/enums/SmsTypeEnum.java rename to src/main/java/com/youlai/boot/support/sms/SmsTypeEnum.java index ea062a59..55154ffc 100644 --- a/src/main/java/com/youlai/boot/framework/integration/sms/enums/SmsTypeEnum.java +++ b/src/main/java/com/youlai/boot/support/sms/SmsTypeEnum.java @@ -1,4 +1,4 @@ -package com.youlai.boot.framework.integration.sms.enums; +package com.youlai.boot.support.sms; import com.youlai.boot.common.base.IBaseEnum; import lombok.Getter; diff --git a/src/main/java/com/youlai/boot/framework/integration/sms/service/impl/AliyunSmsService.java b/src/main/java/com/youlai/boot/support/sms/impl/AliyunSmsServiceImpl.java similarity index 58% rename from src/main/java/com/youlai/boot/framework/integration/sms/service/impl/AliyunSmsService.java rename to src/main/java/com/youlai/boot/support/sms/impl/AliyunSmsServiceImpl.java index 8053b8f2..24cf0623 100644 --- a/src/main/java/com/youlai/boot/framework/integration/sms/service/impl/AliyunSmsService.java +++ b/src/main/java/com/youlai/boot/support/sms/impl/AliyunSmsServiceImpl.java @@ -1,4 +1,4 @@ -package com.youlai.boot.framework.integration.sms.service.impl; +package com.youlai.boot.support.sms.impl; import cn.hutool.json.JSONUtil; import com.aliyuncs.CommonRequest; @@ -8,72 +8,56 @@ import com.aliyuncs.IAcsClient; import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.http.MethodType; import com.aliyuncs.profile.DefaultProfile; -import com.youlai.boot.framework.integration.sms.config.AliyunSmsProperties; -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.config.property.AliyunSmsProperties; +import com.youlai.boot.support.sms.SmsService; import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; import java.util.Map; /** - * 阿里云短信业务类 + * 阿里云短信服务实现 * * @author Ray - * @since 2024/8/17 + * @since 3.1.0 */ -@Service +@Slf4j +@Component +@ConditionalOnProperty(value = "sms.type", havingValue = "aliyun", matchIfMissing = true) @RequiredArgsConstructor -public class AliyunSmsService implements SmsService { +public class AliyunSmsServiceImpl implements SmsService { private final AliyunSmsProperties aliyunSmsProperties; - /** - * 发送短信验证码 - * - * @param mobile 手机号 13388886666 - * @param smsType 短信模板 SMS_194640010 - * @param templateParams 模板参数 [{"code":"123456"}] - * @return boolean 是否发送成功 - */ @Override - public boolean sendSms(String mobile, SmsTypeEnum smsType, Map templateParams) { - + public boolean send(String mobile, SmsTypeEnum smsType, Map templateParams) { + // 根据 smsType 从配置中获取对应的阿里云短信模板编码 String templateCode = aliyunSmsProperties.getTemplates().get(smsType.getValue()); DefaultProfile profile = DefaultProfile.getProfile(aliyunSmsProperties.getRegionId(), aliyunSmsProperties.getAccessKeyId(), aliyunSmsProperties.getAccessKeySecret()); IAcsClient client = new DefaultAcsClient(profile); - // 创建通用的请求对象 CommonRequest request = new CommonRequest(); - // 指定请求方式 request.setSysMethod(MethodType.POST); - // 短信api的请求地址(固定) request.setSysDomain(aliyunSmsProperties.getDomain()); - // 签名算法版(固定) request.setSysVersion("2017-05-25"); - // 请求 API 的名称(固定) request.setSysAction("SendSms"); - // 指定地域名称 request.putQueryParameter("RegionId", aliyunSmsProperties.getRegionId()); - // 要给哪个手机号发送短信 指定手机号 request.putQueryParameter("PhoneNumbers", mobile); - // 您的申请签名 request.putQueryParameter("SignName", aliyunSmsProperties.getSignName()); - // 您申请的模板 code request.putQueryParameter("TemplateCode", templateCode); - request.putQueryParameter("TemplateParam", JSONUtil.toJsonStr(templateParams)); try { CommonResponse response = client.getCommonResponse(request); return response.getHttpResponse().isSuccess(); } catch (ClientException e) { - e.printStackTrace(); + log.error("发送短信失败: {}", e.getMessage()); } return false; } - - } diff --git a/src/main/java/com/youlai/boot/support/sse/SseService.java b/src/main/java/com/youlai/boot/support/sse/SseService.java new file mode 100644 index 00000000..6613a78d --- /dev/null +++ b/src/main/java/com/youlai/boot/support/sse/SseService.java @@ -0,0 +1,132 @@ +package com.youlai.boot.support.sse; + +import com.youlai.boot.support.sse.constant.SseEventConstants; +import com.youlai.boot.support.sse.constant.SseTopics; +import com.youlai.boot.support.sse.model.OnlineUserDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +/** + * SSE 推送服务 + *

    + * 组合 {@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 systemMessage = Map.of( + "sender", "系统通知", + "content", message, + "timestamp", System.currentTimeMillis() + ); + sessionManager.broadcast(SseTopics.SYSTEM, systemMessage); + log.info("已发送系统消息: {}", message); + } + + /** + * 获取在线用户列表 + * + * @return 在线用户列表 + */ + public List getOnlineUsers() { + return sessionManager.getOnlineUsernames().stream() + .map(username -> { + int sessionCount = sessionManager.getUserConnectionCount(username); + long loginTime = sessionManager.getEarliestLoginTime(username); + return new OnlineUserDTO(username, sessionCount, loginTime); + }) + .toList(); + } + + /** + * 获取在线用户数 + * + * @return 在线用户数 + */ + public int getOnlineUserCount() { + return sessionManager.getOnlineUserCount(); + } +} diff --git a/src/main/java/com/youlai/boot/message/registry/SseSessionRegistry.java b/src/main/java/com/youlai/boot/support/sse/SseSessionManager.java similarity index 81% rename from src/main/java/com/youlai/boot/message/registry/SseSessionRegistry.java rename to src/main/java/com/youlai/boot/support/sse/SseSessionManager.java index f6923b41..55c25b24 100644 --- a/src/main/java/com/youlai/boot/message/registry/SseSessionRegistry.java +++ b/src/main/java/com/youlai/boot/support/sse/SseSessionManager.java @@ -1,6 +1,6 @@ -package com.youlai.boot.message.registry; +package com.youlai.boot.support.sse; -import com.youlai.boot.message.dto.OnlineUserDTO; +import com.youlai.boot.support.sse.constant.SseEventConstants; import lombok.extern.slf4j.Slf4j; import org.springframework.context.event.ContextClosedEvent; import org.springframework.context.event.EventListener; @@ -16,16 +16,22 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; /** - * SSE 会话注册表 + * SSE 会话管理器 *

    - * 维护 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> userEmittersMap = new ConcurrentHashMap<>(); @@ -48,13 +54,13 @@ public class SseSessionRegistry { emitterTimeMap.put(emitter, System.currentTimeMillis()); log.debug("用户[{}]SSE连接已建立", username); - // 设置连接超时和完成回调 emitter.onCompletion(() -> { removeEmitter(emitter); log.debug("用户[{}]SSE连接已完成", username); }); emitter.onTimeout(() -> { removeEmitter(emitter); + emitter.complete(); log.debug("用户[{}]SSE连接超时", username); }); emitter.onError(e -> { @@ -135,22 +141,26 @@ public class SseSessionRegistry { } /** - * 获取所有在线用户列表 + * 获取所有在线用户名 */ - public List getOnlineUsers() { - return userEmittersMap.entrySet().stream() - .map(entry -> { - String username = entry.getKey(); - Set emitters = entry.getValue(); - long earliestTime = emitters.stream() - .map(emitterTimeMap::get) - .filter(t -> t != null) - .mapToLong(Long::longValue) - .min() - .orElse(System.currentTimeMillis()); - return new OnlineUserDTO(username, emitters.size(), earliestTime); - }) - .collect(Collectors.toList()); + public Set getOnlineUsernames() { + return userEmittersMap.keySet(); + } + + /** + * 获取指定用户的最早登录时间 + */ + public long getEarliestLoginTime(String username) { + Set emitters = userEmittersMap.get(username); + if (emitters == null || emitters.isEmpty()) { + return System.currentTimeMillis(); + } + return emitters.stream() + .map(emitterTimeMap::get) + .filter(t -> t != null) + .mapToLong(Long::longValue) + .min() + .orElse(System.currentTimeMillis()); } /** @@ -178,13 +188,16 @@ public class SseSessionRegistry { return true; } catch (IOException e) { log.warn("发送SSE事件失败: {}", e.getMessage()); - removeEmitter(emitter); + emitter.completeWithError(e); return false; } } /** * 向所有连接广播事件 + * + * @param eventName 事件名称 + * @param data 数据 */ public void broadcast(String eventName, Object data) { getAllEmitters().forEach(emitter -> sendEvent(emitter, eventName, data)); @@ -192,6 +205,10 @@ public class SseSessionRegistry { /** * 向指定用户发送事件 + * + * @param username 用户名 + * @param eventName 事件名称 + * @param data 数据 */ public void sendToUser(String username, String eventName, Object data) { Set emitters = userEmittersMap.get(username); @@ -201,7 +218,7 @@ public class SseSessionRegistry { } /** - * 心跳检测:每30秒向所有连接发送ping事件,及时清理已断开的僵尸连接 + * 心跳检测:每 30 秒向所有连接发送 ping 事件,清理已断开的僵尸连接 */ @Scheduled(fixedRate = 30000) public void heartbeat() { @@ -211,7 +228,7 @@ public class SseSessionRegistry { List failedEmitters = new ArrayList<>(); for (SseEmitter emitter : emitterUserMap.keySet()) { try { - emitter.send(SseEmitter.event().name("ping").data("heartbeat")); + emitter.send(SseEmitter.event().name(SseEventConstants.PING).data("heartbeat")); } catch (Exception e) { failedEmitters.add(emitter); } diff --git a/src/main/java/com/youlai/boot/support/sse/constant/SseEventConstants.java b/src/main/java/com/youlai/boot/support/sse/constant/SseEventConstants.java new file mode 100644 index 00000000..4bd71119 --- /dev/null +++ b/src/main/java/com/youlai/boot/support/sse/constant/SseEventConstants.java @@ -0,0 +1,19 @@ +package com.youlai.boot.support.sse.constant; + +/** + * SSE 通用事件常量 + * + * @author Ray.Hao + * @since 3.1.0 + */ +public final class SseEventConstants { + + private SseEventConstants() { + } + + /** 心跳事件 */ + public static final String PING = "ping"; + + /** 在线用户数事件 */ + public static final String ONLINE_USERS = "online-users"; +} diff --git a/src/main/java/com/youlai/boot/support/sse/constant/SseTopics.java b/src/main/java/com/youlai/boot/support/sse/constant/SseTopics.java new file mode 100644 index 00000000..3d82972b --- /dev/null +++ b/src/main/java/com/youlai/boot/support/sse/constant/SseTopics.java @@ -0,0 +1,25 @@ +package com.youlai.boot.support.sse.constant; + +/** + * SSE 业务主题常量 + * + * @author Ray.Hao + * @since 3.1.0 + */ +public final class SseTopics { + + private SseTopics() { + } + + /** 字典变更事件 */ + public static final String DICT = "dict"; + + /** 系统消息事件 */ + public static final String SYSTEM = "system"; + + /** 通知事件 */ + public static final String NOTICE = "notice"; + + /** 通知撤回事件 */ + public static final String NOTICE_REVOKE = "notice-revoke"; +} diff --git a/src/main/java/com/youlai/boot/message/dto/OnlineUserDTO.java b/src/main/java/com/youlai/boot/support/sse/model/OnlineUserDTO.java similarity index 63% rename from src/main/java/com/youlai/boot/message/dto/OnlineUserDTO.java rename to src/main/java/com/youlai/boot/support/sse/model/OnlineUserDTO.java index 57521550..81cb9e51 100644 --- a/src/main/java/com/youlai/boot/message/dto/OnlineUserDTO.java +++ b/src/main/java/com/youlai/boot/support/sse/model/OnlineUserDTO.java @@ -1,4 +1,4 @@ -package com.youlai.boot.message.dto; +package com.youlai.boot.support.sse.model; import lombok.AllArgsConstructor; import lombok.Data; @@ -8,9 +8,9 @@ import java.io.Serial; import java.io.Serializable; /** - * 在线用户信息DTO + * 在线用户信息 DTO。 *

    - * 用于返回在线用户的基本信息,包括用户名、会话数量和登录时间。 + * 用于 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 getOnlineCount() { - return Result.success(sseService.getOnlineUserCount()); - } } diff --git a/src/main/java/com/youlai/boot/system/controller/UserController.java b/src/main/java/com/youlai/boot/system/controller/UserController.java index f9deca1b..ce9a926c 100644 --- a/src/main/java/com/youlai/boot/system/controller/UserController.java +++ b/src/main/java/com/youlai/boot/system/controller/UserController.java @@ -2,8 +2,8 @@ package com.youlai.boot.system.controller; import cn.idev.excel.EasyExcel; import cn.idev.excel.ExcelWriter; -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.enums.StatusEnum; diff --git a/src/main/java/com/youlai/boot/system/handler/XxlJobSampleHandler.java b/src/main/java/com/youlai/boot/system/handler/XxlJobSampleHandler.java index 69628788..e69de29b 100644 --- a/src/main/java/com/youlai/boot/system/handler/XxlJobSampleHandler.java +++ b/src/main/java/com/youlai/boot/system/handler/XxlJobSampleHandler.java @@ -1,19 +0,0 @@ -package com.youlai.boot.system.handler; - -import com.xxl.job.core.handler.annotation.XxlJob; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -/** - * xxl-job 测试示例(Bean模式) - */ -@Component -@Slf4j -public class XxlJobSampleHandler { - - @XxlJob("demoJobHandler") - public void demoJobHandler() { - log.info("XXL-JOB, Hello World."); - } - -} diff --git a/src/main/java/com/youlai/boot/system/job/OnlineUserCountTask.java b/src/main/java/com/youlai/boot/system/job/OnlineUserCountTask.java new file mode 100644 index 00000000..025f9532 --- /dev/null +++ b/src/main/java/com/youlai/boot/system/job/OnlineUserCountTask.java @@ -0,0 +1,27 @@ +package com.youlai.boot.system.job; + +import com.youlai.boot.support.sse.SseService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * 在线用户统计定时任务 + *

    每 3 分钟统计并广播当前在线用户数量

    + * + * @author Ray.Hao + * @since 3.1.0 + */ +@Component +@Slf4j +@RequiredArgsConstructor +public class OnlineUserCountTask { + + private final SseService sseService; + + @Scheduled(cron = "0 */3 * * * ?") + public void execute() { + sseService.sendOnlineCount(); + } +} diff --git a/src/main/java/com/youlai/boot/system/mapper/DeptMapper.java b/src/main/java/com/youlai/boot/system/mapper/DeptMapper.java index f92e6ae6..e00f27bf 100644 --- a/src/main/java/com/youlai/boot/system/mapper/DeptMapper.java +++ b/src/main/java/com/youlai/boot/system/mapper/DeptMapper.java @@ -3,7 +3,7 @@ package com.youlai.boot.system.mapper; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.toolkit.Constants; -import com.youlai.boot.framework.annotation.DataPermission; +import com.youlai.boot.common.annotation.DataPermission; import com.youlai.boot.system.model.entity.Dept; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; diff --git a/src/main/java/com/youlai/boot/system/mapper/UserMapper.java b/src/main/java/com/youlai/boot/system/mapper/UserMapper.java index 1da5402d..06b630f1 100644 --- a/src/main/java/com/youlai/boot/system/mapper/UserMapper.java +++ b/src/main/java/com/youlai/boot/system/mapper/UserMapper.java @@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.system.model.entity.SysUser; import com.youlai.boot.system.model.query.UserQuery; import com.youlai.boot.system.model.form.UserForm; -import com.youlai.boot.framework.annotation.DataPermission; +import com.youlai.boot.common.annotation.DataPermission; import com.youlai.boot.framework.security.model.SecurityUser; import com.youlai.boot.system.model.vo.UserExportVO; import com.youlai.boot.system.model.vo.UserPageVO; diff --git a/src/main/java/com/youlai/boot/message/dto/DictChangeEvent.java b/src/main/java/com/youlai/boot/system/model/dto/DictChangeEvent.java similarity index 85% rename from src/main/java/com/youlai/boot/message/dto/DictChangeEvent.java rename to src/main/java/com/youlai/boot/system/model/dto/DictChangeEvent.java index 934f2411..d6d0f5e3 100644 --- a/src/main/java/com/youlai/boot/message/dto/DictChangeEvent.java +++ b/src/main/java/com/youlai/boot/system/model/dto/DictChangeEvent.java @@ -1,4 +1,4 @@ -package com.youlai.boot.message.dto; +package com.youlai.boot.system.model.dto; import lombok.AllArgsConstructor; import lombok.Data; @@ -11,7 +11,6 @@ import java.io.Serializable; * 字典变更事件 *

    * 当字典数据发生变更时,通过 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 implements private final RoleMenuService roleMenuService; private final SmsService smsService; - - private final MailService mailService; + private final EmailService emailService; private final StringRedisTemplate redisTemplate; @@ -328,6 +327,11 @@ public class UserServiceImpl extends ServiceImpl implements // 用户权限集合 if (CollectionUtil.isNotEmpty(roles)) { Set perms = roleMenuService.getRolePermsByRoleCodes(roles); + // 缓存未命中时触发全量刷新(防止 DB 数据已更新但缓存未同步) + if (CollectionUtil.isEmpty(perms)) { + roleMenuService.refreshRolePermsCache(); + perms = roleMenuService.getRolePermsByRoleCodes(roles); + } userInfoVo.setPerms(perms); } return userInfoVo; @@ -454,7 +458,13 @@ public class UserServiceImpl extends ServiceImpl implements Map templateParams = new HashMap<>(); templateParams.put("code", code); - boolean result = smsService.sendSms(mobile, SmsTypeEnum.CHANGE_MOBILE, templateParams); + boolean result = true; + try { + smsService.send(mobile, SmsTypeEnum.CHANGE_MOBILE, templateParams); + } catch (Exception e) { + result = false; + log.error("发送短信验证码失败", e); + } if (result) { // 缓存验证码,5分钟有效,用于更换手机号校验 String redisCacheKey = StrUtil.format(RedisConstants.Captcha.MOBILE_CODE, mobile); @@ -537,7 +547,7 @@ public class UserServiceImpl extends ServiceImpl implements // TODO 为了方便测试,验证码固定为 123456,实际开发中在配置了邮箱服务后,可以使用上面的随机验证码 String code = "123456"; - mailService.sendMail(email, "邮箱验证码", "您的验证码为:" + code + ",请在5分钟内使用"); + emailService.send(email, "邮箱验证码", "您的验证码为:" + code + ",请在5分钟内使用"); // 缓存验证码,5分钟有效,用于更换邮箱校验 String redisCacheKey = StrUtil.format(RedisConstants.Captcha.EMAIL_CODE, email); redisTemplate.opsForValue().set(redisCacheKey, code, 5, TimeUnit.MINUTES); diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 14fcf7ca..10c9fd0e 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -89,15 +89,13 @@ security: allow-multi-login: true # 是否允许多设备登录 # 安全白名单路径,仅跳过 AuthorizationFilter 过滤器,还是会走 Spring Security 的其他过滤器(CSRF、CORS等) ignore-urls: - - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录) + - /api/v1/auth/login/** # 登录接口(账号密码登录) - /api/v1/auth/captcha # 验证码获取接口 - - /api/v1/auth/sms/code # 发送登录短信验证码 + - /api/v1/auth/sms/** # 短信验证码接口(发送验证码、短信登录) - /api/v1/auth/refresh-token # 刷新令牌接口 - - /api/v1/wxma/auth/** # 微信小程序认证接口(静默登录/手机号快捷登录/绑定手机号) + - /api/v1/auth/wxma/** # 微信小程序认证接口(静默登录/手机号快捷登录/绑定手机号) - /api/v1/logs/** # 日志接口(访问日志列表) - - /api/v1/auth/qr-code/generate # 扫码登录-生成票据 - - /api/v1/auth/qr-code/status # 扫码登录-查询状态 - - /api/v1/auth/qr-code/login # 扫码登录-换取会话令牌 + - /api/v1/auth/qr-code/** # 扫码登录接口(生成票据/查询状态/换取令牌) # 非安全端点路径,完全绕过 Spring Security 的过滤器 unsecured-urls: - ${springdoc.swagger-ui.path} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 8729f67b..3f0c9f93 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -88,15 +88,13 @@ security: allow-multi-login: true # 是否允许多设备登录 # 安全白名单路径,仅跳过 AuthorizationFilter 过滤器,还是会走 Spring Security 的其他过滤器(CSRF、CORS等) ignore-urls: - - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录) + - /api/v1/auth/login/** # 登录接口(账号密码登录) - /api/v1/auth/captcha # 验证码获取接口 - - /api/v1/auth/sms/code # 发送登录短信验证码 + - /api/v1/auth/sms/** # 短信验证码接口(发送验证码、短信登录) - /api/v1/auth/refresh-token # 刷新令牌接口 - - /api/v1/wxma/auth/** # 微信小程序认证接口(静默登录/手机号快捷登录/绑定手机号) + - /api/v1/auth/wxma/** # 微信小程序认证接口(静默登录/手机号快捷登录/绑定手机号) - /api/v1/logs/** # 日志接口(访问日志列表) - - /api/v1/auth/qr-code/generate # 扫码登录-生成票据 - - /api/v1/auth/qr-code/status # 扫码登录-查询状态 - - /api/v1/auth/qr-code/login # 扫码登录-换取会话令牌 + - /api/v1/auth/qr-code/** # 扫码登录接口(生成票据/查询状态/换取令牌) # 非安全端点路径,完全绕过 Spring Security 的过滤器 unsecured-urls: - ${springdoc.swagger-ui.path} diff --git a/src/main/resources/templates/codegen/backend/entity.java.vm b/src/main/resources/templates/codegen/backend/entity.java.vm index 7bb0d76e..15a8dd43 100644 --- a/src/main/resources/templates/codegen/backend/entity.java.vm +++ b/src/main/resources/templates/codegen/backend/entity.java.vm @@ -9,7 +9,7 @@ import java.time.LocalDateTime; import java.math.BigDecimal; #end import com.baomidou.mybatisplus.annotation.TableName; -import ${packageName}.framework.base.BaseEntity; +import ${packageName}.common.base.BaseEntity; /** * $!{businessName}实体对象 diff --git a/src/test/java/com/youlai/boot/auth/service/impl/AuthServiceImplTest.java b/src/test/java/com/youlai/boot/auth/service/impl/AuthServiceImplTest.java deleted file mode 100644 index ab8d40a2..00000000 --- a/src/test/java/com/youlai/boot/auth/service/impl/AuthServiceImplTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.youlai.boot.auth.service.impl; - -import com.youlai.boot.common.exception.BusinessException; -import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.framework.captcha.service.CaptchaService; -import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum; -import com.youlai.boot.framework.integration.sms.service.SmsService; -import com.youlai.boot.framework.security.token.TokenManager; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.security.authentication.AuthenticationManager; - -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class AuthServiceImplTest { - - @Mock - private AuthenticationManager authenticationManager; - - @Mock - private TokenManager tokenManager; - - @Mock - private SmsService smsService; - - @Mock - private RedisTemplate redisTemplate; - - @Mock - private CaptchaService captchaService; - - @Test - void should_not_cache_sms_code_when_send_fails() { - when(smsService.sendSms(eq("13800138000"), eq(SmsTypeEnum.LOGIN), any(Map.class))).thenReturn(false); - - AuthServiceImpl authService = new AuthServiceImpl( - authenticationManager, - tokenManager, - smsService, - redisTemplate, - captchaService - ); - - assertThatThrownBy(() -> authService.sendSmsCode("13800138000")) - .isInstanceOf(BusinessException.class) - .hasMessage("短信验证码发送失败"); - - verify(redisTemplate, never()).opsForValue(); - } -} diff --git a/src/test/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandlerTest.java b/src/test/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandlerTest.java deleted file mode 100644 index 0c88711f..00000000 --- a/src/test/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandlerTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.youlai.boot.framework.web.advice; - -import com.youlai.boot.common.result.Result; -import com.youlai.boot.common.result.ResultCode; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -class GlobalExceptionHandlerTest { - - @Test - void should_not_expose_unknown_exception_message() throws Exception { - GlobalExceptionHandler handler = new GlobalExceptionHandler(); - - Result result = handler.handleException(new IllegalStateException("database password leaked")); - - assertThat(result.getCode()).isEqualTo(ResultCode.SYSTEM_ERROR.getCode()); - assertThat(result.getMsg()).isEqualTo("系统异常,请稍后重试"); - } -} diff --git a/src/test/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfigTest.java b/src/test/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfigTest.java deleted file mode 100644 index 8c0b9494..00000000 --- a/src/test/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfigTest.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.youlai.boot.framework.web.config; - -import org.junit.jupiter.api.Test; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -import java.util.concurrent.Executor; -import java.util.concurrent.ThreadPoolExecutor; - -import static org.assertj.core.api.Assertions.assertThat; - -class OperationLogExecutorConfigTest { - - @Test - void should_create_operation_log_executor() { - Executor executor = new OperationLogExecutorConfig().operationLogExecutor(); - - assertThat(executor).isInstanceOf(ThreadPoolTaskExecutor.class); - ThreadPoolTaskExecutor taskExecutor = (ThreadPoolTaskExecutor) executor; - assertThat(taskExecutor.getThreadNamePrefix()).isEqualTo("operation-log-"); - assertThat(taskExecutor.getCorePoolSize()).isEqualTo(1); - assertThat(taskExecutor.getMaxPoolSize()).isEqualTo(2); - assertThat(taskExecutor.getThreadPoolExecutor().getQueue().remainingCapacity()).isEqualTo(1000); - assertThat(taskExecutor.getThreadPoolExecutor().getRejectedExecutionHandler()) - .isInstanceOf(ThreadPoolExecutor.CallerRunsPolicy.class); - - taskExecutor.shutdown(); - } -} diff --git a/src/test/java/com/youlai/boot/generator/SystemCodeGenerator.java b/src/test/java/com/youlai/boot/generator/SystemCodeGenerator.java deleted file mode 100644 index 32ce294f..00000000 --- a/src/test/java/com/youlai/boot/generator/SystemCodeGenerator.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.youlai.boot.generator; - -import com.baomidou.mybatisplus.generator.FastAutoGenerator; - -import com.baomidou.mybatisplus.generator.config.DataSourceConfig; -import com.baomidou.mybatisplus.generator.config.OutputFile; -import com.baomidou.mybatisplus.generator.config.builder.CustomFile; - -import java.util.*; - -/** - * 系统模块代码生成 - *

    - * 代码生成、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 customFiles = new ArrayList<>(); - customFiles.add(new CustomFile.Builder().fileName("Vo.java").templatePath("/templates/vo.java.vm").packageName("model.vo").build()); - customFiles.add(new CustomFile.Builder().fileName("Dto.java").templatePath("/templates/dto.java.vm").packageName("model.dto").build()); - customFiles.add(new CustomFile.Builder().fileName("Bo.java").templatePath("/templates/bo.java.vm").packageName("model.bo").build()); - customFiles.add(new CustomFile.Builder().fileName("PageQuery.java").templatePath("/templates/query.java.vm").packageName("model.query").build()); - customFiles.add(new CustomFile.Builder().fileName("PageVo.java").templatePath("/templates/pageVo.java.vm").packageName("model.vo").build()); - customFiles.add(new CustomFile.Builder().fileName("Form.java").templatePath("/templates/form.java.vm").packageName("model.form").build()); - customFiles.add(new CustomFile.Builder().fileName("Converter.java").templatePath("/templates/converter.java.vm").packageName("converter").build()); - consumer.customFile(customFiles); - consumer.beforeOutputFile((tableInfo, objectMap) -> { - // 为每个表生成首字母小写的实体名 - String entityName = tableInfo.getEntityName(); - String lowerCaseEntity = entityName.substring(0, 1).toLowerCase() + entityName.substring(1); - // 注入自定义参数 - objectMap.put("lowerFirstEntityName", lowerCaseEntity); - - }); - - }) - // 策略配置 - .strategyConfig((scanner, builder) -> { - - builder.entityBuilder() - .enableLombok() // 是否使用lombok - //.enableFileOverride() // 开启覆盖已生成的文件 - .logicDeleteColumnName("deleted") // 逻辑删除字段名 - .enableRemoveIsPrefix() // 开启移除is前缀 - ; - - builder.mapperBuilder() - .enableBaseColumnList() - .enableBaseResultMap() - ; - - builder.serviceBuilder() - .formatServiceFileName("%sService" - ); - - - builder.addTablePrefix("sys_") // 过滤移除表前缀 sys_user 表生成的实体类 User.java - .addInclude(scanner.apply("请输入表名,多个表名用,隔开")); - } - ) - .execute() - - ; - } -} diff --git a/src/test/java/com/youlai/boot/security/filter/TokenAuthenticationFilterTest.java b/src/test/java/com/youlai/boot/security/filter/TokenAuthenticationFilterTest.java deleted file mode 100644 index e8393b06..00000000 --- a/src/test/java/com/youlai/boot/security/filter/TokenAuthenticationFilterTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.youlai.boot.security.filter; - -import com.youlai.boot.common.constant.SecurityConstants; -import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter; -import com.youlai.boot.framework.security.token.TokenManager; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpHeaders; -import org.springframework.mock.web.MockFilterChain; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.core.context.SecurityContextHolder; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class TokenAuthenticationFilterTest { - - @Mock - private TokenManager tokenManager; - - @AfterEach - void tearDown() { - SecurityContextHolder.clearContext(); - } - - @Test - void should_clear_context_and_return_unauthorized_when_token_validation_fails() throws Exception { - String token = "invalid-token"; - when(tokenManager.validateToken(token)).thenThrow(new IllegalStateException("token store unavailable")); - SecurityContextHolder.getContext().setAuthentication(new org.springframework.security.authentication.TestingAuthenticationToken("old", null)); - - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/users"); - request.addHeader(HttpHeaders.AUTHORIZATION, SecurityConstants.BEARER_TOKEN_PREFIX + token); - MockHttpServletResponse response = new MockHttpServletResponse(); - - new TokenAuthenticationFilter(tokenManager).doFilter(request, response, new MockFilterChain()); - - assertThat(response.getStatus()).isEqualTo(401); - assertThat(response.getContentAsString()).contains(ResultCode.ACCESS_TOKEN_INVALID.getCode()); - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); - } -} diff --git a/src/test/java/com/youlai/boot/security/token/JwtTokenManagerTest.java b/src/test/java/com/youlai/boot/security/token/JwtTokenManagerTest.java deleted file mode 100644 index 14979f0d..00000000 --- a/src/test/java/com/youlai/boot/security/token/JwtTokenManagerTest.java +++ /dev/null @@ -1,350 +0,0 @@ -package com.youlai.boot.security.token; - -import com.youlai.boot.framework.security.config.SecurityProperties; -import com.youlai.boot.framework.security.model.AuthenticationToken; -import com.youlai.boot.framework.security.model.RoleDataScope; -import com.youlai.boot.framework.security.model.SecurityUserDetails; -import com.youlai.boot.framework.security.token.JwtTokenManager; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ValueOperations; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; - -import java.util.Collection; -import java.util.List; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * JwtTokenManager 单元测试 - * - * @author Ray.Hao - */ -@ExtendWith(MockitoExtension.class) -class JwtTokenManagerTest { - - @Mock - private RedisTemplate redisTemplate; - - @Mock - private ValueOperations valueOperations; - - private JwtTokenManager tokenManager; - private SecurityProperties securityProperties; - - private static final String TEST_SECRET_KEY = "TestSecretKey01234567890123456789"; - private static final int ACCESS_TOKEN_TTL = 3600; - private static final int REFRESH_TOKEN_TTL = 604800; - - @BeforeEach - void setUp() { - securityProperties = createSecurityProperties(); - - lenient().when(redisTemplate.opsForValue()).thenReturn(valueOperations); - lenient().when(redisTemplate.hasKey(anyString())).thenReturn(false); - - tokenManager = new JwtTokenManager(securityProperties, redisTemplate); - } - - @Nested - @DisplayName("Token 生成测试") - class GenerateTokenTests { - - @Test - @DisplayName("应成功生成有效的访问令牌和刷新令牌") - void should_generate_valid_token() { - Authentication authentication = createTestAuthentication(); - - AuthenticationToken token = tokenManager.generateToken(authentication); - - assertThat(token).isNotNull(); - assertThat(token.getAccessToken()).isNotBlank(); - assertThat(token.getRefreshToken()).isNotBlank(); - assertThat(token.getTokenType()).isEqualTo("Bearer"); - assertThat(token.getExpiresIn()).isEqualTo(ACCESS_TOKEN_TTL); - assertThat(token.getAccessToken()).isNotEqualTo(token.getRefreshToken()); - } - - @Test - @DisplayName("生成的 Token 应包含用户信息") - void should_contain_user_info() { - Authentication authentication = createTestAuthentication(); - - AuthenticationToken token = tokenManager.generateToken(authentication); - Authentication parsed = tokenManager.parseToken(token.getAccessToken()); - - assertThat(parsed).isNotNull(); - assertThat(parsed.getName()).isEqualTo("testuser"); - - SecurityUserDetails userDetails = (SecurityUserDetails) parsed.getPrincipal(); - assertThat(userDetails.getUserId()).isEqualTo(1L); - assertThat(userDetails.getDeptId()).isEqualTo(100L); - } - - @Test - @DisplayName("生成的 Token 应包含角色权限") - void should_contain_authorities() { - Authentication authentication = createTestAuthentication(); - - AuthenticationToken token = tokenManager.generateToken(authentication); - Authentication parsed = tokenManager.parseToken(token.getAccessToken()); - - Collection authorities = parsed.getAuthorities(); - assertThat(authorities).hasSize(2); - assertThat(authorities) - .extracting("authority") - .containsExactlyInAnyOrder("ROLE_ADMIN", "ROLE_USER"); - } - - @Test - @DisplayName("生成的 Token 应包含数据权限") - void should_contain_data_scopes() { - Authentication authentication = createTestAuthentication(); - - AuthenticationToken token = tokenManager.generateToken(authentication); - Authentication parsed = tokenManager.parseToken(token.getAccessToken()); - - SecurityUserDetails userDetails = (SecurityUserDetails) parsed.getPrincipal(); - List dataScopes = userDetails.getDataScopes(); - - assertThat(dataScopes).hasSize(2); - assertThat(dataScopes.get(0).getRoleCode()).isEqualTo("ADMIN"); - assertThat(dataScopes.get(0).getDataScope()).isEqualTo(1); - } - } - - @Nested - @DisplayName("Token 校验测试") - class ValidateTokenTests { - - @Test - @DisplayName("有效 Token 校验应返回 true") - void should_validate_valid_token() { - Authentication authentication = createTestAuthentication(); - AuthenticationToken token = tokenManager.generateToken(authentication); - - boolean isValid = tokenManager.validateToken(token.getAccessToken()); - - assertThat(isValid).isTrue(); - } - - @Test - @DisplayName("无效签名 Token 校验应返回 false") - void should_reject_invalid_signature() { - String invalidToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"; - - boolean isValid = tokenManager.validateToken(invalidToken); - - assertThat(isValid).isFalse(); - } - - @Test - @DisplayName("刷新令牌校验应区分访问令牌") - void should_distinguish_refresh_token() { - Authentication authentication = createTestAuthentication(); - AuthenticationToken token = tokenManager.generateToken(authentication); - - // 访问令牌不能作为刷新令牌使用 - boolean isValidRefreshToken = tokenManager.validateRefreshToken(token.getAccessToken()); - assertThat(isValidRefreshToken).isFalse(); - - // 刷新令牌是有效的 - boolean isValidRefreshToken2 = tokenManager.validateRefreshToken(token.getRefreshToken()); - assertThat(isValidRefreshToken2).isTrue(); - } - } - - @Nested - @DisplayName("Token 撤销测试") - class InvalidateTokenTests { - - @Test - @DisplayName("撤销 Token 后校验应返回 false") - void should_invalidate_token() { - Authentication authentication = createTestAuthentication(); - AuthenticationToken token = tokenManager.generateToken(authentication); - - // 先验证 Token 有效 - assertThat(tokenManager.validateToken(token.getAccessToken())).isTrue(); - - // 撤销 Token - tokenManager.invalidateToken(token.getAccessToken()); - - // 验证 Redis 存储了撤销标记 - verify(valueOperations).set(anyString(), any(Boolean.class), anyLong(), any()); - } - - @Test - @DisplayName("撤销空 Token 应安全处理") - void should_handle_null_token() { - tokenManager.invalidateToken(null); - tokenManager.invalidateToken(""); - - verify(valueOperations, never()).set(anyString(), any(), anyLong(), any()); - } - } - - @Nested - @DisplayName("用户会话失效测试") - class InvalidateUserSessionsTests { - - @Test - @DisplayName("失效用户所有会话后,旧 Token 应无效") - void should_invalidate_user_sessions() { - Authentication authentication = createTestAuthentication(); - AuthenticationToken token = tokenManager.generateToken(authentication); - - // 模拟用户会话失效后的 Redis 状态 - when(valueOperations.get(anyString())).thenReturn(System.currentTimeMillis() / 1000 + 1000); - - // 验证 Token 已失效 - boolean isValid = tokenManager.validateToken(token.getAccessToken()); - assertThat(isValid).isFalse(); - } - - @Test - @DisplayName("失效空用户 ID 应安全处理") - void should_handle_null_user_id() { - tokenManager.invalidateUserSessions(null); - - verify(valueOperations, never()).set(anyString(), any(), anyLong(), any()); - } - } - - @Nested - @DisplayName("Token 刷新测试") - class RefreshTokenTests { - - @Test - @DisplayName("应成功刷新访问令牌") - void should_refresh_access_token() { - Authentication authentication = createTestAuthentication(); - AuthenticationToken originalToken = tokenManager.generateToken(authentication); - - AuthenticationToken refreshedToken = tokenManager.refreshToken(originalToken.getRefreshToken()); - - assertThat(refreshedToken).isNotNull(); - assertThat(refreshedToken.getAccessToken()).isNotBlank(); - assertThat(refreshedToken.getAccessToken()).isNotEqualTo(originalToken.getAccessToken()); - assertThat(refreshedToken.getRefreshToken()).isEqualTo(originalToken.getRefreshToken()); - } - - @Test - @DisplayName("使用访问令牌刷新应失败") - void should_fail_with_access_token() { - Authentication authentication = createTestAuthentication(); - AuthenticationToken token = tokenManager.generateToken(authentication); - - // 使用访问令牌尝试刷新 - boolean isValidRefresh = tokenManager.validateRefreshToken(token.getAccessToken()); - assertThat(isValidRefresh).isFalse(); - } - } - - @Nested - @DisplayName("Token 解析测试") - class ParseTokenTests { - - @Test - @DisplayName("应正确解析 Token 中的用户信息") - void should_parse_user_details() { - Authentication authentication = createTestAuthentication(); - AuthenticationToken token = tokenManager.generateToken(authentication); - - Authentication parsed = tokenManager.parseToken(token.getAccessToken()); - - assertThat(parsed).isInstanceOf(UsernamePasswordAuthenticationToken.class); - assertThat(parsed.getPrincipal()).isInstanceOf(SecurityUserDetails.class); - - SecurityUserDetails userDetails = (SecurityUserDetails) parsed.getPrincipal(); - assertThat(userDetails.getUserId()).isEqualTo(1L); - assertThat(userDetails.getDeptId()).isEqualTo(100L); - assertThat(userDetails.getUsername()).isEqualTo("testuser"); - } - - @Test - @DisplayName("应正确解析自定义部门数据权限") - void should_parse_custom_data_scope() { - Authentication authentication = createTestAuthenticationWithCustomScope(); - AuthenticationToken token = tokenManager.generateToken(authentication); - - Authentication parsed = tokenManager.parseToken(token.getAccessToken()); - SecurityUserDetails userDetails = (SecurityUserDetails) parsed.getPrincipal(); - - List dataScopes = userDetails.getDataScopes(); - assertThat(dataScopes).hasSize(1); - assertThat(dataScopes.get(0).getCustomDeptIds()).containsExactly(10L, 20L, 30L); - } - } - - // ========== 测试数据构建方法 ========== - - private SecurityProperties createSecurityProperties() { - SecurityProperties properties = new SecurityProperties(); - SecurityProperties.SessionConfig sessionConfig = new SecurityProperties.SessionConfig(); - sessionConfig.setType("jwt"); - sessionConfig.setAccessTokenTimeToLive(ACCESS_TOKEN_TTL); - sessionConfig.setRefreshTokenTimeToLive(REFRESH_TOKEN_TTL); - - SecurityProperties.JwtConfig jwtConfig = new SecurityProperties.JwtConfig(); - jwtConfig.setSecretKey(TEST_SECRET_KEY); - sessionConfig.setJwt(jwtConfig); - - properties.setSession(sessionConfig); - properties.setIgnoreUrls(new String[]{"/api/v1/auth/login/**"}); - properties.setUnsecuredUrls(new String[]{"/doc.html"}); - return properties; - } - - private Authentication createTestAuthentication() { - SecurityUserDetails userDetails = new SecurityUserDetails(); - userDetails.setUserId(1L); - userDetails.setUsername("testuser"); - userDetails.setDeptId(100L); - userDetails.setEnabled(true); - userDetails.setDataScopes(List.of( - new RoleDataScope("ADMIN", 1, null), // 全部数据权限 - new RoleDataScope("USER", 4, null) // 本人数据权限 - )); - userDetails.setRoles(Set.of("ADMIN", "USER")); - - return new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - } - - private Authentication createTestAuthenticationWithCustomScope() { - SecurityUserDetails userDetails = new SecurityUserDetails(); - userDetails.setUserId(2L); - userDetails.setUsername("customuser"); - userDetails.setDeptId(200L); - userDetails.setEnabled(true); - userDetails.setDataScopes(List.of( - new RoleDataScope("CUSTOM_ROLE", 5, List.of(10L, 20L, 30L)) // 自定义部门权限 - )); - userDetails.setRoles(Set.of("CUSTOM")); - - return new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); - } -}