10 Commits

Author SHA1 Message Date
Ray.Hao
10eb81ccd1 feat: 重构微信小程序认证及优化代码生成模板
- 将微信小程序认证相关类重命名(WechatMini -> WxMa)
- 新增 WxMaAuthenticationToken 和 WxMaAuthenticationProvider
- 调整代码生成前端模板样式与导入路径
2026-03-30 07:48:38 +08:00
Ray.Hao
9cd3ff88f8 refactor: 重构目录结构统一规范
- shared/ → common/(constant、enums、model)
- framework/cache/config/ → framework/cache/(扁平化)
- framework/integration/captcha/ → framework/captcha/
- config/property/ → 各模块 config/ 下
- interfaces/ → module/(sse、mail、sms)
- 移除冗余枚举 LogModuleEnum
2026-03-28 09:00:35 +08:00
Ray.Hao
234b12f297 refactor: 移除Token参数支持,统一异常响应状态码为200或500 2026-03-24 15:23:03 +08:00
Ray.Hao
8f5c1fc8e4 refactor: 优化响应状态码映射,权限不足时返回403 Forbidden 2026-03-24 10:57:05 +08:00
Ray.Hao
c71becea68 fix: 移除数据权限单元测试 2026-03-24 09:28:10 +08:00
Ray.Hao
8188c82c3d feat: 重构项目结构并新增微信小程序认证模块 2026-03-24 07:52:05 +08:00
Ray.Hao
465e63c99d feat: WebSocket 迁移到 SSE 实现实时推送 2026-03-18 17:41:05 +08:00
Ray.Hao
ba6203424a build: 移除 Spring Boot Admin 依赖和配置 2026-03-16 14:33:20 +08:00
Ray.Hao
69f062d6b9 更新微信小程序配置为占位符 2026-03-16 08:10:11 +08:00
Ray.Hao
63c8cbc873 集成 Spring Boot Admin 监控服务端及客户端依赖,并新增 AdminServerConfig 配置类启用应用监控。 2026-03-16 08:09:03 +08:00
201 changed files with 2029 additions and 3248 deletions

View File

@@ -1,60 +0,0 @@
# 2.7.1 (2024/4/18)
### 🐛 fix
- 修复用户名或者密码错误时,返回的错误信息不正确问题
### 🛠️ refactor
- JWT 解析和验证代码优化重构
- 优化代码结构和完善注释,提高代码可读性
# 2.7.0 (2024/4/13)
### ✨ feat
- 集成 Mybatis-Plus generator 代码生成器
# 2.6.0 (2024/3/6)
### ✨ feat
- 黑名单方式实现 JWT 主动注销过期
### 🛠️ refactor
- 角色权限重构
# 2.5.0 (2023/12/6)
### ✨ feat
- [集成 Spring Cache 和 Redis 缓存,路由缓存](https://blog.csdn.net/u013737132/article/details/134789862)
### 🛠️ refactor
- 权限判断逻辑调整,用户绑定权限调整为角色绑定权限
### fix
- [接口无请求权限Spring Security 自定义异常无效问题修复](https://youlai.blog.csdn.net/article/details/134718249)
# 2.4.1 (2023/11/7)
### ✂️ refactor
- 项目目录结构优化
### ⬆️ chore
- 升级 SpringBoot 版本 `3.1.4``3.1.5`
# 2.2.1 (2023/5/25)
### 🐛 fix
- 修复多级路由的组件路径错误导致页面404问题
# 2.2.0 (2023/5/21)
### ✨ feat
- 菜单、角色、字典、部门添加接口权限控制
### 🐛 fix
- 用户登录权限缓存键值不一致导致获取用户数据权限错误问题修复
### ✂️ refactor
- 递归获取菜单、部门属性列表代码重构优化
### ⬆️ chore
- 升级 SpringBoot 版本 `3.0.6``3.1.0`
### 📝 docs
- SQL 脚本更新sys_menu 新增 `tree_path` 字段 (升级需更新SQL脚本)

View File

@@ -49,7 +49,7 @@
| [vue3-element-template](https://gitee.com/youlaiorg/vue3-element-template) | Vue 3 + Element Plus | 前端精简模板 | | [vue3-element-template](https://gitee.com/youlaiorg/vue3-element-template) | Vue 3 + Element Plus | 前端精简模板 |
| [youlai-boot-tenant](https://gitee.com/youlaiorg/youlai-boot-tenant) | Spring Boot 4 | 多租户 SaaS 版 | | [youlai-boot-tenant](https://gitee.com/youlaiorg/youlai-boot-tenant) | Spring Boot 4 | 多租户 SaaS 版 |
| [youlai-boot-flex](https://gitee.com/youlaiorg/youlai-boot-flex) | Spring Boot 3 + MyBatis-Flex | MyBatis-Flex 版 | | [youlai-boot-flex](https://gitee.com/youlaiorg/youlai-boot-flex) | Spring Boot 3 + MyBatis-Flex | MyBatis-Flex 版 |
| [youlai-uniapp](https://gitee.com/youlaiorg/youlai-uniapp) | Vue 3 + uni-app | 移动端应用 | | [youlai-app](https://gitee.com/youlaiorg/youlai-app) | Vue 3 + uni-app | 移动端应用 |
--- ---
@@ -97,24 +97,27 @@ spring:
--- ---
## 📁 目结构 ## 📁 目结构
``` ```
youlai-boot youlai-boot
├── docker/ # Docker 部署 ├── docker/ # Docker 部署
├── sql/ # 数据库脚本 ├── sql/ # 数据库脚本
├── src/main/java/com/youlai/boot/ ├── src/main/java/com/youlai/boot/
│ ├── auth/ # 认证模块 │ ├── auth/ # 认证授权业务
│ ├── common/ # 公共模块 │ ├── common/ # 全局通用(常量、枚举、工具类、统一响应结果)
│ ├── config/ # 配置模块 │ ├── framework/ # 底层技术基座(高内聚积木块)
│ ├── core/ # 核心模块AOP、异常、过滤器 │ ├── cache/ # Redis/Caffeine 缓存
│ ├── file/ # 文件服务 │ ├── captcha/ # 验证码
│ ├── plugin/ # 插件扩展Knife4j、MyBatis │ ├── integration/ # SMS/Mail/WxMa 集成
│ ├── security/ # 安全模块JWT、Token │ ├── job/ # XxlJob 定时任务
│ ├── support/ # 支撑服务邮件、短信、WebSocket │ ├── mybatis/ # 数据库/MP配置/拦截器
│ ├── system/ # 系统模块(用户、角色、菜单、部门) │ ├── openapi/ # OpenAPI/Swagger 文档
│ ├── tool/ # 工具模块(代码生成) │ ├── security/ # 鉴权过滤器/Token机制
│ └── YouLaiBootApplication.java # 启动类 │ └── web/ # 跨域/全局异常/限流/Jackson
│ ├── module/ # 业务模块File、Codegen 等)
│ ├── system/ # 核心系统模块(用户/角色/菜单/部门)
│ └── YouLaiBootApplication.java # 启动类
└── pom.xml # Maven 配置 └── pom.xml # Maven 配置
``` ```
@@ -167,17 +170,4 @@ docker-compose up -d
<img src="https://foruda.gitee.com/images/1737108820762592766/3390ed0d_716974.png" width="280"> <img src="https://foruda.gitee.com/images/1737108820762592766/3390ed0d_716974.png" width="280">
</div> </div>
**微信交流**:添加 **`haoxianrui`**,备注「前端/后端/全栈」 > 二维码过期?添加微信 **`haoxianrui`**,备注「前端/后端/全栈」即可拉你入群。
---
如果项目对你有帮助,欢迎 ⭐️ Star 支持!
<div align="center">
<a href="https://gitee.com/youlaiorg/youlai-boot"><b>⭐ Gitee</b></a> &nbsp;•&nbsp;
<a href="https://github.com/haoxianrui/youlai-boot"><b>⭐ GitHub</b></a> &nbsp;•&nbsp;
<a href="https://atomgit.com/youlai/youlai-boot"><b>⭐ AtomGit</b></a>
<br/>
<a href="https://www.youlai.tech"><b>🌐 官网</b></a> &nbsp;•&nbsp;
<a href="https://youlai.blog.csdn.net/"><b>📝 博客</b></a>
</div>

View File

@@ -6,7 +6,7 @@
<groupId>com.youlai</groupId> <groupId>com.youlai</groupId>
<artifactId>youlai-boot</artifactId> <artifactId>youlai-boot</artifactId>
<version>4.1.0</version> <version>4.3.0</version>
<description>基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。</description> <description>基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。</description>
<parent> <parent>
@@ -63,6 +63,8 @@
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version> <transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
<weixin-java-miniapp.version>4.8.1.B</weixin-java-miniapp.version> <weixin-java-miniapp.version>4.8.1.B</weixin-java-miniapp.version>
</properties> </properties>
<dependencies> <dependencies>
@@ -131,11 +133,6 @@
<artifactId>spring-boot-starter-validation</artifactId> <artifactId>spring-boot-starter-validation</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId> <artifactId>spring-boot-starter-mail</artifactId>

View File

@@ -372,7 +372,7 @@ CREATE TABLE `sys_user` (
-- Records of sys_user -- Records of sys_user
-- ---------------------------- -- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345677', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0); INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345677', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345678', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0); INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18888888888', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0); INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
INSERT INTO `sys_user` VALUES (4, 'dept_manager', '部门主管', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345680', 1, 'manager@youlaitech.com', now(), NULL, now(), NULL, 0); INSERT INTO `sys_user` VALUES (4, 'dept_manager', '部门主管', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345680', 1, 'manager@youlaitech.com', now(), NULL, now(), NULL, 0);
INSERT INTO `sys_user` VALUES (5, 'dept_member', '部门成员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345681', 1, 'member@youlaitech.com', now(), NULL, now(), NULL, 0); INSERT INTO `sys_user` VALUES (5, 'dept_member', '部门成员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345681', 1, 'member@youlaitech.com', now(), NULL, now(), NULL, 0);
@@ -406,26 +406,30 @@ INSERT IGNORE INTO `sys_user_role` VALUES (7, 7);
-- ---------------------------- -- ----------------------------
DROP TABLE IF EXISTS `sys_log`; DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log` ( CREATE TABLE `sys_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`module` varchar(50) NOT NULL COMMENT '日志模块', `module` TINYINT NOT NULL COMMENT '模块,数字枚举,参考 LogModule 枚举',
`request_method` varchar(64) NOT NULL COMMENT '请求方式', `action_type` TINYINT NOT NULL COMMENT '操作类型,数字枚举,参考 ActionType 枚举',
`request_params` text COMMENT '请求参数(批量请求参数可能会超过text)', `title` VARCHAR(100) NOT NULL COMMENT '前端显示标题',
`response_content` mediumtext COMMENT '返回参数', `content` TEXT COMMENT '自定义日志内容',
`content` varchar(255) NOT NULL COMMENT '日志内容', `operator_id` BIGINT NOT NULL COMMENT '操作人ID',
`request_uri` varchar(255) COMMENT '请求路径', `operator_name` VARCHAR(50) COMMENT '操作人名称',
`method` varchar(255) COMMENT '方法名', `request_uri` VARCHAR(255) COMMENT '请求路径',
`ip` varchar(45) COMMENT 'IP地址', `request_method` VARCHAR(10) COMMENT '请求方法',
`province` varchar(100) COMMENT '省份', `ip` VARCHAR(45) COMMENT 'IP地址',
`city` varchar(100) COMMENT '城市', `province` VARCHAR(100) COMMENT '省份',
`execution_time` bigint COMMENT '执行时间(ms)', `city` VARCHAR(100) COMMENT '城市',
`browser` varchar(100) COMMENT '浏览器', `device` VARCHAR(100) COMMENT '设备',
`browser_version` varchar(100) COMMENT '浏览器版本', `os` VARCHAR(100) COMMENT '操作系统',
`os` varchar(100) COMMENT '终端系统', `browser` VARCHAR(100) COMMENT '浏览器',
`create_by` bigint COMMENT '创建人ID', `status` TINYINT DEFAULT 1 COMMENT '0失败 1成功',
`create_time` datetime COMMENT '创建时间', `error_msg` VARCHAR(255) COMMENT '错误信息',
PRIMARY KEY (`id`) USING BTREE, `execution_time` INT COMMENT '执行时间(ms)',
KEY `idx_create_time` (`create_time`) `create_time` DATETIME COMMENT '操作时间',
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表'; PRIMARY KEY (`id`) USING BTREE,
KEY `idx_module_action_time` (`module`, `action_type`, `create_time`),
KEY `idx_operator_time` (`operator_id`, `create_time`),
KEY `idx_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表';
-- ---------------------------- -- ----------------------------
-- Table structure for gen_table -- Table structure for gen_table

View File

@@ -333,7 +333,7 @@ CREATE TABLE `sys_user` (
-- Records of sys_user -- Records of sys_user
-- ---------------------------- -- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345677', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0); INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345677', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345678', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0); INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18888888888', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0); INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
INSERT INTO `sys_user` VALUES (4, 'dept_manager', '部门主管', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345680', 1, 'manager@youlaitech.com', now(), NULL, now(), NULL, 0); INSERT INTO `sys_user` VALUES (4, 'dept_manager', '部门主管', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345680', 1, 'manager@youlaitech.com', now(), NULL, now(), NULL, 0);
INSERT INTO `sys_user` VALUES (5, 'dept_member', '部门成员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345681', 1, 'member@youlaitech.com', now(), NULL, now(), NULL, 0); INSERT INTO `sys_user` VALUES (5, 'dept_member', '部门成员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345681', 1, 'member@youlaitech.com', now(), NULL, now(), NULL, 0);
@@ -366,26 +366,30 @@ INSERT IGNORE INTO `sys_user_role` VALUES (7, 7);
-- ---------------------------- -- ----------------------------
DROP TABLE IF EXISTS `sys_log`; DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log` ( CREATE TABLE `sys_log` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`module` varchar(50) NOT NULL COMMENT '日志模块', `module` TINYINT NOT NULL COMMENT '模块,数字枚举,参考 LogModule 枚举',
`request_method` varchar(64) NOT NULL COMMENT '请求方式', `action_type` TINYINT NOT NULL COMMENT '操作类型,数字枚举,参考 ActionType 枚举',
`request_params` text COMMENT '请求参数(批量请求参数可能会超过text)', `title` VARCHAR(100) NOT NULL COMMENT '前端显示标题',
`response_content` mediumtext COMMENT '返回参数', `content` TEXT COMMENT '自定义日志内容',
`content` varchar(255) NOT NULL COMMENT '日志内容', `operator_id` BIGINT NOT NULL COMMENT '操作人ID',
`request_uri` varchar(255) COMMENT '请求路径', `operator_name` VARCHAR(50) COMMENT '操作人名称',
`method` varchar(255) COMMENT '方法名', `request_uri` VARCHAR(255) COMMENT '请求路径',
`ip` varchar(45) COMMENT 'IP地址', `request_method` VARCHAR(10) COMMENT '请求方法',
`province` varchar(100) COMMENT '省份', `ip` VARCHAR(45) COMMENT 'IP地址',
`city` varchar(100) COMMENT '城市', `province` VARCHAR(100) COMMENT '省份',
`execution_time` bigint COMMENT '执行时间(ms)', `city` VARCHAR(100) COMMENT '城市',
`browser` varchar(100) COMMENT '浏览器', `device` VARCHAR(100) COMMENT '设备',
`browser_version` varchar(100) COMMENT '浏览器版本', `os` VARCHAR(100) COMMENT '操作系统',
`os` varchar(100) COMMENT '终端系统', `browser` VARCHAR(100) COMMENT '浏览器',
`create_by` bigint COMMENT '创建人ID', `status` TINYINT DEFAULT 1 COMMENT '0失败 1成功',
`create_time` datetime COMMENT '创建时间', `error_msg` VARCHAR(255) COMMENT '错误信息',
PRIMARY KEY (`id`) USING BTREE, `execution_time` INT COMMENT '执行时间(ms)',
KEY `idx_create_time` (`create_time`) `create_time` DATETIME COMMENT '操作时间',
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表'; PRIMARY KEY (`id`) USING BTREE,
KEY `idx_module_action_time` (`module`, `action_type`, `create_time`),
KEY `idx_operator_time` (`operator_id`, `create_time`),
KEY `idx_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表';
-- ---------------------------- -- ----------------------------
-- Table structure for gen_table -- Table structure for gen_table

View File

@@ -1,12 +1,13 @@
package com.youlai.boot.auth.controller; package com.youlai.boot.auth.controller;
import com.youlai.boot.auth.model.vo.CaptchaVO; import com.youlai.boot.auth.model.LoginReq;
import com.youlai.boot.auth.model.dto.LoginRequest; import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.core.web.Result; import com.youlai.boot.common.result.Result;
import com.youlai.boot.auth.service.AuthService; import com.youlai.boot.auth.service.AuthService;
import com.youlai.boot.common.annotation.Log; import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.security.model.AuthenticationToken; import com.youlai.boot.framework.captcha.model.CaptchaInfo;
import com.youlai.boot.framework.security.model.AuthenticationToken;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -32,24 +33,24 @@ public class AuthController {
@Operation(summary = "获取验证码") @Operation(summary = "获取验证码")
@GetMapping("/captcha") @GetMapping("/captcha")
public Result<CaptchaVO> getCaptcha() { public Result<CaptchaInfo> getCaptcha() {
CaptchaVO captcha = authService.getCaptcha(); CaptchaInfo captcha = authService.getCaptcha();
return Result.success(captcha); return Result.success(captcha);
} }
@Operation(summary = "账号密码登录") @Operation(summary = "账号密码登录")
@PostMapping("/login") @PostMapping("/login")
@Log(value = "登录", module = LogModuleEnum.LOGIN) @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
public Result<AuthenticationToken> login(@RequestBody @Valid LoginRequest request) { public Result<AuthenticationToken> login(@RequestBody @Valid LoginReq request) {
AuthenticationToken authenticationToken = authService.login(request.getUsername(), request.getPassword()); AuthenticationToken authenticationToken = authService.login(request.getUsername(), request.getPassword());
return Result.success(authenticationToken); return Result.success(authenticationToken);
} }
@Operation(summary = "短信验证码登录") @Operation(summary = "短信验证码登录")
@PostMapping("/login/sms") @PostMapping("/login/sms")
@Log(value = "短信验证码登录", module = LogModuleEnum.LOGIN) @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
public Result<AuthenticationToken> loginBySms( public Result<AuthenticationToken> loginBySms(
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile, @Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile,
@Parameter(description = "验证码", example = "123456") @RequestParam String code @Parameter(description = "验证码", example = "123456") @RequestParam String code
) { ) {
AuthenticationToken loginResult = authService.loginBySms(mobile, code); AuthenticationToken loginResult = authService.loginBySms(mobile, code);
@@ -59,7 +60,7 @@ public class AuthController {
@Operation(summary = "发送登录短信验证码") @Operation(summary = "发送登录短信验证码")
@PostMapping("/sms/code") @PostMapping("/sms/code")
public Result<Void> sendSmsCode( public Result<Void> sendSmsCode(
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile @Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile
) { ) {
authService.sendSmsCode(mobile); authService.sendSmsCode(mobile);
return Result.success(); return Result.success();
@@ -67,7 +68,7 @@ public class AuthController {
@Operation(summary = "退出登录") @Operation(summary = "退出登录")
@DeleteMapping("/logout") @DeleteMapping("/logout")
@Log(value = "退出登录", module = LogModuleEnum.LOGIN) @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGOUT)
public Result<Void> logout() { public Result<Void> logout() {
authService.logout(); authService.logout();
return Result.success(); return Result.success();

View File

@@ -1,21 +1,27 @@
package com.youlai.boot.auth.controller; package com.youlai.boot.auth.controller;
import com.youlai.boot.auth.model.vo.WechatMiniappLoginResult; import com.youlai.boot.auth.model.WxMaBindMobileReq;
import com.youlai.boot.auth.service.WechatMiniappAuthService; import com.youlai.boot.auth.model.WxMaPhoneLoginReq;
import com.youlai.boot.auth.model.WxMaLoginResp;
import com.youlai.boot.auth.service.WxMaAuthService;
import com.youlai.boot.common.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.enums.LogModuleEnum;
import com.youlai.boot.core.web.Result; import com.youlai.boot.common.result.Result;
import com.youlai.boot.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.AuthenticationToken;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping; 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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid;
/** /**
* 微信小程序认证控制层 * 微信小程序认证控制层
* *
@@ -24,12 +30,12 @@ import org.springframework.web.bind.annotation.RestController;
*/ */
@Tag(name = "13.微信小程序认证") @Tag(name = "13.微信小程序认证")
@RestController @RestController
@RequestMapping("/api/v1/wechat/miniapp/auth") @RequestMapping("/api/v1/wxma/auth")
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class WechatMiniappAuthController { public class WxMaAuthController {
private final WechatMiniappAuthService wechatMiniappAuthService; private final WxMaAuthService wxMaAuthService;
/** /**
* 静默登录 * 静默登录
@@ -42,12 +48,12 @@ public class WechatMiniappAuthController {
*/ */
@Operation(summary = "静默登录", description = "通过微信 code 登录,已绑定用户直接返回 token未绑定用户返回 openid 需绑定手机号") @Operation(summary = "静默登录", description = "通过微信 code 登录,已绑定用户直接返回 token未绑定用户返回 openid 需绑定手机号")
@PostMapping("/silent-login") @PostMapping("/silent-login")
@Log(value = "微信小程序静默登录", module = LogModuleEnum.LOGIN) @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
public Result<WechatMiniappLoginResult> silentLogin( public Result<WxMaLoginResp> silentLogin(
@Parameter(description = "微信登录凭证wx.login 获取)", required = true, example = "0xxx") @Parameter(description = "微信登录凭证wx.login 获取)", required = true, example = "0xxx")
@RequestParam String code @RequestParam String code
) { ) {
WechatMiniappLoginResult result = wechatMiniappAuthService.silentLogin(code); WxMaLoginResp result = wxMaAuthService.silentLogin(code);
return Result.success(result); return Result.success(result);
} }
@@ -60,14 +66,9 @@ public class WechatMiniappAuthController {
*/ */
@Operation(summary = "手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序") @Operation(summary = "手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序")
@PostMapping("/phone-login") @PostMapping("/phone-login")
@Log(value = "微信小程序手机号快捷登录", module = LogModuleEnum.LOGIN) @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
public Result<AuthenticationToken> phoneLogin( public Result<AuthenticationToken> phoneLogin(@Valid @RequestBody WxMaPhoneLoginReq req) {
@Parameter(description = "微信登录凭证wx.login 获取)", required = true, example = "0xxx") AuthenticationToken result = wxMaAuthService.phoneLogin(req.getLoginCode(), req.getPhoneCode());
@RequestParam String loginCode,
@Parameter(description = "手机号授权凭证getPhoneNumber 事件获取)", required = true, example = "0xxx")
@RequestParam String phoneCode
) {
AuthenticationToken result = wechatMiniappAuthService.phoneLogin(loginCode, phoneCode);
return Result.success(result); return Result.success(result);
} }
@@ -80,16 +81,9 @@ public class WechatMiniappAuthController {
*/ */
@Operation(summary = "绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录") @Operation(summary = "绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录")
@PostMapping("/bind-mobile") @PostMapping("/bind-mobile")
@Log(value = "微信小程序绑定手机号", module = LogModuleEnum.LOGIN) @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
public Result<AuthenticationToken> bindMobile( public Result<AuthenticationToken> bindMobile(@Valid @RequestBody WxMaBindMobileReq req) {
@Parameter(description = "微信用户唯一标识(静默登录返回)", required = true) AuthenticationToken result = wxMaAuthService.bindMobile(req.getOpenid(), req.getMobile(), req.getSmsCode());
@RequestParam String openid,
@Parameter(description = "手机号码", required = true, example = "18812345678")
@RequestParam String mobile,
@Parameter(description = "短信验证码", required = true, example = "123456")
@RequestParam String smsCode
) {
AuthenticationToken result = wechatMiniappAuthService.bindMobile(openid, mobile, smsCode);
return Result.success(result); return Result.success(result);
} }
} }

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.auth.model.dto; package com.youlai.boot.auth.model;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -13,7 +13,7 @@ import jakarta.validation.constraints.NotBlank;
*/ */
@Schema(description = "登录请求参数") @Schema(description = "登录请求参数")
@Data @Data
public class LoginRequest { public class LoginReq {
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin") @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
@NotBlank(message = "用户名不能为空") @NotBlank(message = "用户名不能为空")
@@ -28,5 +28,4 @@ public class LoginRequest {
@Schema(description = "验证码", example = "123456") @Schema(description = "验证码", example = "123456")
private String captchaCode; private String captchaCode;
} }

View File

@@ -0,0 +1,28 @@
package com.youlai.boot.auth.model;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 微信小程序绑定手机号请求
*
* @author Ray.Hao
* @since 3.0.0
*/
@Schema(description = "微信小程序绑定手机号请求")
@Data
public class WxMaBindMobileReq {
@NotBlank(message = "openid 不能为空")
@Schema(description = "微信用户唯一标识(静默登录返回)", example = "oVBkZ0aYgDMDIywRdgPW8-joxXc4")
private String openid;
@NotBlank(message = "手机号不能为空")
@Schema(description = "手机号码", example = "18888888888")
private String mobile;
@NotBlank(message = "短信验证码不能为空")
@Schema(description = "短信验证码", example = "123456")
private String smsCode;
}

View File

@@ -0,0 +1,42 @@
package com.youlai.boot.auth.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 微信小程序登录响应
*
* @author Ray.Hao
* @since 2.4.0
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "微信小程序登录响应")
public class WxMaLoginResp {
@Schema(description = "是否新用户")
private Boolean isNewUser;
@Schema(description = "是否需要绑定手机号")
private Boolean needBindMobile;
@Schema(description = "微信openid绑定手机号时需要")
private String openid;
@Schema(description = "访问令牌")
private String accessToken;
@Schema(description = "刷新令牌")
private String refreshToken;
@Schema(description = "令牌类型")
private String tokenType;
@Schema(description = "过期时间(秒)")
private Integer expiresIn;
}

View File

@@ -0,0 +1,24 @@
package com.youlai.boot.auth.model;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 微信小程序手机号快捷登录请求
*
* @author Ray.Hao
* @since 3.0.0
*/
@Schema(description = "微信小程序手机号快捷登录请求")
@Data
public class WxMaPhoneLoginReq {
@NotBlank(message = "微信登录凭证不能为空")
@Schema(description = "微信登录凭证wx.login 获取)", example = "0xxx")
private String loginCode;
@NotBlank(message = "手机号授权凭证不能为空")
@Schema(description = "手机号授权凭证getPhoneNumber 事件获取)", example = "0xxx")
private String phoneCode;
}

View File

@@ -1,63 +0,0 @@
package com.youlai.boot.auth.model.vo;
import com.youlai.boot.security.model.AuthenticationToken;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 微信小程序登录结果
*
* @author Ray.Hao
* @since 2.4.0
*/
@Data
@Schema(description = "微信小程序登录结果")
public class WechatMiniappLoginResult {
@Schema(description = "是否新用户")
private Boolean isNewUser;
@Schema(description = "是否需要绑定手机号")
private Boolean needBindMobile;
@Schema(description = "微信openid绑定手机号时需要")
private String openid;
@Schema(description = "访问令牌")
private String accessToken;
@Schema(description = "刷新令牌")
private String refreshToken;
@Schema(description = "令牌类型")
private String tokenType;
@Schema(description = "过期时间(秒)")
private Integer expiresIn;
/**
* 创建需要绑定手机号的结果
*/
public static WechatMiniappLoginResult needBindMobile(String openid) {
WechatMiniappLoginResult result = new WechatMiniappLoginResult();
result.setIsNewUser(true);
result.setNeedBindMobile(true);
result.setOpenid(openid);
return result;
}
/**
* 创建登录成功的结果
*/
public static WechatMiniappLoginResult success(AuthenticationToken token) {
WechatMiniappLoginResult result = new WechatMiniappLoginResult();
result.setIsNewUser(false);
result.setNeedBindMobile(false);
result.setAccessToken(token.getAccessToken());
result.setRefreshToken(token.getRefreshToken());
result.setTokenType(token.getTokenType());
result.setExpiresIn(token.getExpiresIn());
return result;
}
}

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.auth.service; package com.youlai.boot.auth.service;
import com.youlai.boot.auth.model.vo.CaptchaVO; import com.youlai.boot.framework.captcha.model.CaptchaInfo;
import com.youlai.boot.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.AuthenticationToken;
/** /**
* 认证服务接口 * 认证服务接口
@@ -43,10 +43,8 @@ public interface AuthService {
/** /**
* 获取验证码 * 获取验证码
*
* @return 验证码
*/ */
CaptchaVO getCaptcha(); CaptchaInfo getCaptcha();
/** /**
* 刷新令牌 * 刷新令牌

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.auth.service; package com.youlai.boot.auth.service;
import com.youlai.boot.auth.model.vo.WechatMiniappLoginResult; import com.youlai.boot.auth.model.WxMaLoginResp;
import com.youlai.boot.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.AuthenticationToken;
/** /**
* 微信小程序认证服务接口 * 微信小程序认证服务接口
@@ -9,7 +9,7 @@ import com.youlai.boot.security.model.AuthenticationToken;
* @author Ray.Hao * @author Ray.Hao
* @since 2.4.0 * @since 2.4.0
*/ */
public interface WechatMiniappAuthService { public interface WxMaAuthService {
/** /**
* 静默登录 * 静默登录
@@ -21,7 +21,7 @@ public interface WechatMiniappAuthService {
* @param code 微信登录凭证wx.login 获取 * @param code 微信登录凭证wx.login 获取
* @return 登录结果成功返回 token需绑定返回 openid * @return 登录结果成功返回 token需绑定返回 openid
*/ */
WechatMiniappLoginResult silentLogin(String code); WxMaLoginResp silentLogin(String code);
/** /**
* 手机号快捷登录 * 手机号快捷登录

View File

@@ -1,24 +1,17 @@
package com.youlai.boot.auth.service.impl; package com.youlai.boot.auth.service.impl;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.auth.model.vo.CaptchaVO;
import com.youlai.boot.auth.service.AuthService; import com.youlai.boot.auth.service.AuthService;
import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.enums.CaptchaTypeEnum; import com.youlai.boot.framework.captcha.model.CaptchaInfo;
import com.youlai.boot.config.property.CaptchaProperties; import com.youlai.boot.framework.captcha.service.CaptchaService;
import com.youlai.boot.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.security.model.SmsAuthenticationToken; import com.youlai.boot.framework.security.model.SmsAuthenticationToken;
import com.youlai.boot.security.token.TokenManager; import com.youlai.boot.framework.security.token.TokenManager;
import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.support.sms.enums.SmsTypeEnum; import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
import com.youlai.boot.support.sms.service.SmsService; import com.youlai.boot.framework.integration.sms.service.SmsService;
import com.youlai.boot.system.service.UserSocialService; import com.youlai.boot.system.service.LogService;
import com.youlai.boot.system.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
@@ -28,7 +21,6 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.awt.*;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -47,12 +39,10 @@ public class AuthServiceImpl implements AuthService {
private final AuthenticationManager authenticationManager; private final AuthenticationManager authenticationManager;
private final TokenManager tokenManager; private final TokenManager tokenManager;
private final Font captchaFont;
private final CaptchaProperties captchaProperties;
private final CodeGenerator codeGenerator;
private final SmsService smsService; private final SmsService smsService;
private final RedisTemplate<String, Object> redisTemplate; private final RedisTemplate<String, Object> redisTemplate;
private final CaptchaService captchaService;
private final LogService logService;
/** /**
* 用户名密码登录 * 用户名密码登录
* *
@@ -124,7 +114,6 @@ public class AuthServiceImpl implements AuthService {
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
AuthenticationToken authenticationToken = tokenManager.generateToken(authentication); AuthenticationToken authenticationToken = tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
return authenticationToken; return authenticationToken;
} }
@@ -143,50 +132,10 @@ public class AuthServiceImpl implements AuthService {
/** /**
* 获取验证码 * 获取验证码
*
* @return 验证码
*/ */
@Override @Override
public CaptchaVO getCaptcha() { public CaptchaInfo getCaptcha() {
return captchaService.generate();
String captchaType = captchaProperties.getType();
int width = captchaProperties.getWidth();
int height = captchaProperties.getHeight();
int interfereCount = captchaProperties.getInterfereCount();
int codeLength = captchaProperties.getCode().getLength();
AbstractCaptcha captcha;
if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount);
} else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength);
} else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount);
} else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount);
} else {
throw new IllegalArgumentException("Invalid captcha type: " + captchaType);
}
captcha.setGenerator(codeGenerator);
captcha.setTextAlpha(captchaProperties.getTextAlpha());
captcha.setFont(captchaFont);
String captchaCode = captcha.getCode();
String imageBase64Data = captcha.getImageBase64Data();
// 验证码文本缓存至Redis用于登录校验
String captchaId = IdUtil.fastSimpleUUID();
redisTemplate.opsForValue().set(
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId),
captchaCode,
captchaProperties.getExpireSeconds(),
TimeUnit.SECONDS
);
return CaptchaVO.builder()
.captchaId(captchaId)
.captchaBase64(imageBase64Data)
.build();
} }
/** /**

View File

@@ -5,16 +5,16 @@ import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.auth.model.vo.WechatMiniappLoginResult; import com.youlai.boot.auth.model.WxMaLoginResp;
import com.youlai.boot.auth.service.WechatMiniappAuthService; import com.youlai.boot.auth.service.WxMaAuthService;
import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.security.exception.NeedBindMobileException; import com.youlai.boot.framework.security.exception.NeedBindMobileException;
import com.youlai.boot.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.security.model.SysUserDetails; import com.youlai.boot.framework.security.model.SysUserDetails;
import com.youlai.boot.security.model.WechatMiniAuthenticationToken; import com.youlai.boot.framework.security.model.WechatMiniAuthenticationToken;
import com.youlai.boot.security.token.TokenManager; import com.youlai.boot.framework.security.token.TokenManager;
import com.youlai.boot.system.enums.SocialPlatformEnum; import com.youlai.boot.system.enums.SocialPlatformEnum;
import com.youlai.boot.system.model.entity.User; import com.youlai.boot.system.model.entity.SysUser;
import com.youlai.boot.system.service.UserSocialService; import com.youlai.boot.system.service.UserSocialService;
import com.youlai.boot.system.service.UserService; import com.youlai.boot.system.service.UserService;
import com.youlai.boot.system.service.UserRoleService; import com.youlai.boot.system.service.UserRoleService;
@@ -35,12 +35,12 @@ import java.util.Collections;
* 微信小程序认证服务实现 * 微信小程序认证服务实现
* *
* @author Ray.Hao * @author Ray.Hao
* @since 2.4.0 * @since 4.0.0
*/ */
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService { public class WxMaAuthServiceImpl implements WxMaAuthService {
private final WxMaService wxMaService; private final WxMaService wxMaService;
private final AuthenticationManager authenticationManager; private final AuthenticationManager authenticationManager;
@@ -54,16 +54,27 @@ public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService {
* 静默登录 * 静默登录
*/ */
@Override @Override
public WechatMiniappLoginResult silentLogin(String code) { public WxMaLoginResp silentLogin(String code) {
WechatMiniAuthenticationToken token = new WechatMiniAuthenticationToken(code); WechatMiniAuthenticationToken token = new WechatMiniAuthenticationToken(code);
try { try {
Authentication authentication = authenticationManager.authenticate(token); Authentication authentication = authenticationManager.authenticate(token);
AuthenticationToken authToken = tokenManager.generateToken(authentication); AuthenticationToken authToken = tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
return WechatMiniappLoginResult.success(authToken); return WxMaLoginResp.builder()
.isNewUser(false)
.needBindMobile(false)
.accessToken(authToken.getAccessToken())
.refreshToken(authToken.getRefreshToken())
.tokenType(authToken.getTokenType())
.expiresIn(authToken.getExpiresIn())
.build();
} catch (NeedBindMobileException e) { } catch (NeedBindMobileException e) {
return WechatMiniappLoginResult.needBindMobile(e.getOpenid()); return WxMaLoginResp.builder()
.isNewUser(true)
.needBindMobile(true)
.openid(e.getOpenid())
.build();
} }
} }
@@ -83,7 +94,7 @@ public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService {
log.info("微信小程序手机号快捷登录openid={}, mobile={}", openid, mobile); log.info("微信小程序手机号快捷登录openid={}, mobile={}", openid, mobile);
// 3. 查询或创建用户 // 3. 查询或创建用户
User user = findOrCreateUser(mobile); SysUser user = findOrCreateUser(mobile);
// 4. 绑定微信 openid // 4. 绑定微信 openid
bindWechatOpenid(user, session); bindWechatOpenid(user, session);
@@ -102,7 +113,7 @@ public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService {
validateSmsCode(mobile, smsCode); validateSmsCode(mobile, smsCode);
// 2. 查询或创建用户 // 2. 查询或创建用户
User user = findOrCreateUser(mobile); SysUser user = findOrCreateUser(mobile);
// 3. 绑定微信 openid // 3. 绑定微信 openid
userSocialService.bindOrUpdate( userSocialService.bindOrUpdate(
@@ -148,9 +159,9 @@ public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService {
/** /**
* 查询或创建用户 * 查询或创建用户
*/ */
private User findOrCreateUser(String mobile) { private SysUser findOrCreateUser(String mobile) {
User user = userService.lambdaQuery() SysUser user = userService.lambdaQuery()
.eq(User::getMobile, mobile) .eq(SysUser::getMobile, mobile)
.one(); .one();
if (user == null) { if (user == null) {
@@ -167,8 +178,8 @@ public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService {
* 新用户默认分配 GUEST访问游客角色 * 新用户默认分配 GUEST访问游客角色
* </p> * </p>
*/ */
private User createNewUser(String mobile) { private SysUser createNewUser(String mobile) {
User user = new User(); SysUser user = new SysUser();
user.setMobile(mobile); user.setMobile(mobile);
user.setUsername("wx_" + IdUtil.fastSimpleUUID().substring(0, 8)); user.setUsername("wx_" + IdUtil.fastSimpleUUID().substring(0, 8));
user.setNickname("微信用户"); user.setNickname("微信用户");
@@ -187,7 +198,7 @@ public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService {
/** /**
* 绑定微信 openid * 绑定微信 openid
*/ */
private void bindWechatOpenid(User user, WxMaJscode2SessionResult session) { private void bindWechatOpenid(SysUser user, WxMaJscode2SessionResult session) {
try { try {
userSocialService.bindOrUpdate( userSocialService.bindOrUpdate(
user.getId(), user.getId(),

View File

@@ -1,5 +1,6 @@
package com.youlai.boot.common.annotation; package com.youlai.boot.common.annotation;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.enums.LogModuleEnum;
import java.lang.annotation.*; import java.lang.annotation.*;
@@ -16,34 +17,31 @@ import java.lang.annotation.*;
public @interface Log { public @interface Log {
/** /**
* 日志描述 * 模块
* *
* @return 日志描述 * @return 模块
*/ */
String value() default "";
/**
* 日志模块
*
* @return 日志模块
*/
LogModuleEnum module(); LogModuleEnum module();
/** /**
* 是否记录请求参数 * 操作类型
* *
* @return 是否记录请求参数 * @return 操作类型
*/ */
boolean params() default true; ActionTypeEnum value();
/** /**
* 是否记录响应结果 * 操作标题(可选,默认使用枚举描述)
* <br/> *
* 响应结果默认不记录,避免日志过大 * @return 标题
* @return 是否记录响应结果
*/ */
boolean result() default false; String title() default "";
/**
* 自定义日志内容(可选,用于记录操作细节)
*
* @return 日志内容
*/
String content() default "";
} }

View File

@@ -1,6 +1,6 @@
package com.youlai.boot.common.annotation; package com.youlai.boot.common.annotation;
import com.youlai.boot.core.validator.FieldValidator; import com.youlai.boot.common.validator.FieldValidator;
import jakarta.validation.Constraint; import jakarta.validation.Constraint;
import jakarta.validation.Payload; import jakarta.validation.Payload;

View File

@@ -0,0 +1,136 @@
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.common.annotation.Log;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.util.IPUtils;
import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.system.model.entity.SysLog;
import com.youlai.boot.system.service.LogService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.LocalDateTime;
/**
* 日志切面
*
* @author Ray.Hao
* @since 2.10.0
*/
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class LogAspect {
private final LogService logService;
/**
* 日志注解切点
*/
@Pointcut("@annotation(logAnnotation)")
public void logPointCut(Log logAnnotation) {
}
/**
* 环绕通知:记录操作日志
*/
@Around(value = "logPointCut(logAnnotation)", argNames = "pjp,logAnnotation")
public Object around(ProceedingJoinPoint pjp, Log logAnnotation) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = null;
Exception exception = null;
try {
result = pjp.proceed();
return result;
} catch (Exception e) {
exception = e;
throw e;
} finally {
long executionTime = System.currentTimeMillis() - startTime;
saveLogAsync(logAnnotation, executionTime, exception);
}
}
/**
* 异步保存日志
*/
@Async
public void saveLogAsync(Log logAnnotation, long executionTime, Exception exception) {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return;
}
HttpServletRequest request = attributes.getRequest();
// 解析 User-Agent
String userAgentStr = request.getHeader("User-Agent");
UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
// 解析 IP 地区
String ip = IPUtils.getIpAddr(request);
String region = IPUtils.getRegion(ip);
String province = null;
String city = null;
if (StrUtil.isNotBlank(region)) {
String[] parts = region.split("\\|");
if (parts.length >= 3) {
province = StrUtil.blankToDefault(parts[2], null);
city = StrUtil.blankToDefault(parts[3], null);
}
}
// 获取当前用户信息
Long userId = SecurityUtils.getUserId();
String username = SecurityUtils.getUsername();
// 构建日志实体
LogModuleEnum module = logAnnotation.module();
ActionTypeEnum actionType = logAnnotation.value();
String title = StrUtil.blankToDefault(logAnnotation.title(),
module.getLabel() + "-" + actionType.getLabel());
String content = logAnnotation.content();
SysLog logEntity = new SysLog();
logEntity.setModule(module);
logEntity.setActionType(actionType);
logEntity.setTitle(title);
logEntity.setContent(content);
logEntity.setOperatorId(userId);
logEntity.setOperatorName(username);
logEntity.setRequestUri(request.getRequestURI());
logEntity.setRequestMethod(request.getMethod());
logEntity.setIp(ip);
logEntity.setProvince(province);
logEntity.setCity(city);
logEntity.setDevice(userAgent.getOs().getName());
logEntity.setOs(userAgent.getOs().getName());
logEntity.setBrowser(userAgent.getBrowser().getName());
logEntity.setStatus(exception == null ? 1 : 0);
logEntity.setErrorMsg(exception != null ? exception.getMessage() : null);
logEntity.setExecutionTime((int) executionTime);
logEntity.setCreateTime(LocalDateTime.now());
logService.save(logEntity);
} catch (Exception e) {
log.error("保存操作日志异常: {}", e.getMessage());
}
}
}

View File

@@ -1,11 +1,11 @@
package com.youlai.boot.core.aspect; package com.youlai.boot.common.aspect;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil; import cn.hutool.crypto.digest.DigestUtil;
import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.core.exception.BusinessException; import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.annotation.RepeatSubmit; import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.common.util.IPUtils; import com.youlai.boot.common.util.IPUtils;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;

View File

@@ -0,0 +1,56 @@
package com.youlai.boot.common.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import com.youlai.boot.common.base.IBaseEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
/**
* 操作类型枚举
*
* @author Ray
* @since 2.10.0
*/
@Schema(enumAsRef = true)
@Getter
public enum ActionTypeEnum implements IBaseEnum<Integer> {
LOGIN(1, "登录"),
LOGOUT(2, "登出"),
INSERT(3, "新增"),
UPDATE(4, "修改"),
DELETE(5, "删除"),
GRANT(6, "授权"),
EXPORT(7, "导出"),
IMPORT(8, "导入"),
UPLOAD(9, "上传"),
DOWNLOAD(10, "下载"),
CHANGE_PASSWORD(11, "修改密码"),
RESET_PASSWORD(12, "重置密码"),
ENABLE(13, "启用"),
DISABLE(14, "禁用"),
LIST(15, "查询列表"),
OTHER(99, "其他");
@EnumValue
private final Integer value;
@JsonValue
private final String label;
ActionTypeEnum(Integer value, String label) {
this.value = value;
this.label = label;
}
@Override
public Integer getValue() {
return this.value;
}
@Override
public String getLabel() {
return this.label;
}
}

View File

@@ -1,6 +1,8 @@
package com.youlai.boot.common.enums; package com.youlai.boot.common.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonValue;
import com.youlai.boot.common.base.IBaseEnum;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter; import lombok.Getter;
@@ -12,22 +14,39 @@ import lombok.Getter;
*/ */
@Schema(enumAsRef = true) @Schema(enumAsRef = true)
@Getter @Getter
public enum LogModuleEnum { public enum LogModuleEnum implements IBaseEnum<Integer> {
EXCEPTION("异常"), LOGIN(1, "登录"),
LOGIN("登录"), USER(2, "用户管理"),
USER("用户"), ROLE(3, "角色管理"),
DEPT("部门"), DEPT(4, "部门管理"),
ROLE("角色"), MENU(5, "菜单管理"),
MENU("菜单"), DICT(6, "字典管理"),
DICT("字典"), CONFIG(7, "系统配置"),
SETTING("系统配置"), FILE(8, "文件管理"),
OTHER("其他"); NOTICE(9, "通知公告"),
LOG(10, "日志管理"),
CODEGEN(11, "代码生成"),
OTHER(99, "其他");
@EnumValue
private final Integer value;
@JsonValue @JsonValue
private final String moduleName; private final String label;
LogModuleEnum(String moduleName) { LogModuleEnum(Integer value, String label) {
this.moduleName = moduleName; this.value = value;
this.label = label;
} }
}
@Override
public Integer getValue() {
return this.value;
}
@Override
public String getLabel() {
return this.label;
}
}

View File

@@ -1,52 +0,0 @@
package com.youlai.boot.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum RequestMethodEnum {
/**
* 搜寻 @AnonymousGetMapping
*/
GET("GET"),
/**
* 搜寻 @AnonymousPostMapping
*/
POST("POST"),
/**
* 搜寻 @AnonymousPutMapping
*/
PUT("PUT"),
/**
* 搜寻 @AnonymousPatchMapping
*/
PATCH("PATCH"),
/**
* 搜寻 @AnonymousDeleteMapping
*/
DELETE("DELETE"),
/**
* 否则就是所有 Request 接口都放行
*/
ALL("All");
/**
* Request 类型
*/
private final String type;
public static RequestMethodEnum find(String type) {
for (RequestMethodEnum value : RequestMethodEnum.values()) {
if (value.getType().equals(type)) {
return value;
}
}
return ALL;
}
}

View File

@@ -1,6 +1,6 @@
package com.youlai.boot.core.exception; package com.youlai.boot.common.exception;
import com.youlai.boot.core.web.IResultCode; import com.youlai.boot.common.result.IResultCode;
import lombok.Getter; import lombok.Getter;
import org.slf4j.helpers.MessageFormatter; import org.slf4j.helpers.MessageFormatter;

View File

@@ -27,4 +27,4 @@ public class KeyValue {
@Schema(description = "选项的标签") @Schema(description = "选项的标签")
private String value; private String value;
} }

View File

@@ -50,4 +50,4 @@ public class Option<T> {
@JsonInclude(value = JsonInclude.Include.NON_EMPTY) @JsonInclude(value = JsonInclude.Include.NON_EMPTY)
private List<Option<T>> children; private List<Option<T>> children;
} }

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.web; package com.youlai.boot.common.result;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.web; package com.youlai.boot.common.result;
/** /**
* 响应码接口 * 响应码接口

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.web; package com.youlai.boot.common.result;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.web; package com.youlai.boot.common.result;
import cn.hutool.extra.servlet.JakartaServletUtil; import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
@@ -10,7 +10,7 @@ import org.springframework.http.MediaType;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/** /**
* Web响应写入器 * 响应写入器
* <p> * <p>
* 用于在过滤器Security处理器等无法使用 @RestControllerAdvice 的场景中统一写入HTTP响应 * 用于在过滤器Security处理器等无法使用 @RestControllerAdvice 的场景中统一写入HTTP响应
* 支持写入成功响应和错误响应 * 支持写入成功响应和错误响应
@@ -20,12 +20,12 @@ import java.nio.charset.StandardCharsets;
* @since 2.0.0 * @since 2.0.0
*/ */
@Slf4j @Slf4j
public final class WebResponseWriter { public final class ResponseWriter {
/** /**
* 私有构造函数防止实例化 * 私有构造函数防止实例化
*/ */
private WebResponseWriter() { private ResponseWriter() {
throw new UnsupportedOperationException("工具类不允许实例化"); throw new UnsupportedOperationException("工具类不允许实例化");
} }
@@ -69,7 +69,7 @@ public final class WebResponseWriter {
Result<?> result = message == null Result<?> result = message == null
? Result.failed(resultCode) ? Result.failed(resultCode)
: Result.failed(resultCode, message); : Result.failed(resultCode, message);
int httpStatus = mapHttpStatus(resultCode); int httpStatus = mapHttpStatus(resultCode);
writeResult(response, result, httpStatus); writeResult(response, result, httpStatus);
} }
@@ -85,11 +85,11 @@ public final class WebResponseWriter {
try { try {
// 设置HTTP状态码 // 设置HTTP状态码
response.setStatus(httpStatus); response.setStatus(httpStatus);
// 设置响应编码和内容类型 // 设置响应编码和内容类型
response.setCharacterEncoding(StandardCharsets.UTF_8.toString()); response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setContentType(MediaType.APPLICATION_JSON_VALUE);
// 写入响应 // 写入响应
JakartaServletUtil.write(response, JakartaServletUtil.write(response,
JSONUtil.toJsonStr(result), JSONUtil.toJsonStr(result),
@@ -103,6 +103,9 @@ public final class WebResponseWriter {
/** /**
* 根据业务结果码映射HTTP状态码 * 根据业务结果码映射HTTP状态码
* 401: 未认证token无效/过期
* 403: 权限不足
* 400: 其他业务错误
* *
* @param resultCode 业务结果码 * @param resultCode 业务结果码
* @return HTTP状态码 * @return HTTP状态码
@@ -110,13 +113,10 @@ public final class WebResponseWriter {
private static int mapHttpStatus(ResultCode resultCode) { private static int mapHttpStatus(ResultCode resultCode) {
return switch (resultCode) { return switch (resultCode) {
case ACCESS_UNAUTHORIZED, case ACCESS_UNAUTHORIZED,
ACCESS_TOKEN_INVALID, ACCESS_TOKEN_INVALID,
REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
case ACCESS_PERMISSION_EXCEPTION -> HttpStatus.FORBIDDEN.value();
default -> HttpStatus.BAD_REQUEST.value(); default -> HttpStatus.BAD_REQUEST.value();
}; };
} }
} }

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.web; package com.youlai.boot.common.result;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.web; package com.youlai.boot.common.result;
import java.io.Serializable; import java.io.Serializable;

View File

@@ -1,61 +0,0 @@
package com.youlai.boot.common.util;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.format.annotation.DateTimeFormat;
import java.lang.reflect.Field;
/**
* 日期工具类
*
* @author haoxr
* @since 2.4.2
*/
public class DateUtils {
/**
* 区间日期格式化为数据库日期格式
* <p>
* eg2021-01-01 → 2021-01-01 00:00:00
*
* @param obj 要处理的对象
* @param startTimeFieldName 起始时间字段名
* @param endTimeFieldName 结束时间字段名
*/
public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) {
Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName);
Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName);
if (startTimeField != null) {
processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00");
}
if (endTimeField != null) {
processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59");
}
}
/**
* 处理日期字段
*
* @param obj 要处理的对象
* @param field 字段
* @param fieldName 字段名
* @param targetPattern 目标数据库日期格式
*/
private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) {
Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName);
if (fieldValue != null) {
// 得到原始的日期格式
String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd";
// 转换为日期对象
DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern);
// 转换为目标数据库日期格式
ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern));
}
}
}

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.validator; package com.youlai.boot.common.validator;
import com.youlai.boot.common.annotation.ValidField; import com.youlai.boot.common.annotation.ValidField;
import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidator;
@@ -18,16 +18,14 @@ public class FieldValidator implements ConstraintValidator<ValidField, String> {
@Override @Override
public void initialize(ValidField constraintAnnotation) { public void initialize(ValidField constraintAnnotation) {
// 初始化允许的值列表
this.allowedValues = constraintAnnotation.allowedValues(); this.allowedValues = constraintAnnotation.allowedValues();
} }
@Override @Override
public boolean isValid(String value, ConstraintValidatorContext context) { public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) { if (value == null) {
return true; // 如果字段允许为空可以返回 true return true;
} }
// 检查值是否在允许列表中
return Arrays.asList(allowedValues).contains(value); return Arrays.asList(allowedValues).contains(value);
} }
} }

View File

@@ -1,293 +0,0 @@
package com.youlai.boot.config;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.security.model.SysUserDetails;
import com.youlai.boot.security.token.TokenManager;
import com.youlai.boot.support.websocket.service.WebSocketService;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* WebSocket 配置类
*
* 核心功能:
* - 配置 WebSocket 端点
* - 配置消息代理
* - 实现连接认证与授权
* - 管理用户会话生命周期
*
* @author Ray.Hao
* @since 3.0.0
*/
@EnableWebSocketMessageBroker
@Configuration
@Slf4j
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private static final String WS_ENDPOINT = "/ws";
private static final String APP_DESTINATION_PREFIX = "/app";
private static final String USER_DESTINATION_PREFIX = "/user";
private static final String[] BROKER_DESTINATIONS = {"/topic", "/queue"};
private final TokenManager tokenManager;
private final WebSocketService webSocketService;
public WebSocketConfig(TokenManager tokenManager, @Lazy WebSocketService webSocketService) {
this.tokenManager = tokenManager;
this.webSocketService = webSocketService;
log.info("✓ WebSocket 配置已加载");
}
/**
* 注册 STOMP 端点
*
* 客户端通过该端点建立 WebSocket 连接
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint(WS_ENDPOINT)
.setAllowedOriginPatterns("*"); // 允许跨域(生产环境建议配置具体域名)
log.info("✓ STOMP 端点已注册: {}", WS_ENDPOINT);
}
/**
* 配置消息代理
*
* - /app 前缀:客户端发送消息到服务端的前缀
* - /topic 前缀:用于广播消息
* - /queue 前缀:用于点对点消息
* - /user 前缀:服务端发送给特定用户的消息前缀
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 客户端发送消息的请求前缀
registry.setApplicationDestinationPrefixes(APP_DESTINATION_PREFIX);
// 启用简单消息代理,处理 /topic 和 /queue 前缀的消息
registry.enableSimpleBroker(BROKER_DESTINATIONS);
// 服务端通知客户端的前缀
registry.setUserDestinationPrefix(USER_DESTINATION_PREFIX);
log.info("✓ 消息代理已配置: app={}, broker={}, user={}",
APP_DESTINATION_PREFIX, BROKER_DESTINATIONS, USER_DESTINATION_PREFIX);
}
/**
* 配置客户端入站通道拦截器
*
* 核心功能:
* 1. 连接建立时:解析 JWT Token 并绑定用户身份
* 2. 连接关闭时:触发用户下线通知
* 3. 安全防护:拦截无效连接请求
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
// 防御性检查:确保 accessor 不为空
if (accessor == null) {
log.warn("⚠ 收到异常消息:无法获取 StompHeaderAccessor");
return ChannelInterceptor.super.preSend(message, channel);
}
StompCommand command = accessor.getCommand();
if (command == null) {
return ChannelInterceptor.super.preSend(message, channel);
}
try {
switch (command) {
case CONNECT:
handleConnect(accessor);
break;
case DISCONNECT:
handleDisconnect(accessor);
break;
case SUBSCRIBE:
handleSubscribe(accessor);
break;
default:
// 其他命令不需要特殊处理
break;
}
} catch (AuthenticationException ex) {
// 认证失败时强制关闭连接
log.error("❌ 连接认证失败: {}", ex.getMessage());
throw ex;
} catch (Exception ex) {
// 捕获其他未知异常
log.error("❌ WebSocket 消息处理异常", ex);
throw new MessagingException("消息处理失败: " + ex.getMessage());
}
return ChannelInterceptor.super.preSend(message, channel);
}
});
log.info("✓ 客户端入站通道拦截器已配置");
}
/**
* 处理客户端连接请求
*
* 安全校验流程:
* 1. 提取 Authorization 头
* 2. 验证 Bearer Token 格式
* 3. 解析并验证 JWT 有效性
* 4. 绑定用户身份到当前会话
* 5. 记录用户上线状态
*/
private void handleConnect(StompHeaderAccessor accessor) {
String authorization = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
// 安全检查:确保 Authorization 头存在且格式正确
if (StrUtil.isBlank(authorization)) {
log.warn("⚠ 非法连接请求:缺少 Authorization 头");
throw new AuthenticationCredentialsNotFoundException("缺少 Authorization 头");
}
if (!authorization.startsWith("Bearer ")) {
log.warn("⚠ 非法连接请求Authorization 头格式错误");
throw new BadCredentialsException("Authorization 头格式错误");
}
// 提取 JWT Token移除 "Bearer " 前缀)
String token = authorization.substring(7);
if (StrUtil.isBlank(token)) {
log.warn("⚠ 非法连接请求Token 为空");
throw new BadCredentialsException("Token 为空");
}
// 解析并验证 Token
Authentication authentication;
try {
authentication = tokenManager.parseToken(token);
} catch (Exception ex) {
log.error("❌ Token 解析失败", ex);
throw new BadCredentialsException("Token 无效: " + ex.getMessage());
}
// 验证解析结果
if (authentication == null || !authentication.isAuthenticated()) {
log.warn("⚠ Token 解析失败:认证对象无效");
throw new BadCredentialsException("Token 解析失败");
}
// 获取用户详细信息
Object principal = authentication.getPrincipal();
if (!(principal instanceof SysUserDetails)) {
log.error("❌ 无效的用户凭证类型: {}", principal.getClass().getName());
throw new BadCredentialsException("用户凭证类型错误");
}
SysUserDetails userDetails = (SysUserDetails) principal;
String username = userDetails.getUsername();
if (StrUtil.isBlank(username)) {
log.warn("⚠ 用户名为空");
throw new BadCredentialsException("用户名为空");
}
// 绑定用户身份到当前会话(重要:用于 @SendToUser 等注解)
accessor.setUser(authentication);
// 获取会话 ID
String sessionId = accessor.getSessionId();
if (sessionId == null) {
log.warn("⚠ 会话 ID 为空,使用临时 ID");
sessionId = "temp-" + System.nanoTime();
}
// 记录用户上线状态
try {
webSocketService.userConnected(username, sessionId);
log.info("✓ WebSocket 连接建立成功: 用户[{}], 会话[{}]", username, sessionId);
} catch (Exception ex) {
log.error("❌ 记录用户上线状态失败: 用户[{}], 会话[{}]", username, sessionId, ex);
// 不抛出异常,允许连接继续
}
}
/**
* 处理客户端断开连接事件
*
* 注意:
* - 只有成功建立过认证的连接才会触发下线事件
* - 防止未认证成功的连接产生脏数据
*/
private void handleDisconnect(StompHeaderAccessor accessor) {
Authentication authentication = (Authentication) accessor.getUser();
// 防御性检查:只处理已认证的连接
if (authentication == null || !authentication.isAuthenticated()) {
log.debug("未认证的连接断开,跳过处理");
return;
}
Object principal = authentication.getPrincipal();
if (!(principal instanceof SysUserDetails)) {
log.warn("⚠ 断开连接时用户凭证类型异常");
return;
}
SysUserDetails userDetails = (SysUserDetails) principal;
String username = userDetails.getUsername();
if (StrUtil.isNotBlank(username)) {
try {
webSocketService.userDisconnected(username);
log.info("✓ WebSocket 连接断开: 用户[{}]", username);
} catch (Exception ex) {
log.error("❌ 记录用户下线状态失败: 用户[{}]", username, ex);
}
}
}
/**
* 处理客户端订阅事件(可选)
*
* 用于记录订阅信息或实施订阅级别的权限控制
*/
private void handleSubscribe(StompHeaderAccessor accessor) {
Authentication authentication = (Authentication) accessor.getUser();
if (authentication != null && authentication.isAuthenticated()) {
String destination = accessor.getDestination();
String username = authentication.getName();
log.debug("用户[{}]订阅主题: {}", username, destination);
// TODO: 这里可以实现订阅级别的权限控制
// 例如:检查用户是否有权限订阅某个主题
}
}
}

View File

@@ -1,243 +0,0 @@
package com.youlai.boot.core.aspect;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.TimeInterval;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import cn.hutool.json.JSONUtil;
import com.aliyun.oss.HttpMethod;
import com.youlai.boot.common.util.IPUtils;
import com.youlai.boot.security.util.SecurityUtils;
import com.youlai.boot.system.model.entity.Log;
import com.youlai.boot.system.service.LogService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.HandlerMapping;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
/**
* 日志切面
*
* @author Ray.Hao
* @since 2024/6/25
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class LogAspect {
private final LogService logService;
private final HttpServletRequest request;
private final CacheManager cacheManager;
/**
* 切点
*/
@Pointcut("@annotation(com.youlai.boot.common.annotation.Log)")
public void logPointcut() {
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@Around("logPointcut() && @annotation(logAnnotation)")
public Object doAround(ProceedingJoinPoint joinPoint, com.youlai.boot.common.annotation.Log logAnnotation) throws Throwable {
// 在方法执行前获取用户ID避免在方法执行过程中清除上下文导致获取不到用户ID
Long userId = SecurityUtils.getUserId();
TimeInterval timer = DateUtil.timer();
Object result = null;
Exception exception = null;
try {
result = joinPoint.proceed();
} catch (Exception e) {
exception = e;
throw e;
} finally {
long executionTime = timer.interval(); // 执行时长
this.saveLog(joinPoint, exception, result, logAnnotation, executionTime, userId);
}
return result;
}
/**
* 保存日志
*
* @param joinPoint 切点
* @param e 异常
* @param jsonResult 响应结果
* @param logAnnotation 日志注解
* @param userId 用户ID
*/
private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, com.youlai.boot.common.annotation.Log logAnnotation, long executionTime, Long userId) {
String requestURI = request.getRequestURI();
// 创建日志记录
Log log = new Log();
log.setExecutionTime(executionTime);
// 设置日志模块和内容
log.setModule(logAnnotation.module());
// 异常情况:追加异常信息到日志内容
if (e != null) {
log.setContent(logAnnotation.value() + "(失败:" + e.getMessage() + "");
// 请求参数(异常时也记录,便于排查问题)
this.setRequestParameters(joinPoint, log);
// 异常堆栈(截取前 2000 字符,避免过长)
String stackTrace = JSONUtil.toJsonStr(e.getStackTrace());
log.setResponseContent(StrUtil.sub(stackTrace, 0, 2000));
} else {
// 正常情况
log.setContent(logAnnotation.value());
// 请求参数
if (logAnnotation.params()) {
this.setRequestParameters(joinPoint, log);
}
// 响应结果
if (logAnnotation.result() && jsonResult != null) {
log.setResponseContent(JSONUtil.toJsonStr(jsonResult));
}
}
log.setRequestUri(requestURI);
log.setCreateBy(userId);
String ipAddr = IPUtils.getIpAddr(request);
if (StrUtil.isNotBlank(ipAddr)) {
log.setIp(ipAddr);
String region = IPUtils.getRegion(ipAddr);
// 中国|0|四川省|成都市|电信 解析省和市
if (StrUtil.isNotBlank(region)) {
String[] regionArray = region.split("\\|");
if (regionArray.length > 2) {
log.setProvince(regionArray[2]);
log.setCity(regionArray[3]);
}
}
}
// 获取浏览器和终端系统信息
String userAgentString = request.getHeader("User-Agent");
UserAgent userAgent = resolveUserAgent(userAgentString);
if (Objects.nonNull(userAgent)) {
// 系统信息
log.setOs(userAgent.getOs().getName());
// 浏览器信息
log.setBrowser(userAgent.getBrowser().getName());
log.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString));
}
//获取方法名
String methodName = joinPoint.getSignature().getName();
log.setMethod(methodName);
// 保存日志到数据库
logService.save(log);
}
/**
* 设置请求参数到日志对象中
*
* @param joinPoint 切点
* @param log 操作日志
*/
private void setRequestParameters(JoinPoint joinPoint, Log log) {
String requestMethod = request.getMethod();
log.setRequestMethod(requestMethod);
if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) {
String params = convertArgumentsToString(joinPoint.getArgs());
log.setRequestParams(StrUtil.sub(params, 0, 65535));
} else {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
Map<?, ?> paramsMap = (Map<?, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
log.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535));
} else {
log.setRequestParams("");
}
}
}
/**
* 将参数数组转换为字符串
*
* @param paramsArray 参数数组
* @return 参数字符串
*/
private String convertArgumentsToString(Object[] paramsArray) {
StringBuilder params = new StringBuilder();
if (paramsArray != null) {
for (Object param : paramsArray) {
if (!shouldFilterObject(param)) {
// 如果是基本类型或者枚举类型,直接添加到参数字符串中
if(param.getClass().isPrimitive() || param.getClass().isEnum()) {
params.append(param).append(" ");
} else {
params.append(JSONUtil.toJsonStr(param)).append(" ");
}
}
}
}
return params.toString().trim();
}
/**
* 判断是否需要过滤的对象。
*
* @param obj 对象信息。
* @return 如果是需要过滤的对象则返回true否则返回false。
*/
private boolean shouldFilterObject(Object obj) {
Class<?> clazz = obj.getClass();
if (clazz.isArray()) {
return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection<?> collection = (Collection<?>) obj;
return collection.stream().anyMatch(item -> item instanceof MultipartFile);
} else if (Map.class.isAssignableFrom(clazz)) {
Map<?, ?> map = (Map<?, ?>) obj;
return map.values().stream().anyMatch(value -> value instanceof MultipartFile);
}
return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse;
}
/**
* 解析UserAgent
*
* @param userAgentString UserAgent字符串
* @return UserAgent
*/
public UserAgent resolveUserAgent(String userAgentString) {
if (StrUtil.isBlank(userAgentString)) {
return null;
}
// 给userAgentStringMD5加密一次防止过长
String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString);
//判断是否命中缓存
UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class);
if (userAgent != null) {
return userAgent;
}
userAgent = UserAgentUtil.parse(userAgentString);
Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent);
return userAgent;
}
}

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.config; package com.youlai.boot.framework.cache;
import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.config; package com.youlai.boot.framework.cache;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.cache.autoconfigure.CacheProperties; import org.springframework.boot.cache.autoconfigure.CacheProperties;

View File

@@ -1,9 +1,13 @@
package com.youlai.boot.config; package com.youlai.boot.framework.cache;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer;
/** /**
@@ -29,11 +33,21 @@ public class RedisConfig {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setConnectionFactory(redisConnectionFactory);
// Key 使用 String 序列化
redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(RedisSerializer.json());
redisTemplate.setHashKeySerializer(RedisSerializer.string()); redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setHashValueSerializer(RedisSerializer.json());
// Value 使用自定义 JSON 序列化不写入类型信息避免 HashSet 等集合被序列化成带 @class 的结构
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 禁用类型信息写入避免集合类型名被当成元素
objectMapper.disableDefaultTyping();
Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
redisTemplate.setValueSerializer(jsonSerializer);
redisTemplate.setHashValueSerializer(jsonSerializer);
redisTemplate.afterPropertiesSet(); redisTemplate.afterPropertiesSet();
return redisTemplate; return redisTemplate;

View File

@@ -1,9 +1,8 @@
package com.youlai.boot.config; package com.youlai.boot.framework.captcha.config;
import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.captcha.generator.MathGenerator; import cn.hutool.captcha.generator.MathGenerator;
import cn.hutool.captcha.generator.RandomGenerator; import cn.hutool.captcha.generator.RandomGenerator;
import com.youlai.boot.config.property.CaptchaProperties;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.config.property; package com.youlai.boot.framework.captcha.config;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;

View File

@@ -0,0 +1,19 @@
package com.youlai.boot.framework.captcha.exception;
import com.youlai.boot.common.result.ResultCode;
import lombok.Getter;
/**
* 验证码异常
*/
@Getter
public class CaptchaException extends RuntimeException {
private final ResultCode resultCode;
public CaptchaException(ResultCode resultCode) {
super(resultCode.getMsg());
this.resultCode = resultCode;
}
}

View File

@@ -1,21 +1,22 @@
package com.youlai.boot.auth.model.vo; package com.youlai.boot.framework.captcha.model;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
/** /**
* 验证码信息 * 验证码信息
*
* @author RayHao
* @since 2023/03/24
*/ */
@Schema(description = "验证码信息")
@Data @Data
@Builder @Builder
public class CaptchaVO { @NoArgsConstructor
@AllArgsConstructor
@Schema(description = "验证码信息")
public class CaptchaInfo {
@Schema(description = "验证码缓存 ID") @Schema(description = "验证码缓存ID")
private String captchaId; private String captchaId;
@Schema(description = "验证码图片Base64字符串") @Schema(description = "验证码图片Base64字符串")

View File

@@ -0,0 +1,102 @@
package com.youlai.boot.framework.captcha.service;
import cn.hutool.captcha.AbstractCaptcha;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.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 lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.awt.Font;
import java.util.concurrent.TimeUnit;
/**
* 验证码服务
*/
@Service
@RequiredArgsConstructor
public class CaptchaService {
private final RedisTemplate<String, Object> redisTemplate;
private final CaptchaProperties captchaProperties;
private final CodeGenerator codeGenerator;
private final Font captchaFont;
/**
* 生成验证码
*/
public CaptchaInfo generate() {
String captchaType = captchaProperties.getType();
int width = captchaProperties.getWidth();
int height = captchaProperties.getHeight();
int interfereCount = captchaProperties.getInterfereCount();
int codeLength = captchaProperties.getCode().getLength();
AbstractCaptcha captcha;
if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount);
} else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength);
} else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount);
} else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) {
captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount);
} else {
throw new IllegalArgumentException("Invalid captcha type: " + captchaType);
}
captcha.setGenerator(codeGenerator);
captcha.setTextAlpha(captchaProperties.getTextAlpha());
captcha.setFont(captchaFont);
String captchaCode = captcha.getCode();
String imageBase64Data = captcha.getImageBase64Data();
String captchaId = IdUtil.fastSimpleUUID();
redisTemplate.opsForValue().set(
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId),
captchaCode,
captchaProperties.getExpireSeconds(),
TimeUnit.SECONDS
);
return CaptchaInfo.builder()
.captchaId(captchaId)
.captchaBase64(imageBase64Data)
.build();
}
/**
* 校验验证码,失败抛异常
*
* @param captchaId 验证码ID
* @param captchaCode 用户输入的验证码
* @throws CaptchaException 验证码错误或过期
*/
public void validate(String captchaId, String captchaCode) {
if (StrUtil.isBlank(captchaId) || StrUtil.isBlank(captchaCode)) {
throw new CaptchaException(ResultCode.USER_VERIFICATION_CODE_ERROR);
}
String cacheKey = StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId);
String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey);
if (cachedCode == null) {
throw new CaptchaException(ResultCode.USER_VERIFICATION_CODE_EXPIRED);
}
if (!codeGenerator.verify(cachedCode, captchaCode)) {
throw new CaptchaException(ResultCode.USER_VERIFICATION_CODE_ERROR);
}
redisTemplate.delete(cacheKey);
}
}

View File

@@ -1,6 +1,5 @@
package com.youlai.boot.config; package com.youlai.boot.framework.integration.mail.config;
import com.youlai.boot.config.property.MailProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.config.property; package com.youlai.boot.framework.integration.mail.config;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;

View File

@@ -1,7 +1,6 @@
package com.youlai.boot.support.mail.service.impl; package com.youlai.boot.framework.integration.mail.service;
import com.youlai.boot.config.property.MailProperties; import com.youlai.boot.framework.integration.mail.config.MailProperties;
import com.youlai.boot.support.mail.service.MailService;
import jakarta.mail.MessagingException; import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -15,15 +14,15 @@ import org.springframework.stereotype.Service;
import java.io.File; import java.io.File;
/** /**
* 邮件服务实现类 * 邮件服务
* *
* @author Ray * @author Ray.Hao
* @since 2024/8/17 * @since 2024/8/17
*/ */
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j @Slf4j
public class MailServiceImpl implements MailService { public class MailService {
private final JavaMailSender mailSender; private final JavaMailSender mailSender;
@@ -36,7 +35,6 @@ public class MailServiceImpl implements MailService {
* @param subject 邮件主题 * @param subject 邮件主题
* @param text 邮件内容 * @param text 邮件内容
*/ */
@Override
public void sendMail(String to, String subject, String text) { public void sendMail(String to, String subject, String text) {
try { try {
SimpleMailMessage message = new SimpleMailMessage(); SimpleMailMessage message = new SimpleMailMessage();
@@ -58,7 +56,6 @@ public class MailServiceImpl implements MailService {
* @param text 邮件内容 * @param text 邮件内容
* @param filePath 附件路径 * @param filePath 附件路径
*/ */
@Override
public void sendMailWithAttachment(String to, String subject, String text, String filePath) { public void sendMailWithAttachment(String to, String subject, String text, String filePath) {
MimeMessage message = mailSender.createMimeMessage(); MimeMessage message = mailSender.createMimeMessage();
try { try {

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.config.property; package com.youlai.boot.framework.integration.sms.config;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.support.sms.enums; package com.youlai.boot.framework.integration.sms.enums;
import com.youlai.boot.common.base.IBaseEnum; import com.youlai.boot.common.base.IBaseEnum;
import lombok.Getter; import lombok.Getter;

View File

@@ -1,6 +1,6 @@
package com.youlai.boot.support.sms.service; package com.youlai.boot.framework.integration.sms.service;
import com.youlai.boot.support.sms.enums.SmsTypeEnum; import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
import java.util.Map; import java.util.Map;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.support.sms.service.impl; package com.youlai.boot.framework.integration.sms.service.impl;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.aliyuncs.CommonRequest; import com.aliyuncs.CommonRequest;
@@ -8,9 +8,9 @@ import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType; import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile; import com.aliyuncs.profile.DefaultProfile;
import com.youlai.boot.config.property.AliyunSmsProperties; import com.youlai.boot.framework.integration.sms.config.AliyunSmsProperties;
import com.youlai.boot.support.sms.enums.SmsTypeEnum; import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
import com.youlai.boot.support.sms.service.SmsService; import com.youlai.boot.framework.integration.sms.service.SmsService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;

View File

@@ -1,9 +1,8 @@
package com.youlai.boot.config; package com.youlai.boot.framework.integration.wxma;
import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; 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.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.config.property; package com.youlai.boot.framework.integration.wxma;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.config; package com.youlai.boot.framework.job;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;

View File

@@ -1,12 +1,12 @@
package com.youlai.boot.config; package com.youlai.boot.framework.mybatis.config;
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.config.GlobalConfig; import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.youlai.boot.plugin.mybatis.MyDataPermissionHandler; import com.youlai.boot.framework.mybatis.handler.MyMetaObjectHandler;
import com.youlai.boot.plugin.mybatis.MyMetaObjectHandler; import com.youlai.boot.framework.mybatis.interceptor.MyDataPermissionHandler;
import org.apache.ibatis.mapping.DatabaseIdProvider; import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.mapping.VendorDatabaseIdProvider; import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.plugin.mybatis; package com.youlai.boot.framework.mybatis.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.plugin.mybatis; package com.youlai.boot.framework.mybatis.interceptor;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
@@ -6,9 +6,9 @@ import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
import com.youlai.boot.common.annotation.DataPermission; import com.youlai.boot.common.annotation.DataPermission;
import com.youlai.boot.common.enums.DataScopeEnum; import com.youlai.boot.common.enums.DataScopeEnum;
import com.youlai.boot.security.model.RoleDataScope; import com.youlai.boot.framework.security.model.RoleDataScope;
import com.youlai.boot.security.model.SysUserDetails; import com.youlai.boot.framework.security.model.SysUserDetails;
import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.framework.security.util.SecurityUtils;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.*; import net.sf.jsqlparser.expression.*;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.plugin.knife4j; package com.youlai.boot.framework.openapi;
import com.github.xiaoymin.knife4j.annotations.ApiSupport; import com.github.xiaoymin.knife4j.annotations.ApiSupport;
@@ -147,4 +147,4 @@ public class Knife4jOpenApiCustomizer extends com.github.xiaoymin.knife4j.spring
} }
return classes; return classes;
} }
} }

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.config; package com.youlai.boot.framework.openapi;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import com.youlai.boot.config.property.SecurityProperties; import com.youlai.boot.framework.security.config.SecurityProperties;
import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Contact;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.config; package com.youlai.boot.framework.security.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;

View File

@@ -1,20 +1,18 @@
package com.youlai.boot.config; package com.youlai.boot.framework.security.config;
import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.hutool.captcha.generator.CodeGenerator; import com.youlai.boot.framework.captcha.service.CaptchaService;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import com.youlai.boot.config.property.SecurityProperties; import com.youlai.boot.framework.web.filter.RateLimiterFilter;
import com.youlai.boot.core.filter.RateLimiterFilter; import com.youlai.boot.framework.security.filter.CaptchaValidationFilter;
import com.youlai.boot.security.filter.CaptchaValidationFilter; import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter;
import com.youlai.boot.security.filter.TokenAuthenticationFilter; import com.youlai.boot.framework.security.handler.MyAccessDeniedHandler;
import com.youlai.boot.security.handler.MyAccessDeniedHandler; import com.youlai.boot.framework.security.handler.MyAuthenticationEntryPoint;
import com.youlai.boot.security.handler.MyAuthenticationEntryPoint; import com.youlai.boot.framework.security.provider.SmsAuthenticationProvider;
import com.youlai.boot.security.provider.SmsAuthenticationProvider; import com.youlai.boot.framework.security.provider.WxMaAuthenticationProvider;
import com.youlai.boot.security.provider.WechatMiniAuthenticationProvider; import com.youlai.boot.framework.security.token.TokenManager;
import com.youlai.boot.security.token.TokenManager; import com.youlai.boot.framework.security.service.SysUserDetailsService;
import com.youlai.boot.security.service.SysUserDetailsService;
import com.youlai.boot.system.service.ConfigService; import com.youlai.boot.system.service.ConfigService;
import com.youlai.boot.system.service.UserSocialService;
import com.youlai.boot.system.service.UserService; import com.youlai.boot.system.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -53,22 +51,18 @@ public class SecurityConfig {
private final UserService userService; private final UserService userService;
private final SysUserDetailsService userDetailsService; private final SysUserDetailsService userDetailsService;
private final CodeGenerator codeGenerator; private final CaptchaService captchaService;
private final ConfigService configService; private final ConfigService configService;
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
private final WxMaService wxMaService;
private final UserSocialService userSocialService;
/** /**
* 配置安全过滤链 SecurityFilterChain * 配置安全过滤链 SecurityFilterChain
*/ */
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http return http
.authorizeHttpRequests(requestMatcherRegistry -> { .authorizeHttpRequests(requestMatcherRegistry -> {
// 配置无需登录即可访问的公开接口 // 配置无需登录即可访问的公开接口配置文件方式
String[] ignoreUrls = securityProperties.getIgnoreUrls(); String[] ignoreUrls = securityProperties.getIgnoreUrls();
if (ArrayUtil.isNotEmpty(ignoreUrls)) { if (ArrayUtil.isNotEmpty(ignoreUrls)) {
requestMatcherRegistry.requestMatchers(ignoreUrls).permitAll(); requestMatcherRegistry.requestMatchers(ignoreUrls).permitAll();
@@ -95,7 +89,7 @@ public class SecurityConfig {
// 限流过滤器 // 限流过滤器
.addFilterBefore(new RateLimiterFilter(redisTemplate, configService), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new RateLimiterFilter(redisTemplate, configService), UsernamePasswordAuthenticationFilter.class)
// 验证码校验过滤器 // 验证码校验过滤器
.addFilterBefore(new CaptchaValidationFilter(redisTemplate, codeGenerator), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new CaptchaValidationFilter(captchaService), UsernamePasswordAuthenticationFilter.class)
// 验证和解析过滤器 // 验证和解析过滤器
.addFilterBefore(new TokenAuthenticationFilter(tokenManager), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new TokenAuthenticationFilter(tokenManager), UsernamePasswordAuthenticationFilter.class)
.build(); .build();
@@ -138,8 +132,11 @@ public class SecurityConfig {
* 微信小程序认证 Provider * 微信小程序认证 Provider
*/ */
@Bean @Bean
public WechatMiniAuthenticationProvider wechatMiniAuthenticationProvider() { public WxMaAuthenticationProvider wechatMiniAuthenticationProvider(
return new WechatMiniAuthenticationProvider(wxMaService, userSocialService); WxMaService wxMaService,
SysUserDetailsService sysUserDetailsService
) {
return new WxMaAuthenticationProvider(wxMaService, sysUserDetailsService);
} }
/** /**
@@ -149,12 +146,13 @@ public class SecurityConfig {
public AuthenticationManager authenticationManager( public AuthenticationManager authenticationManager(
DaoAuthenticationProvider daoAuthenticationProvider, DaoAuthenticationProvider daoAuthenticationProvider,
SmsAuthenticationProvider smsAuthenticationProvider, SmsAuthenticationProvider smsAuthenticationProvider,
WechatMiniAuthenticationProvider wechatMiniAuthenticationProvider WxMaAuthenticationProvider wxMaAuthenticationProvider
) { ) {
return new ProviderManager( return new ProviderManager(
daoAuthenticationProvider, daoAuthenticationProvider,
smsAuthenticationProvider, smsAuthenticationProvider,
wechatMiniAuthenticationProvider wxMaAuthenticationProvider
); );
} }
} }

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.config.property; package com.youlai.boot.framework.security.config;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.security.exception; package com.youlai.boot.framework.security.exception;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.security.exception; package com.youlai.boot.framework.security.exception;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;

View File

@@ -1,20 +1,19 @@
package com.youlai.boot.security.filter; package com.youlai.boot.framework.security.filter;
import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.core.web.WebResponseWriter; import com.youlai.boot.common.result.ResponseWriter;
import com.youlai.boot.framework.captcha.exception.CaptchaException;
import com.youlai.boot.framework.captcha.service.CaptchaService;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream; import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
@@ -40,12 +39,10 @@ public class CaptchaValidationFilter extends OncePerRequestFilter {
public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode";
public static final String CAPTCHA_ID_PARAM_NAME = "captchaId"; public static final String CAPTCHA_ID_PARAM_NAME = "captchaId";
private final RedisTemplate<String, Object> redisTemplate; private final CaptchaService captchaService;
private final CodeGenerator codeGenerator;
public CaptchaValidationFilter(RedisTemplate<String, Object> redisTemplate, CodeGenerator codeGenerator) { public CaptchaValidationFilter(CaptchaService captchaService) {
this.redisTemplate = redisTemplate; this.captchaService = captchaService;
this.codeGenerator = codeGenerator;
} }
@Override @Override
@@ -61,7 +58,7 @@ public class CaptchaValidationFilter extends OncePerRequestFilter {
// 仅支持 JSON 登录 // 仅支持 JSON 登录
String contentType = request.getContentType(); String contentType = request.getContentType();
if (contentType == null || !contentType.contains(MediaType.APPLICATION_JSON_VALUE)) { if (contentType == null || !contentType.contains(MediaType.APPLICATION_JSON_VALUE)) {
WebResponseWriter.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR); ResponseWriter.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
return; return;
} }
@@ -79,24 +76,12 @@ public class CaptchaValidationFilter extends OncePerRequestFilter {
captchaId = jsonObject.getStr(CAPTCHA_ID_PARAM_NAME); captchaId = jsonObject.getStr(CAPTCHA_ID_PARAM_NAME);
} }
if (StrUtil.isBlank(captchaCode) || StrUtil.isBlank(captchaId)) { try {
WebResponseWriter.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR); captchaService.validate(captchaId, captchaCode);
return;
}
String cacheVerifyCode = (String) redisTemplate.opsForValue().get(
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId)
);
if (cacheVerifyCode == null) {
WebResponseWriter.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED);
return;
}
if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
HttpServletRequest repeatableRequest = new RepeatableReadRequestWrapper(requestWrapper, bodyBytes); HttpServletRequest repeatableRequest = new RepeatableReadRequestWrapper(requestWrapper, bodyBytes);
chain.doFilter(repeatableRequest, response); chain.doFilter(repeatableRequest, response);
} else { } catch (CaptchaException e) {
WebResponseWriter.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR); ResponseWriter.writeError(response, e.getResultCode());
} }
} }

View File

@@ -1,10 +1,10 @@
package com.youlai.boot.security.filter; package com.youlai.boot.framework.security.filter;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.core.web.WebResponseWriter; import com.youlai.boot.common.result.ResponseWriter;
import com.youlai.boot.security.token.TokenManager; import com.youlai.boot.framework.security.token.TokenManager;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@@ -40,19 +40,14 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); String rawToken = resolveToken(request);
try { try {
if (StrUtil.isNotBlank(authorizationHeader) if (StrUtil.isNotBlank(rawToken)) {
&& authorizationHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
// 剥离Bearer前缀获取原始令牌
String rawToken = authorizationHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
// 执行令牌有效性检查包含密码学验签和过期时间验证 // 执行令牌有效性检查包含密码学验签和过期时间验证
boolean isValidToken = tokenManager.validateToken(rawToken); boolean isValidToken = tokenManager.validateToken(rawToken);
if (!isValidToken) { if (!isValidToken) {
WebResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID); ResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
return; return;
} }
@@ -63,11 +58,23 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
} catch (Exception ex) { } catch (Exception ex) {
// 安全上下文清除保障防止上下文残留 // 安全上下文清除保障防止上下文残留
SecurityContextHolder.clearContext(); SecurityContextHolder.clearContext();
WebResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID); ResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
return; return;
} }
// 继续后续过滤器链执行 // 继续后续过滤器链执行
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
/**
* 从请求中解析 Token仅支持 Authorization Header
*/
private String resolveToken(HttpServletRequest request) {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StrUtil.isNotBlank(authorizationHeader)
&& authorizationHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
return authorizationHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
}
return null;
}
} }

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.security.handler; package com.youlai.boot.framework.security.handler;
import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.core.web.WebResponseWriter; import com.youlai.boot.common.result.ResponseWriter;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.access.AccessDeniedHandler;
@@ -18,7 +18,8 @@ public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override @Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) { public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
WebResponseWriter.writeError(response, ResultCode.ACCESS_UNAUTHORIZED); // 权限不足返回 403 Forbidden
ResponseWriter.writeError(response, ResultCode.ACCESS_PERMISSION_EXCEPTION);
} }
} }

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.security.handler; package com.youlai.boot.framework.security.handler;
import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.core.web.WebResponseWriter; import com.youlai.boot.common.result.ResponseWriter;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
@@ -32,13 +32,13 @@ public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
if (authException instanceof BadCredentialsException) { if (authException instanceof BadCredentialsException) {
// 用户名或密码错误 // 用户名或密码错误
WebResponseWriter.writeError(response, ResultCode.USER_PASSWORD_ERROR); ResponseWriter.writeError(response, ResultCode.USER_PASSWORD_ERROR);
} else if(authException instanceof InsufficientAuthenticationException){ } else if(authException instanceof InsufficientAuthenticationException){
// 请求头缺失AuthorizationToken格式错误Token过期签名验证失败 // 请求头缺失AuthorizationToken格式错误Token过期签名验证失败
WebResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID); ResponseWriter.writeError(response, ResultCode.ACCESS_TOKEN_INVALID);
} else { } else {
// 其他未明确处理的认证异常如账户被锁定账户禁用等 // 其他未明确处理的认证异常如账户被锁定账户禁用等
WebResponseWriter.writeError(response, ResultCode.USER_LOGIN_EXCEPTION, authException.getMessage()); ResponseWriter.writeError(response, ResultCode.USER_LOGIN_EXCEPTION, authException.getMessage());
} }
} }
} }

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.security.model; package com.youlai.boot.framework.security.model;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder; import lombok.Builder;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.security.model; package com.youlai.boot.framework.security.model;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.security.model; package com.youlai.boot.framework.security.model;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.security.model; package com.youlai.boot.framework.security.model;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.security.model; package com.youlai.boot.framework.security.model;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.security.model; package com.youlai.boot.framework.security.model;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.security.model; package com.youlai.boot.framework.security.model;
import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
@@ -10,7 +10,7 @@ import java.util.Collection;
/** /**
* 微信小程序认证 Token * 微信小程序认证 Token
*/ */
public class WechatMiniAuthenticationToken extends AbstractAuthenticationToken { public class WxMaAuthenticationToken extends AbstractAuthenticationToken {
@Serial @Serial
private static final long serialVersionUID = 622L; private static final long serialVersionUID = 622L;
@@ -34,7 +34,7 @@ public class WechatMiniAuthenticationToken extends AbstractAuthenticationToken {
* *
* @param code 微信小程序code * @param code 微信小程序code
*/ */
public WechatMiniAuthenticationToken(String code) { public WxMaAuthenticationToken(String code) {
super(AuthorityUtils.NO_AUTHORITIES); super(AuthorityUtils.NO_AUTHORITIES);
this.principal = code; this.principal = code;
this.credentials = null; this.credentials = null;
@@ -47,7 +47,7 @@ public class WechatMiniAuthenticationToken extends AbstractAuthenticationToken {
* @param principal 用户详情SysUserDetails * @param principal 用户详情SysUserDetails
* @param authorities 授权信息 * @param authorities 授权信息
*/ */
public WechatMiniAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { public WxMaAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities); super(authorities);
this.principal = principal; this.principal = principal;
this.credentials = null; this.credentials = null;
@@ -57,8 +57,8 @@ public class WechatMiniAuthenticationToken extends AbstractAuthenticationToken {
/** /**
* 创建已认证的 Token静态工厂方法 * 创建已认证的 Token静态工厂方法
*/ */
public static WechatMiniAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) { public static WxMaAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
return new WechatMiniAuthenticationToken(principal, authorities); return new WxMaAuthenticationToken(principal, authorities);
} }
@Override @Override

View File

@@ -1,12 +1,12 @@
package com.youlai.boot.security.provider; package com.youlai.boot.framework.security.provider;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.security.exception.CaptchaValidationException; import com.youlai.boot.framework.security.exception.CaptchaValidationException;
import com.youlai.boot.security.model.SmsAuthenticationToken; import com.youlai.boot.framework.security.model.SmsAuthenticationToken;
import com.youlai.boot.security.model.SysUserDetails; import com.youlai.boot.framework.security.model.SysUserDetails;
import com.youlai.boot.security.model.UserAuthInfo; import com.youlai.boot.framework.security.model.UserAuthInfo;
import com.youlai.boot.system.service.UserService; import com.youlai.boot.system.service.UserService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;

View File

@@ -1,15 +1,14 @@
package com.youlai.boot.security.provider; package com.youlai.boot.framework.security.provider;
import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.youlai.boot.security.exception.NeedBindMobileException; import com.youlai.boot.framework.security.exception.NeedBindMobileException;
import com.youlai.boot.security.model.SysUserDetails; import com.youlai.boot.framework.security.model.SysUserDetails;
import com.youlai.boot.security.model.UserAuthInfo; import com.youlai.boot.framework.security.model.UserAuthInfo;
import com.youlai.boot.security.model.WechatMiniAuthenticationToken; import com.youlai.boot.framework.security.model.WxMaAuthenticationToken;
import com.youlai.boot.system.enums.SocialPlatformEnum; import com.youlai.boot.framework.security.service.SysUserDetailsService;
import com.youlai.boot.system.model.entity.UserSocial; import com.youlai.boot.system.model.entity.UserSocial;
import com.youlai.boot.system.service.UserSocialService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.error.WxErrorException;
@@ -24,10 +23,10 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
*/ */
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class WechatMiniAuthenticationProvider implements AuthenticationProvider { public class WxMaAuthenticationProvider implements AuthenticationProvider {
private final WxMaService wxMaService; private final WxMaService wxMaService;
private final UserSocialService userSocialService; private final SysUserDetailsService sysUserDetailsService;
@Override @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
@@ -47,7 +46,7 @@ public class WechatMiniAuthenticationProvider implements AuthenticationProvider
log.info("微信小程序登录openid={}", openid); log.info("微信小程序登录openid={}", openid);
// 2. 根据 openid 查询绑定信息 // 2. 根据 openid 查询绑定信息
UserSocial userSocial = userSocialService.getByPlatformAndOpenid(SocialPlatformEnum.WECHAT_MINI, openid); UserSocial userSocial = sysUserDetailsService.getWechatMiniBindInfo(openid);
if (userSocial == null) { if (userSocial == null) {
// 未绑定抛出异常提示需要绑定手机号 // 未绑定抛出异常提示需要绑定手机号
@@ -56,7 +55,7 @@ public class WechatMiniAuthenticationProvider implements AuthenticationProvider
} }
// 3. 获取用户认证信息 // 3. 获取用户认证信息
UserAuthInfo userAuthInfo = userSocialService.getAuthInfoByOpenid(SocialPlatformEnum.WECHAT_MINI, openid); UserAuthInfo userAuthInfo = sysUserDetailsService.getAuthInfoByWechatOpenid(openid);
if (userAuthInfo == null) { if (userAuthInfo == null) {
log.warn("微信小程序登录失败用户不存在openid={}", openid); log.warn("微信小程序登录失败用户不存在openid={}", openid);
@@ -70,14 +69,14 @@ public class WechatMiniAuthenticationProvider implements AuthenticationProvider
} }
// 5. 更新 session_key // 5. 更新 session_key
userSocialService.updateSessionKey(userSocial.getId(), sessionKey); sysUserDetailsService.updateWechatSessionKey(userSocial.getId(), sessionKey);
// 6. 构建已认证 Token // 6. 构建已认证 Token
SysUserDetails userDetails = new SysUserDetails(userAuthInfo); SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
log.info("微信小程序登录成功username={}, openid={}", userAuthInfo.getUsername(), openid); log.info("微信小程序登录成功username={}, openid={}", userAuthInfo.getUsername(), openid);
return WechatMiniAuthenticationToken.authenticated(userDetails, userDetails.getAuthorities()); return WxMaAuthenticationToken.authenticated(userDetails, userDetails.getAuthorities());
} catch (WxErrorException e) { } catch (WxErrorException e) {
log.error("微信小程序登录失败调用微信接口异常code={}", code, e); log.error("微信小程序登录失败调用微信接口异常code={}", code, e);
@@ -87,7 +86,7 @@ public class WechatMiniAuthenticationProvider implements AuthenticationProvider
@Override @Override
public boolean supports(Class<?> authentication) { public boolean supports(Class<?> authentication) {
return WechatMiniAuthenticationToken.class.isAssignableFrom(authentication); return WxMaAuthenticationToken.class.isAssignableFrom(authentication);
} }
} }

View File

@@ -1,8 +1,8 @@
package com.youlai.boot.security.service; package com.youlai.boot.framework.security.service;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.system.service.RoleMenuService; import com.youlai.boot.system.service.RoleMenuService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;

View File

@@ -1,7 +1,10 @@
package com.youlai.boot.security.service; package com.youlai.boot.framework.security.service;
import com.youlai.boot.security.model.SysUserDetails; import com.youlai.boot.framework.security.model.SysUserDetails;
import com.youlai.boot.security.model.UserAuthInfo; import com.youlai.boot.framework.security.model.UserAuthInfo;
import com.youlai.boot.system.enums.SocialPlatformEnum;
import com.youlai.boot.system.model.entity.UserSocial;
import com.youlai.boot.system.service.UserSocialService;
import com.youlai.boot.system.service.UserService; import com.youlai.boot.system.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -22,6 +25,7 @@ import org.springframework.stereotype.Service;
public class SysUserDetailsService implements UserDetailsService { public class SysUserDetailsService implements UserDetailsService {
private final UserService userService; private final UserService userService;
private final UserSocialService userSocialService;
/** /**
* 根据用户名获取用户信息 * 根据用户名获取用户信息
@@ -45,4 +49,34 @@ public class SysUserDetailsService implements UserDetailsService {
throw e; throw e;
} }
} }
/**
* 根据微信小程序openid查询绑定信息
*
* @param openid 微信小程序openid
* @return 绑定信息未绑定返回null
*/
public UserSocial getWechatMiniBindInfo(String openid) {
return userSocialService.getByPlatformAndOpenid(SocialPlatformEnum.WECHAT_MINI, openid);
}
/**
* 根据微信小程序openid获取用户认证信息
*
* @param openid 微信小程序openid
* @return 用户认证信息用户不存在返回null
*/
public UserAuthInfo getAuthInfoByWechatOpenid(String openid) {
return userSocialService.getAuthInfoByOpenid(SocialPlatformEnum.WECHAT_MINI, openid);
}
/**
* 更新微信小程序session_key
*
* @param bindId 绑定记录ID
* @param sessionKey session_key
*/
public void updateWechatSessionKey(Long bindId, String sessionKey) {
userSocialService.updateSessionKey(bindId, sessionKey);
}
} }

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.security.token; package com.youlai.boot.framework.security.token;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
@@ -12,13 +12,13 @@ import cn.hutool.jwt.JWTUtil;
import com.youlai.boot.common.constant.JwtClaimConstants; import com.youlai.boot.common.constant.JwtClaimConstants;
import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.core.exception.BusinessException; import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.config.property.SecurityProperties; import com.youlai.boot.framework.security.config.SecurityProperties;
import com.youlai.boot.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.security.model.RoleDataScope; import com.youlai.boot.framework.security.model.RoleDataScope;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import com.youlai.boot.security.model.SysUserDetails; import com.youlai.boot.framework.security.model.SysUserDetails;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

View File

@@ -1,16 +1,16 @@
package com.youlai.boot.security.token; package com.youlai.boot.framework.security.token;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.core.exception.BusinessException; import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.config.property.SecurityProperties; import com.youlai.boot.framework.security.config.SecurityProperties;
import com.youlai.boot.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.security.model.UserSession; import com.youlai.boot.framework.security.model.UserSession;
import com.youlai.boot.security.model.SysUserDetails; import com.youlai.boot.framework.security.model.SysUserDetails;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.security.token; package com.youlai.boot.framework.security.token;
import com.youlai.boot.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.AuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
/** /**

View File

@@ -1,11 +1,11 @@
package com.youlai.boot.security.util; package com.youlai.boot.framework.security.util;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.constant.SystemConstants; import com.youlai.boot.common.constant.SystemConstants;
import com.youlai.boot.security.model.RoleDataScope; import com.youlai.boot.framework.security.model.RoleDataScope;
import com.youlai.boot.security.model.SysUserDetails; import com.youlai.boot.framework.security.model.SysUserDetails;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;

View File

@@ -1,9 +1,10 @@
package com.youlai.boot.core.exception; package com.youlai.boot.framework.web.advice;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import tools.jackson.core.JacksonException; import tools.jackson.core.JacksonException;
import com.youlai.boot.core.web.Result; import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.common.result.Result;
import com.youlai.boot.common.result.ResultCode;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException; import jakarta.validation.ConstraintViolationException;
@@ -45,7 +46,7 @@ public class GlobalExceptionHandler {
* 当请求参数绑定到对象时发生错误会抛出 BindException 异常 * 当请求参数绑定到对象时发生错误会抛出 BindException 异常
*/ */
@ExceptionHandler(BindException.class) @ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(BindException e) { public <T> Result<T> processException(BindException e) {
log.error("BindException:{}", e.getMessage()); log.error("BindException:{}", e.getMessage());
String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining("")); String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(""));
@@ -59,7 +60,7 @@ public class GlobalExceptionHandler {
* 会捕获到 ConstraintViolationException 异常 * 会捕获到 ConstraintViolationException 异常
*/ */
@ExceptionHandler(ConstraintViolationException.class) @ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(ConstraintViolationException e) { public <T> Result<T> processException(ConstraintViolationException e) {
log.error("ConstraintViolationException:{}", e.getMessage()); log.error("ConstraintViolationException:{}", e.getMessage());
String msg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining("")); String msg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(""));
@@ -73,7 +74,7 @@ public class GlobalExceptionHandler {
* 会抛出 MethodArgumentNotValidException 异常 * 会抛出 MethodArgumentNotValidException 异常
*/ */
@ExceptionHandler(MethodArgumentNotValidException.class) @ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(MethodArgumentNotValidException e) { public <T> Result<T> processException(MethodArgumentNotValidException e) {
log.error("MethodArgumentNotValidException:{}", e.getMessage()); log.error("MethodArgumentNotValidException:{}", e.getMessage());
String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining("")); String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(""));
@@ -98,7 +99,7 @@ public class GlobalExceptionHandler {
* 当请求缺少必需的参数时会抛出 MissingServletRequestParameterException 异常 * 当请求缺少必需的参数时会抛出 MissingServletRequestParameterException 异常
*/ */
@ExceptionHandler(MissingServletRequestParameterException.class) @ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(MissingServletRequestParameterException e) { public <T> Result<T> processException(MissingServletRequestParameterException e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY); return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY);
@@ -110,7 +111,7 @@ public class GlobalExceptionHandler {
* 当请求参数类型不匹配时会抛出 MethodArgumentTypeMismatchException 异常 * 当请求参数类型不匹配时会抛出 MethodArgumentTypeMismatchException 异常
*/ */
@ExceptionHandler(MethodArgumentTypeMismatchException.class) @ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(MethodArgumentTypeMismatchException e) { public <T> Result<T> processException(MethodArgumentTypeMismatchException e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误"); return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误");
@@ -122,7 +123,7 @@ public class GlobalExceptionHandler {
* Servlet 处理请求时发生异常时会抛出 ServletException 异常 * Servlet 处理请求时发生异常时会抛出 ServletException 异常
*/ */
@ExceptionHandler(ServletException.class) @ExceptionHandler(ServletException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(ServletException e) { public <T> Result<T> processException(ServletException e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
return Result.failed(e.getMessage()); return Result.failed(e.getMessage());
@@ -134,7 +135,7 @@ public class GlobalExceptionHandler {
* 当方法接收到非法参数时会抛出 IllegalArgumentException 异常 * 当方法接收到非法参数时会抛出 IllegalArgumentException 异常
*/ */
@ExceptionHandler(IllegalArgumentException.class) @ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) { public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) {
log.error("非法参数异常,异常原因:{}", e.getMessage(), e); log.error("非法参数异常,异常原因:{}", e.getMessage(), e);
return Result.failed(e.getMessage()); return Result.failed(e.getMessage());
@@ -146,7 +147,7 @@ public class GlobalExceptionHandler {
* 当处理 JSON 数据时发生错误会抛出 JacksonException 异常 * 当处理 JSON 数据时发生错误会抛出 JacksonException 异常
*/ */
@ExceptionHandler(JacksonException.class) @ExceptionHandler(JacksonException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> handleJacksonException(JacksonException e) { public <T> Result<T> handleJacksonException(JacksonException e) {
log.error("Json转换异常异常原因{}", e.getMessage(), e); log.error("Json转换异常异常原因{}", e.getMessage(), e);
return Result.failed(e.getMessage()); return Result.failed(e.getMessage());
@@ -158,7 +159,7 @@ public class GlobalExceptionHandler {
* 当请求体不可读时会抛出 HttpMessageNotReadableException 异常 * 当请求体不可读时会抛出 HttpMessageNotReadableException 异常
*/ */
@ExceptionHandler(HttpMessageNotReadableException.class) @ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(HttpMessageNotReadableException e) { public <T> Result<T> processException(HttpMessageNotReadableException e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
String errorMessage = "请求体不可为空"; String errorMessage = "请求体不可为空";
@@ -175,7 +176,7 @@ public class GlobalExceptionHandler {
* 当方法参数类型不匹配时会抛出 TypeMismatchException 异常 * 当方法参数类型不匹配时会抛出 TypeMismatchException 异常
*/ */
@ExceptionHandler(TypeMismatchException.class) @ExceptionHandler(TypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(TypeMismatchException e) { public <T> Result<T> processException(TypeMismatchException e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
return Result.failed(e.getMessage()); return Result.failed(e.getMessage());
@@ -187,7 +188,7 @@ public class GlobalExceptionHandler {
* SQL 语法错误时会抛出 BadSqlGrammarException 异常 * SQL 语法错误时会抛出 BadSqlGrammarException 异常
*/ */
@ExceptionHandler(BadSqlGrammarException.class) @ExceptionHandler(BadSqlGrammarException.class)
@ResponseStatus(HttpStatus.FORBIDDEN) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public <T> Result<T> handleBadSqlGrammarException(BadSqlGrammarException e) { public <T> Result<T> handleBadSqlGrammarException(BadSqlGrammarException e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
String errorMsg = e.getMessage(); String errorMsg = e.getMessage();
@@ -204,7 +205,7 @@ public class GlobalExceptionHandler {
* SQL 语法错误时会抛出 SQLSyntaxErrorException 异常 * SQL 语法错误时会抛出 SQLSyntaxErrorException 异常
*/ */
@ExceptionHandler(SQLSyntaxErrorException.class) @ExceptionHandler(SQLSyntaxErrorException.class)
@ResponseStatus(HttpStatus.FORBIDDEN) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) { public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
return Result.failed(ResultCode.DATABASE_EXECUTION_SYNTAX_ERROR); return Result.failed(ResultCode.DATABASE_EXECUTION_SYNTAX_ERROR);
@@ -217,7 +218,7 @@ public class GlobalExceptionHandler {
* SQL 违反了完整性约束时会抛出 SQLIntegrityConstraintViolationException 异常 * SQL 违反了完整性约束时会抛出 SQLIntegrityConstraintViolationException 异常
*/ */
@ExceptionHandler(SQLIntegrityConstraintViolationException.class) @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
@ResponseStatus(HttpStatus.FORBIDDEN) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public <T> Result<T> handleSQLIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException e) { public <T> Result<T> handleSQLIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
return Result.failed(ResultCode.INTEGRITY_CONSTRAINT_VIOLATION); return Result.failed(ResultCode.INTEGRITY_CONSTRAINT_VIOLATION);
@@ -229,7 +230,7 @@ public class GlobalExceptionHandler {
* 当业务逻辑发生错误时会抛出 BusinessException 异常 * 当业务逻辑发生错误时会抛出 BusinessException 异常
*/ */
@ExceptionHandler(BusinessException.class) @ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> handleBizException(BusinessException e) { public <T> Result<T> handleBizException(BusinessException e) {
log.error("biz exception", e); log.error("biz exception", e);
if (e.getResultCode() != null) { if (e.getResultCode() != null) {
@@ -244,7 +245,7 @@ public class GlobalExceptionHandler {
* 当发生未捕获的异常时会抛出 Exception 异常 * 当发生未捕获的异常时会抛出 Exception 异常
*/ */
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public <T> Result<T> handleException(Exception e) throws Exception { public <T> Result<T> handleException(Exception e) throws Exception {
// Spring Security 异常继续抛出以便交给自定义处理器处理 // Spring Security 异常继续抛出以便交给自定义处理器处理
if (e instanceof AccessDeniedException if (e instanceof AccessDeniedException
@@ -275,4 +276,4 @@ public class GlobalExceptionHandler {
} }
return group; return group;
} }
} }

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.config; package com.youlai.boot.framework.web.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.config; package com.youlai.boot.framework.web.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;

View File

@@ -1,12 +1,12 @@
package com.youlai.boot.core.filter; package com.youlai.boot.framework.web.filter;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SystemConstants; import com.youlai.boot.common.constant.SystemConstants;
import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.util.IPUtils; import com.youlai.boot.common.util.IPUtils;
import com.youlai.boot.core.web.WebResponseWriter; import com.youlai.boot.common.result.ResponseWriter;
import com.youlai.boot.system.service.ConfigService; import com.youlai.boot.system.service.ConfigService;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
@@ -88,7 +88,7 @@ public class RateLimiterFilter extends OncePerRequestFilter {
// 判断是否限流 // 判断是否限流
if (rateLimit(ip)) { if (rateLimit(ip)) {
// 返回限流错误信息 // 返回限流错误信息
WebResponseWriter.writeError(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED); ResponseWriter.writeError(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED);
return; return;
} }

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.filter; package com.youlai.boot.framework.web.filter;
import com.youlai.boot.common.util.IPUtils; import com.youlai.boot.common.util.IPUtils;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.config.property; package com.youlai.boot.module.codegen.config;
import cn.hutool.core.io.file.FileNameUtil; import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;

View File

@@ -1,17 +1,18 @@
package com.youlai.boot.tool.codegen.controller; package com.youlai.boot.module.codegen.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.core.web.PageResult; import com.youlai.boot.common.result.PageResult;
import com.youlai.boot.core.web.Result; import com.youlai.boot.common.result.Result;
import com.youlai.boot.config.property.CodegenProperties; import com.youlai.boot.module.codegen.config.CodegenProperties;
import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.tool.codegen.service.CodegenService; import com.youlai.boot.module.codegen.service.CodegenService;
import com.youlai.boot.tool.codegen.model.form.GenConfigForm; import com.youlai.boot.module.codegen.model.form.GenConfigForm;
import com.youlai.boot.tool.codegen.model.query.TableQuery; import com.youlai.boot.module.codegen.model.query.TableQuery;
import com.youlai.boot.tool.codegen.model.vo.CodegenPreviewVO; import com.youlai.boot.module.codegen.model.vo.CodegenPreviewVO;
import com.youlai.boot.tool.codegen.model.vo.TablePageVO; import com.youlai.boot.module.codegen.model.vo.TablePageVO;
import com.youlai.boot.common.annotation.Log; import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.tool.codegen.service.GenTableService; import com.youlai.boot.module.codegen.service.GenTableService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -45,7 +46,6 @@ public class CodegenController {
@Operation(summary = "获取数据表分页列表") @Operation(summary = "获取数据表分页列表")
@GetMapping("/table") @GetMapping("/table")
@Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER)
public PageResult<TablePageVO> getTablePage( public PageResult<TablePageVO> getTablePage(
TableQuery queryParams TableQuery queryParams
) { ) {
@@ -64,7 +64,7 @@ public class CodegenController {
@Operation(summary = "保存代码生成配置") @Operation(summary = "保存代码生成配置")
@PostMapping("/{tableName}/config") @PostMapping("/{tableName}/config")
@Log(value = "生成代码", module = LogModuleEnum.OTHER) @Log(module = LogModuleEnum.CODEGEN, value = ActionTypeEnum.UPDATE)
public Result<?> saveGenConfig(@RequestBody GenConfigForm formData) { public Result<?> saveGenConfig(@RequestBody GenConfigForm formData) {
genTableService.saveGenConfig(formData); genTableService.saveGenConfig(formData);
return Result.success(); return Result.success();
@@ -81,7 +81,6 @@ public class CodegenController {
@Operation(summary = "获取预览生成代码") @Operation(summary = "获取预览生成代码")
@GetMapping("/{tableName}/preview") @GetMapping("/{tableName}/preview")
@Log(value = "预览生成代码", module = LogModuleEnum.OTHER)
public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName, public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName,
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType, @RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType,
@RequestParam(value = "type", required = false, defaultValue = "ts") String type) { @RequestParam(value = "type", required = false, defaultValue = "ts") String type) {
@@ -91,7 +90,7 @@ public class CodegenController {
@Operation(summary = "下载代码") @Operation(summary = "下载代码")
@GetMapping("/{tableName}/download") @GetMapping("/{tableName}/download")
@Log(value = "下载代码", module = LogModuleEnum.OTHER) @Log(module = LogModuleEnum.CODEGEN, value = ActionTypeEnum.DOWNLOAD)
public void downloadZip(HttpServletResponse response, @PathVariable String tableName, public void downloadZip(HttpServletResponse response, @PathVariable String tableName,
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType, @RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType,
@RequestParam(value = "type", required = false, defaultValue = "ts") String type) { @RequestParam(value = "type", required = false, defaultValue = "ts") String type) {
@@ -106,8 +105,8 @@ public class CodegenController {
outputStream.write(data); outputStream.write(data);
outputStream.flush(); outputStream.flush();
} catch (IOException e) { } catch (IOException e) {
log.error("Error while writing the zip file to response", e); log.error("Error while writing the zip file1 to response", e);
throw new RuntimeException("Failed to write the zip file to response", e); throw new RuntimeException("Failed to write the zip file1 to response", e);
} }
} }
} }

View File

@@ -1,8 +1,8 @@
package com.youlai.boot.tool.codegen.converter; package com.youlai.boot.module.codegen.converter;
import com.youlai.boot.tool.codegen.model.entity.GenTable; import com.youlai.boot.module.codegen.model.entity.GenTable;
import com.youlai.boot.tool.codegen.model.entity.GenTableColumn; import com.youlai.boot.module.codegen.model.entity.GenTableColumn;
import com.youlai.boot.tool.codegen.model.form.GenConfigForm; import com.youlai.boot.module.codegen.model.form.GenConfigForm;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping; import org.mapstruct.Mapping;
@@ -38,4 +38,4 @@ public interface CodegenConverter {
GenTableColumn toGenTableColumn(GenConfigForm.FieldConfig fieldConfig); GenTableColumn toGenTableColumn(GenConfigForm.FieldConfig fieldConfig);
} }

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.codegen.enums; package com.youlai.boot.module.codegen.enums;
import com.baomidou.mybatisplus.annotation.EnumValue; import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.codegen.enums; package com.youlai.boot.module.codegen.enums;
import lombok.Getter; import lombok.Getter;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.codegen.enums; package com.youlai.boot.module.codegen.enums;
import com.baomidou.mybatisplus.annotation.EnumValue; import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;

View File

@@ -1,11 +1,11 @@
package com.youlai.boot.tool.codegen.mapper; package com.youlai.boot.module.codegen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.tool.codegen.model.bo.ColumnMetaData; import com.youlai.boot.module.codegen.model.vo.ColumnMetaVO;
import com.youlai.boot.tool.codegen.model.bo.TableMetaData; import com.youlai.boot.module.codegen.model.vo.TableMetaVO;
import com.youlai.boot.tool.codegen.model.query.TableQuery; import com.youlai.boot.module.codegen.model.query.TableQuery;
import com.youlai.boot.tool.codegen.model.vo.TablePageVO; import com.youlai.boot.module.codegen.model.vo.TablePageVO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.List; import java.util.List;
@@ -35,13 +35,7 @@ public interface DatabaseMapper extends BaseMapper {
* @param tableName * @param tableName
* @return * @return
*/ */
List<ColumnMetaData> getTableColumns(String tableName); List<ColumnMetaVO> getTableColumns(String tableName);
/** TableMetaVO getTableMetadata(String tableName);
* 获取表元数据
*
* @param tableName
* @return
*/
TableMetaData getTableMetadata(String tableName);
} }

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.tool.codegen.mapper; package com.youlai.boot.module.codegen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.boot.tool.codegen.model.entity.GenTableColumn; import com.youlai.boot.module.codegen.model.entity.GenTableColumn;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
/** /**

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.tool.codegen.mapper; package com.youlai.boot.module.codegen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.boot.tool.codegen.model.entity.GenTable; import com.youlai.boot.module.codegen.model.entity.GenTable;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
/** /**

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.codegen.model.entity; package com.youlai.boot.module.codegen.model.entity;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.*;

View File

@@ -1,12 +1,12 @@
package com.youlai.boot.tool.codegen.model.entity; package com.youlai.boot.module.codegen.model.entity;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.youlai.boot.common.base.BaseEntity; import com.youlai.boot.common.base.BaseEntity;
import com.youlai.boot.tool.codegen.enums.FormTypeEnum; import com.youlai.boot.module.codegen.enums.FormTypeEnum;
import com.youlai.boot.tool.codegen.enums.QueryTypeEnum; import com.youlai.boot.module.codegen.enums.QueryTypeEnum;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.tool.codegen.model.form; package com.youlai.boot.module.codegen.model.form;
import com.youlai.boot.tool.codegen.enums.FormTypeEnum; import com.youlai.boot.module.codegen.enums.FormTypeEnum;
import com.youlai.boot.tool.codegen.enums.QueryTypeEnum; import com.youlai.boot.module.codegen.enums.QueryTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;

Some files were not shown because too many files have changed in this diff Show More