# Conflicts:
#	pom.xml
#	src/main/java/com/youlai/boot/common/aspect/LogAspect.java
#	src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java
This commit is contained in:
2026-07-08 09:56:28 +08:00
130 changed files with 1629 additions and 2615 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 KiB

After

Width:  |  Height:  |  Size: 504 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

26
pom.xml
View File

@@ -6,7 +6,7 @@
<groupId>com.youlai</groupId> <groupId>com.youlai</groupId>
<artifactId>youlai-boot</artifactId> <artifactId>youlai-boot</artifactId>
<version>4.3.0</version> <version>4.3.2</version>
<description>基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。</description> <description>基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。</description>
<parent> <parent>
@@ -26,9 +26,9 @@
<druid.version>1.2.24</druid.version> <druid.version>1.2.24</druid.version>
<!-- Spring Boot 4.x 必须使用更新的版本 --> <!-- Spring Boot 4.x 必须使用更新的版本 -->
<mybatis-plus.version>3.5.15</mybatis-plus.version> <mybatis-plus.version>3.5.15</mybatis-plus.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
<knife4j.version>4.5.0</knife4j.version> <knife4j.version>4.5.0</knife4j.version>
<springdoc.version>2.8.9</springdoc.version>
<mapstruct.version>1.6.3</mapstruct.version> <mapstruct.version>1.6.3</mapstruct.version>
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version> <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
@@ -54,8 +54,8 @@
<ip2region.version>2.7.0</ip2region.version> <ip2region.version>2.7.0</ip2region.version>
<!-- 阿里云短信 --> <!-- 阿里云短信 -->
<aliyun.java.sdk.core.version>4.7.6</aliyun.java.sdk.core.version> <aliyun-sdk-core.version>4.7.6</aliyun-sdk-core.version>
<aliyun.java.sdk.dysmsapi.version>2.2.1</aliyun.java.sdk.dysmsapi.version> <aliyun-sdk-dysmsapi.version>2.2.1</aliyun-sdk-dysmsapi.version>
<caffeine.version>2.9.3</caffeine.version> <caffeine.version>2.9.3</caffeine.version>
@@ -64,7 +64,6 @@
<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>
@@ -81,7 +80,7 @@
<version>${hutool.version}</version> <version>${hutool.version}</version>
</dependency> </dependency>
<!-- TransmittableThreadLocal: 支持异步场景的租户上下文传递 --> <!-- TransmittableThreadLocal -->
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId> <artifactId>transmittable-thread-local</artifactId>
@@ -183,7 +182,7 @@
<dependency> <dependency>
<groupId>org.springdoc</groupId> <groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.9</version> <version>${springdoc.version}</version>
</dependency> </dependency>
<!-- MapStruct 对象映射 --> <!-- MapStruct 对象映射 -->
@@ -206,7 +205,7 @@
<version>${xxl-job.version}</version> <version>${xxl-job.version}</version>
</dependency> </dependency>
<!-- Excel 工具EasyExcel-PLus --> <!-- Excel 工具EasyExcel-Plus -->
<dependency> <dependency>
<groupId>cn.idev.excel</groupId> <groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId> <artifactId>fastexcel</artifactId>
@@ -258,13 +257,13 @@
<dependency> <dependency>
<groupId>com.aliyun</groupId> <groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId> <artifactId>aliyun-java-sdk-core</artifactId>
<version>${aliyun.java.sdk.core.version}</version> <version>${aliyun-sdk-core.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.aliyun</groupId> <groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId> <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>${aliyun.java.sdk.dysmsapi.version}</version> <version>${aliyun-sdk-dysmsapi.version}</version>
</dependency> </dependency>
<!-- 本地缓存 --> <!-- 本地缓存 -->
@@ -281,13 +280,6 @@
<version>${weixin-java-miniapp.version}</version> <version>${weixin-java-miniapp.version}</version>
</dependency> </dependency>
<!-- 动态多数据源 -->
<!--<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>${dynamic-datasource.version}</version>
</dependency>-->
<!--以5.3.0版本为例--> <!--以5.3.0版本为例-->
<!-- jiguang-sdk --> <!-- jiguang-sdk -->
<dependency> <dependency>

View File

@@ -116,10 +116,11 @@ CREATE TABLE `sys_menu` (
`parent_id` bigint NOT NULL COMMENT '父菜单ID', `parent_id` bigint NOT NULL COMMENT '父菜单ID',
`tree_path` varchar(255) COMMENT '父节点ID路径', `tree_path` varchar(255) COMMENT '父节点ID路径',
`name` varchar(64) NOT NULL COMMENT '菜单名称', `name` varchar(64) NOT NULL COMMENT '菜单名称',
`type` char(1) NOT NULL COMMENT '菜单类型C-目录 M-菜单 B-按钮)', `type` char(1) NOT NULL COMMENT '菜单类型C-目录 M-菜单 E-外链 B-按钮)',
`route_name` varchar(255) COMMENT '路由名称Vue Router 中用于命名路由)', `route_name` varchar(255) COMMENT '路由名称Vue Router 中用于命名路由)',
`route_path` varchar(128) COMMENT '路由路径Vue Router 中定义的 URL 路径)', `route_path` varchar(128) COMMENT '路由路径Vue Router 中定义的 URL 路径)',
`component` varchar(128) COMMENT '组件路径(组件页面完整路径,相对于 src/views/,缺省后缀 .vue', `component` varchar(128) COMMENT '组件路径(组件页面完整路径,相对于 src/views/,缺省后缀 .vue',
`external_url` varchar(512) COMMENT '外链地址',
`perm` varchar(128) COMMENT '【按钮】权限标识', `perm` varchar(128) COMMENT '【按钮】权限标识',
`always_show` tinyint DEFAULT 0 COMMENT '【目录】只有一个子路由是否始终显示1-是 0-否)', `always_show` tinyint DEFAULT 0 COMMENT '【目录】只有一个子路由是否始终显示1-是 0-否)',
`keep_alive` tinyint DEFAULT 0 COMMENT '【菜单】是否开启页面缓存1-是 0-否)', `keep_alive` tinyint DEFAULT 0 COMMENT '【菜单】是否开启页面缓存1-是 0-否)',
@@ -137,112 +138,112 @@ CREATE TABLE `sys_menu` (
-- Records of sys_menu -- Records of sys_menu
-- ---------------------------- -- ----------------------------
-- 顶级目录1-9系统/代码生成/文档/接口文档/组件/演示/多级/路由 -- 顶级目录1-9系统/代码生成/文档/接口文档/组件/演示/多级/路由
INSERT INTO `sys_menu` VALUES (1, 0, '0', '系统管理', 'C', '', '/system', 'Layout', NULL, NULL, NULL, 1, 1, 'system', '/system/user', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (1, 0, '0', '系统管理', 'C', '', '/system', 'Layout', NULL, NULL, NULL, 1, 1, 'system', '/system/user', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2, 0, '0', '代码生成', 'C', '', '/codegen', 'Layout', NULL, NULL, NULL, 1, 2, 'code', '/codegen/index', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2, 0, '0', '代码生成', 'C', '', '/codegen', 'Layout', NULL, NULL, NULL, 1, 2, 'code', '/codegen/index', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (4, 0, '0', '平台文档', 'C', '', '/doc', 'Layout', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (4, 0, '0', '平台文档', 'C', '', '/doc', 'Layout', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (5, 0, '0', '接口文档', 'C', '', '/api', 'Layout', NULL, NULL, NULL, 1, 5, 'api', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (5, 0, '0', '接口文档', 'C', '', '/api', 'Layout', NULL, NULL, NULL, 1, 5, 'api', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (6, 0, '0', '组件封装', 'C', '', '/component', 'Layout', NULL, NULL, NULL, 1, 6, 'menu', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (6, 0, '0', '组件封装', 'C', '', '/component', 'Layout', NULL, NULL, NULL, 1, 6, 'menu', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (7, 0, '0', '功能演示', 'C', '', '/function', 'Layout', NULL, NULL, NULL, 1, 7, 'menu', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (7, 0, '0', '功能演示', 'C', '', '/function', 'Layout', NULL, NULL, NULL, 1, 7, 'menu', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (8, 0, '0', '多级菜单', 'C', NULL, '/multi-level', 'Layout', NULL, 1, NULL, 1, 8, 'cascader', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (8, 0, '0', '多级菜单', 'C', NULL, '/multi-level', 'Layout', NULL, 1, NULL, 1, 8, 'cascader', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (9, 0, '0', '路由参数', 'C', '', '/route-param', 'Layout', NULL, NULL, NULL, 1, 9, 'el-icon-ElementPlus', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (9, 0, '0', '路由参数', 'C', '', '/route-param', 'Layout', NULL, NULL, NULL, 1, 9, 'el-icon-ElementPlus', '', now(), now(), NULL);
-- 系统管理 -- 系统管理
INSERT INTO `sys_menu` VALUES (210, 1, '0,1', '用户管理', 'M', 'User', 'user', 'system/user/index', NULL, NULL, 1, 1, 1, 'el-icon-User', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (210, 1, '0,1', '用户管理', 'M', 'User', 'user', 'system/user/index', NULL, NULL, 1, 1, 1, 'el-icon-User', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2101, 210, '0,1,210', '用户查询', 'B', NULL, '', NULL, 'sys:user:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2101, 210, '0,1,210', '用户查询', 'B', NULL, '', NULL, 'sys:user:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2102, 210, '0,1,210', '用户新增', 'B', NULL, '', NULL, 'sys:user:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2102, 210, '0,1,210', '用户新增', 'B', NULL, '', NULL, 'sys:user:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2103, 210, '0,1,210', '用户编辑', 'B', NULL, '', NULL, 'sys:user:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2103, 210, '0,1,210', '用户编辑', 'B', NULL, '', NULL, 'sys:user:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2104, 210, '0,1,210', '用户删除', 'B', NULL, '', NULL, 'sys:user:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2104, 210, '0,1,210', '用户删除', 'B', NULL, '', NULL, 'sys:user:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2105, 210, '0,1,210', '重置密码', 'B', NULL, '', NULL, 'sys:user:reset-password', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2105, 210, '0,1,210', '重置密码', 'B', NULL, '', NULL, 'sys:user:reset-password', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2106, 210, '0,1,210', '用户导入', 'B', NULL, '', NULL, 'sys:user:import', NULL, NULL, 1, 6, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2106, 210, '0,1,210', '用户导入', 'B', NULL, '', NULL, 'sys:user:import', NULL, NULL, 1, 6, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2107, 210, '0,1,210', '用户导出', 'B', NULL, '', NULL, 'sys:user:export', NULL, NULL, 1, 7, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2107, 210, '0,1,210', '用户导出', 'B', NULL, '', NULL, 'sys:user:export', NULL, NULL, 1, 7, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (220, 1, '0,1', '角色管理', 'M', 'Role', 'role', 'system/role/index', NULL, NULL, 1, 1, 2, 'role', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (220, 1, '0,1', '角色管理', 'M', 'Role', 'role', 'system/role/index', NULL, NULL, 1, 1, 2, 'role', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2201, 220, '0,1,220', '角色查询', 'B', NULL, '', NULL, 'sys:role:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2201, 220, '0,1,220', '角色查询', 'B', NULL, '', NULL, 'sys:role:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2202, 220, '0,1,220', '角色新增', 'B', NULL, '', NULL, 'sys:role:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2202, 220, '0,1,220', '角色新增', 'B', NULL, '', NULL, 'sys:role:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2203, 220, '0,1,220', '角色编辑', 'B', NULL, '', NULL, 'sys:role:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2203, 220, '0,1,220', '角色编辑', 'B', NULL, '', NULL, 'sys:role:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2204, 220, '0,1,220', '角色删除', 'B', NULL, '', NULL, 'sys:role:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2204, 220, '0,1,220', '角色删除', 'B', NULL, '', NULL, 'sys:role:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2205, 220, '0,1,220', '角色分配权限', 'B', NULL, '', NULL, 'sys:role:assign', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2205, 220, '0,1,220', '角色分配权限', 'B', NULL, '', NULL, 'sys:role:assign', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (230, 1, '0,1', '菜单管理', 'M', 'SysMenu', 'menu', 'system/menu/index', NULL, NULL, 1, 1, 3, 'menu', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (230, 1, '0,1', '菜单管理', 'M', 'SysMenu', 'menu', 'system/menu/index', NULL, NULL, 1, 1, 3, 'menu', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2301, 230, '0,1,230', '菜单查询', 'B', NULL, '', NULL, 'sys:menu:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2301, 230, '0,1,230', '菜单查询', 'B', NULL, '', NULL, 'sys:menu:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2302, 230, '0,1,230', '菜单新增', 'B', NULL, '', NULL, 'sys:menu:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2302, 230, '0,1,230', '菜单新增', 'B', NULL, '', NULL, 'sys:menu:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2303, 230, '0,1,230', '菜单编辑', 'B', NULL, '', NULL, 'sys:menu:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2303, 230, '0,1,230', '菜单编辑', 'B', NULL, '', NULL, 'sys:menu:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2304, 230, '0,1,230', '菜单删除', 'B', NULL, '', NULL, 'sys:menu:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2304, 230, '0,1,230', '菜单删除', 'B', NULL, '', NULL, 'sys:menu:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (240, 1, '0,1', '部门管理', 'M', 'Dept', 'dept', 'system/dept/index', NULL, NULL, 1, 1, 4, 'tree', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (240, 1, '0,1', '部门管理', 'M', 'Dept', 'dept', 'system/dept/index', NULL, NULL, 1, 1, 4, 'tree', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2401, 240, '0,1,240', '部门查询', 'B', NULL, '', NULL, 'sys:dept:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2401, 240, '0,1,240', '部门查询', 'B', NULL, '', NULL, 'sys:dept:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2402, 240, '0,1,240', '部门新增', 'B', NULL, '', NULL, 'sys:dept:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2402, 240, '0,1,240', '部门新增', 'B', NULL, '', NULL, 'sys:dept:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2403, 240, '0,1,240', '部门编辑', 'B', NULL, '', NULL, 'sys:dept:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2403, 240, '0,1,240', '部门编辑', 'B', NULL, '', NULL, 'sys:dept:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2404, 240, '0,1,240', '部门删除', 'B', NULL, '', NULL, 'sys:dept:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2404, 240, '0,1,240', '部门删除', 'B', NULL, '', NULL, 'sys:dept:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (250, 1, '0,1', '字典管理', 'M', 'Dict', 'dict', 'system/dict/index', NULL, NULL, 1, 1, 5, 'dict', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (250, 1, '0,1', '字典管理', 'M', 'Dict', 'dict', 'system/dict/index', NULL, NULL, 1, 1, 5, 'dict', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2501, 250, '0,1,250', '字典查询', 'B', NULL, '', NULL, 'sys:dict:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2501, 250, '0,1,250', '字典查询', 'B', NULL, '', NULL, 'sys:dict:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2502, 250, '0,1,250', '字典新增', 'B', NULL, '', NULL, 'sys:dict:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2502, 250, '0,1,250', '字典新增', 'B', NULL, '', NULL, 'sys:dict:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2503, 250, '0,1,250', '字典编辑', 'B', NULL, '', NULL, 'sys:dict:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2503, 250, '0,1,250', '字典编辑', 'B', NULL, '', NULL, 'sys:dict:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2504, 250, '0,1,250', '字典删除', 'B', NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2504, 250, '0,1,250', '字典删除', 'B', NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (251, 1, '0,1', '字典项', 'M', 'DictItem', 'dict-item', 'system/dict/dict-item', NULL, 0, 1, 0, 6, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (251, 1, '0,1', '字典项', 'M', 'DictItem', 'dict-item', 'system/dict/dict-item', NULL, 0, 1, 0, 6, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2511, 251, '0,1,251', '字典项查询', 'B', NULL, '', NULL, 'sys:dict-item:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2511, 251, '0,1,251', '字典项查询', 'B', NULL, '', NULL, 'sys:dict-item:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2512, 251, '0,1,251', '字典项新增', 'B', NULL, '', NULL, 'sys:dict-item:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2512, 251, '0,1,251', '字典项新增', 'B', NULL, '', NULL, 'sys:dict-item:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2513, 251, '0,1,251', '字典项编辑', 'B', NULL, '', NULL, 'sys:dict-item:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2513, 251, '0,1,251', '字典项编辑', 'B', NULL, '', NULL, 'sys:dict-item:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2514, 251, '0,1,251', '字典项删除', 'B', NULL, '', NULL, 'sys:dict-item:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2514, 251, '0,1,251', '字典项删除', 'B', NULL, '', NULL, 'sys:dict-item:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (260, 1, '0,1', '系统日志', 'M', 'Log', 'log', 'system/log/index', NULL, 0, 1, 1, 7, 'document', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (260, 1, '0,1', '系统日志', 'M', 'Log', 'log', 'system/log/index', NULL, 0, 1, 1, 7, 'document', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2601, 260, '0,1,260', '日志查询', 'B', NULL, '', NULL, 'sys:log:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2601, 260, '0,1,260', '日志查询', 'B', NULL, '', NULL, 'sys:log:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (270, 1, '0,1', '系统配置', 'M', 'Config', 'config', 'system/config/index', NULL, 0, 1, 1, 8, 'setting', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (270, 1, '0,1', '系统配置', 'M', 'Config', 'config', 'system/config/index', NULL, 0, 1, 1, 8, 'setting', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2701, 270, '0,1,270', '系统配置查询', 'B', NULL, '', NULL, 'sys:config:list', 0, 1, 1, 1, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2701, 270, '0,1,270', '系统配置查询', 'B', NULL, '', NULL, 'sys:config:list', 0, 1, 1, 1, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2702, 270, '0,1,270', '系统配置新增', 'B', NULL, '', NULL, 'sys:config:create', 0, 1, 1, 2, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2702, 270, '0,1,270', '系统配置新增', 'B', NULL, '', NULL, 'sys:config:create', 0, 1, 1, 2, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2703, 270, '0,1,270', '系统配置修改', 'B', NULL, '', NULL, 'sys:config:update', 0, 1, 1, 3, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2703, 270, '0,1,270', '系统配置修改', 'B', NULL, '', NULL, 'sys:config:update', 0, 1, 1, 3, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2704, 270, '0,1,270', '系统配置删除', 'B', NULL, '', NULL, 'sys:config:delete', 0, 1, 1, 4, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2704, 270, '0,1,270', '系统配置删除', 'B', NULL, '', NULL, 'sys:config:delete', 0, 1, 1, 4, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2705, 270, '0,1,270', '系统配置刷新', 'B', NULL, '', NULL, 'sys:config:refresh', 0, 1, 1, 5, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2705, 270, '0,1,270', '系统配置刷新', 'B', NULL, '', NULL, 'sys:config:refresh', 0, 1, 1, 5, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (280, 1, '0,1', '通知公告', 'M', 'Notice', 'notice', 'system/notice/index', NULL, NULL, NULL, 1, 9, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (280, 1, '0,1', '通知公告', 'M', 'Notice', 'notice', 'system/notice/index', NULL, NULL, NULL, 1, 9, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2801, 280, '0,1,280', '通知查询', 'B', NULL, '', NULL, 'sys:notice:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2801, 280, '0,1,280', '通知查询', 'B', NULL, '', NULL, 'sys:notice:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2802, 280, '0,1,280', '通知新增', 'B', NULL, '', NULL, 'sys:notice:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2802, 280, '0,1,280', '通知新增', 'B', NULL, '', NULL, 'sys:notice:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2803, 280, '0,1,280', '通知编辑', 'B', NULL, '', NULL, 'sys:notice:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2803, 280, '0,1,280', '通知编辑', 'B', NULL, '', NULL, 'sys:notice:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2804, 280, '0,1,280', '通知删除', 'B', NULL, '', NULL, 'sys:notice:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2804, 280, '0,1,280', '通知删除', 'B', NULL, '', NULL, 'sys:notice:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2805, 280, '0,1,280', '通知发布', 'B', NULL, '', NULL, 'sys:notice:publish', 0, 1, 1, 5, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2805, 280, '0,1,280', '通知发布', 'B', NULL, '', NULL, 'sys:notice:publish', 0, 1, 1, 5, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (2806, 280, '0,1,280', '通知撤回', 'B', NULL, '', NULL, 'sys:notice:revoke', 0, 1, 1, 6, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (2806, 280, '0,1,280', '通知撤回', 'B', NULL, '', NULL, 'sys:notice:revoke', 0, 1, 1, 6, '', NULL, now(), now(), NULL);
-- 代码生成 -- 代码生成
INSERT INTO `sys_menu` VALUES (310, 2, '0,2', '代码生成', 'M', 'Codegen', 'codegen', 'codegen/index', NULL, NULL, 1, 1, 1, 'code', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (310, 2, '0,2', '代码生成', 'M', 'Codegen', 'codegen', 'codegen/index', NULL, NULL, 1, 1, 1, 'code', NULL, now(), now(), NULL);
-- 平台文档(外链通过 route_path 识别) -- 平台文档
INSERT INTO `sys_menu` VALUES (501, 4, '0,4', '平台文档(外链)', 'M', NULL, 'https://juejin.cn/post/7228990409909108793', '', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `external_url`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (501, 4, '0,4', '平台文档(外链)', 'E', NULL, NULL, NULL, 'https://juejin.cn/post/7228990409909108793', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (502, 4, '0,4', '后端文档', 'M', NULL, 'https://youlai.blog.csdn.net/article/details/145178880', '', NULL, NULL, NULL, 1, 2, 'document', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `external_url`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (502, 4, '0,4', '后端文档', 'E', NULL, NULL, NULL, 'https://youlai.blog.csdn.net/article/details/145178880', NULL, NULL, NULL, 1, 2, 'document', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (503, 4, '0,4', '移动端文档', 'M', NULL, 'https://youlai.blog.csdn.net/article/details/143222890', '', NULL, NULL, NULL, 1, 3, 'document', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `external_url`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (503, 4, '0,4', '移动端文档', 'E', NULL, NULL, NULL, 'https://youlai.blog.csdn.net/article/details/143222890', NULL, NULL, NULL, 1, 3, 'document', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (504, 4, '0,4', '内部文档', 'M', NULL, 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `external_url`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (504, 4, '0,4', '内部文档', 'E', 'InternalDoc', 'internal-doc', 'iframe', 'https://juejin.cn/post/7228990409909108793', NULL, NULL, 1, 1, 4, 'document', '', now(), now(), NULL);
-- 接口文档 -- 接口文档
INSERT INTO `sys_menu` VALUES (601, 5, '0,5', 'Apifox', 'M', 'Apifox', 'apifox', 'demo/api/apifox', NULL, NULL, 1, 1, 1, 'api', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `external_url`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (601, 5, '0,5', 'Apifox', 'E', 'Apifox', 'apifox', 'iframe', 'https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5', NULL, NULL, 1, 1, 1, 'api', '', now(), now(), NULL);
-- 组件封装 -- 组件封装
INSERT INTO `sys_menu` VALUES (701, 6, '0,6', '富文本编辑器', 'M', 'WangEditor', 'wang-editor', 'demo/wang-editor', NULL, NULL, 1, 1, 2, '', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (701, 6, '0,6', '富文本编辑器', 'M', 'WangEditor', 'wang-editor', 'demo/wang-editor', NULL, NULL, 1, 1, 2, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (702, 6, '0,6', '图片上传', 'M', 'Upload', 'upload', 'demo/upload', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (702, 6, '0,6', '图片上传', 'M', 'Upload', 'upload', 'demo/upload', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (703, 6, '0,6', '图标选择器', 'M', 'IconSelect', 'icon-select', 'demo/icon-select', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (703, 6, '0,6', '图标选择器', 'M', 'IconSelect', 'icon-select', 'demo/icon-select', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (704, 6, '0,6', '字典组件', 'M', 'DictDemo', 'dict-demo', 'demo/dictionary', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (704, 6, '0,6', '字典组件', 'M', 'DictDemo', 'dict-demo', 'demo/dictionary', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (705, 6, '0,6', '增删改查', 'M', 'Curd', 'curd', 'demo/curd/index', NULL, NULL, 1, 1, 0, '', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (705, 6, '0,6', '增删改查', 'M', 'Curd', 'curd', 'demo/curd/index', NULL, NULL, 1, 1, 0, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (706, 6, '0,6', '列表选择器', 'M', 'TableSelect', 'table-select', 'demo/table-select/index', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (706, 6, '0,6', '列表选择器', 'M', 'TableSelect', 'table-select', 'demo/table-select/index', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (707, 6, '0,6', '拖拽组件', 'M', 'Drag', 'drag', 'demo/drag', NULL, NULL, NULL, 1, 5, '', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (707, 6, '0,6', '拖拽组件', 'M', 'Drag', 'drag', 'demo/drag', NULL, NULL, NULL, 1, 5, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (708, 6, '0,6', '滚动文本', 'M', 'TextScroll', 'text-scroll', 'demo/text-scroll', NULL, NULL, NULL, 1, 6, '', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (708, 6, '0,6', '滚动文本', 'M', 'TextScroll', 'text-scroll', 'demo/text-scroll', NULL, NULL, NULL, 1, 6, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (709, 6, '0,6', '自适应表格操作列', 'M', 'AutoOperationColumn', 'operation-column', 'demo/auto-operation-column', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (709, 6, '0,6', '自适应表格操作列', 'M', 'AutoOperationColumn', 'operation-column', 'demo/auto-operation-column', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL);
-- 功能演示 -- 功能演示
INSERT INTO `sys_menu` VALUES (801, 7, '0,7', 'Icons', 'M', 'IconDemo', 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (801, 7, '0,7', 'Icons', 'M', 'IconDemo', 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (802, 7, '0,7', '字典实时同步', 'M', 'DictSync', 'dict-sync', 'demo/dict-sync', NULL, NULL, NULL, 1, 3, '', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (802, 7, '0,7', '字典实时同步', 'M', 'DictSync', 'dict-sync', 'demo/dict-sync', NULL, NULL, NULL, 1, 3, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (803, 7, '0,7', 'VxeTable', 'M', 'VxeTable', 'vxe-table', 'demo/vxe-table/index', NULL, NULL, 1, 1, 4, 'el-icon-MagicStick', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (803, 7, '0,7', 'VxeTable', 'M', 'VxeTable', 'vxe-table', 'demo/vxe-table/index', NULL, NULL, 1, 1, 4, 'el-icon-MagicStick', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (804, 7, '0,7', 'CURD单文件', 'M', 'CurdSingle', 'curd-single', 'demo/curd-single', NULL, NULL, 1, 1, 5, 'el-icon-Reading', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (804, 7, '0,7', 'CURD单文件', 'M', 'CurdSingle', 'curd-single', 'demo/curd-single', NULL, NULL, 1, 1, 5, 'el-icon-Reading', '', now(), now(), NULL);
-- 多级菜单示例 -- 多级菜单示例
INSERT INTO `sys_menu` VALUES (910, 8, '0,8', '菜单一级', 'C', NULL, 'multi-level1', 'Layout', NULL, 1, NULL, 1, 1, '', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (910, 8, '0,8', '一级菜单', 'C', NULL, 'level-one', 'Layout', NULL, 1, NULL, 1, 1, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (911, 910, '0,8,910', '菜单二级', 'C', NULL, 'multi-level2', 'Layout', NULL, 0, NULL, 1, 1, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (911, 910, '0,8,910', '二级菜单', 'C', NULL, 'level-two', 'Layout', NULL, 0, NULL, 1, 1, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (912, 911, '0,8,910,911', '菜单三级-1', 'M', NULL, 'multi-level3-1', 'demo/multi-level/children/children/level3-1', NULL, 0, 1, 1, 1, '', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (912, 911, '0,8,910,911', '三级菜单 A', 'M', 'MultiLevelLevelThreeA', 'level-three-a', 'demo/multi-level/level-one/level-two/level-three-a/index', NULL, 0, 1, 1, 1, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (913, 911, '0,8,910,911', '菜单三级-2', 'M', NULL, 'multi-level3-2', 'demo/multi-level/children/children/level3-2', NULL, 0, 1, 1, 2, '', '', now(), now(), NULL); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (913, 911, '0,8,910,911', '三级菜单 B', 'M', 'MultiLevelLevelThreeB', 'level-three-b', 'demo/multi-level/level-one/level-two/level-three-b/index', NULL, 0, 1, 1, 2, '', '', now(), now(), NULL);
-- 路由参数 -- 路由参数
INSERT INTO `sys_menu` VALUES (1001, 9, '0,9', '参数(type=1)', 'M', 'RouteParamType1', 'route-param-type1', 'demo/route-param', NULL, 0, 1, 1, 1, 'el-icon-Star', NULL, now(), now(), '{\"type\": \"1\"}'); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (1001, 9, '0,9', '参数(type=1)', 'M', 'RouteParamType1', 'route-param-type1', 'demo/route-param', NULL, 0, 1, 1, 1, 'el-icon-Star', NULL, now(), now(), '{\"type\": \"1\"}');
INSERT INTO `sys_menu` VALUES (1002, 9, '0,9', '参数(type=2)', 'M', 'RouteParamType2', 'route-param-type2', 'demo/route-param', NULL, 0, 1, 1, 2, 'el-icon-StarFilled', NULL, now(), now(), '{\"type\": \"2\"}'); INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) VALUES (1002, 9, '0,9', '参数(type=2)', 'M', 'RouteParamType2', 'route-param-type2', 'demo/route-param', NULL, 0, 1, 1, 2, 'el-icon-StarFilled', NULL, now(), now(), '{\"type\": \"2\"}');
-- ---------------------------- -- ----------------------------
-- Table structure for sys_role -- Table structure for sys_role
@@ -553,16 +554,39 @@ CREATE TABLE `sys_user_notice` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户通知公告关联表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户通知公告关联表';
INSERT INTO `sys_user_notice` VALUES (1, 1, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (1, 1, 2, 1, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (2, 2, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (2, 2, 2, 0, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (3, 3, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (3, 3, 2, 0, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (4, 4, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (4, 4, 2, 1, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (5, 5, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (5, 5, 2, 0, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (6, 6, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (6, 6, 2, 1, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (7, 7, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (7, 7, 2, 0, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (8, 8, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (8, 8, 2, 1, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (9, 9, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (9, 9, 2, 0, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0);
-- root 用户 (user_id=1) 绑定全部通知5 条未读展示红点效果
INSERT INTO `sys_user_notice` VALUES (11, 1, 1, 0, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (12, 2, 1, 1, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (13, 3, 1, 0, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (14, 4, 1, 1, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (15, 5, 1, 0, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (16, 6, 1, 1, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (17, 7, 1, 0, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (18, 8, 1, 1, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (19, 9, 1, 0, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (20, 10, 1, 1, NULL, now(), now(), 0);
-- test 用户 (user_id=3) 绑定除指定通知(ID=7)外的 9 条通知4 条未读
INSERT INTO `sys_user_notice` VALUES (21, 1, 3, 0, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (22, 2, 3, 1, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (23, 3, 3, 0, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (24, 4, 3, 1, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (25, 5, 3, 1, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (26, 6, 3, 0, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (27, 8, 3, 1, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (28, 9, 3, 1, NULL, now(), now(), 0);
INSERT INTO `sys_user_notice` VALUES (29, 10, 3, 0, NULL, now(), now(), 0);
-- ---------------------------- -- ----------------------------
-- Table structure for sys_user_social -- Table structure for sys_user_social
-- ---------------------------- -- ----------------------------
@@ -584,3 +608,4 @@ CREATE TABLE `sys_user_social` (
KEY `idx_user_id` (`user_id`), KEY `idx_user_id` (`user_id`),
KEY `idx_unionid` (`unionid`) KEY `idx_unionid` (`unionid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户第三方账号绑定表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户第三方账号绑定表';

View File

@@ -8,7 +8,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
* 应用启动类 * 应用启动类
* *
* @author Ray.Hao * @author Ray.Hao
* @since 0.0.1 * @since 3.0.0
*/ */
@EnableScheduling @EnableScheduling
@SpringBootApplication @SpringBootApplication

View File

@@ -1,11 +1,12 @@
package com.youlai.boot.auth.controller; package com.youlai.boot.auth.controller;
import com.youlai.boot.auth.model.LoginReq; import com.youlai.boot.auth.model.form.LoginForm;
import com.youlai.boot.common.enums.ActionTypeEnum; 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.common.result.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.common.annotation.RateLimit;
import com.youlai.boot.framework.captcha.model.CaptchaInfo; import com.youlai.boot.framework.captcha.model.CaptchaInfo;
import com.youlai.boot.framework.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;
@@ -14,7 +15,13 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/** /**
* 认证控制层 * 认证控制层
@@ -41,7 +48,8 @@ public class AuthController {
@Operation(summary = "账号密码登录") @Operation(summary = "账号密码登录")
@PostMapping("/login") @PostMapping("/login")
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
public Result<AuthenticationToken> login(@RequestBody @Valid LoginReq request) { @RateLimit
public Result<AuthenticationToken> login(@RequestBody @Valid LoginForm 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);
} }
@@ -59,6 +67,7 @@ public class AuthController {
@Operation(summary = "发送登录短信验证码") @Operation(summary = "发送登录短信验证码")
@PostMapping("/sms/code") @PostMapping("/sms/code")
@RateLimit(limit = 1, window = 60)
public Result<Void> sendSmsCode( public Result<Void> sendSmsCode(
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile @Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile
) { ) {

View File

@@ -1,10 +1,11 @@
package com.youlai.boot.auth.controller; package com.youlai.boot.auth.controller;
import com.youlai.boot.auth.model.WxMaBindMobileReq; import com.youlai.boot.auth.model.form.WxMaBindMobileForm;
import com.youlai.boot.auth.model.WxMaPhoneLoginReq; import com.youlai.boot.auth.model.form.WxMaPhoneLoginForm;
import com.youlai.boot.auth.model.WxMaLoginResp; import com.youlai.boot.auth.model.vo.WxMaLoginVO;
import com.youlai.boot.auth.service.WxMaAuthService; 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.annotation.RateLimit;
import com.youlai.boot.common.enums.ActionTypeEnum; 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.common.result.Result; import com.youlai.boot.common.result.Result;
@@ -49,11 +50,12 @@ public class WxMaAuthController {
@Operation(summary = "静默登录", description = "通过微信 code 登录,已绑定用户直接返回 token未绑定用户返回 openid 需绑定手机号") @Operation(summary = "静默登录", description = "通过微信 code 登录,已绑定用户直接返回 token未绑定用户返回 openid 需绑定手机号")
@PostMapping("/silent-login") @PostMapping("/silent-login")
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
public Result<WxMaLoginResp> silentLogin( @RateLimit
public Result<WxMaLoginVO> silentLogin(
@Parameter(description = "微信登录凭证wx.login 获取)", required = true, example = "0xxx") @Parameter(description = "微信登录凭证wx.login 获取)", required = true, example = "0xxx")
@RequestParam String code @RequestParam String code
) { ) {
WxMaLoginResp result = wxMaAuthService.silentLogin(code); WxMaLoginVO result = wxMaAuthService.silentLogin(code);
return Result.success(result); return Result.success(result);
} }
@@ -67,7 +69,8 @@ public class WxMaAuthController {
@Operation(summary = "手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序") @Operation(summary = "手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序")
@PostMapping("/phone-login") @PostMapping("/phone-login")
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
public Result<AuthenticationToken> phoneLogin(@Valid @RequestBody WxMaPhoneLoginReq req) { @RateLimit
public Result<AuthenticationToken> phoneLogin(@Valid @RequestBody WxMaPhoneLoginForm req) {
AuthenticationToken result = wxMaAuthService.phoneLogin(req.getLoginCode(), req.getPhoneCode()); AuthenticationToken result = wxMaAuthService.phoneLogin(req.getLoginCode(), req.getPhoneCode());
return Result.success(result); return Result.success(result);
} }
@@ -82,7 +85,8 @@ public class WxMaAuthController {
@Operation(summary = "绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录") @Operation(summary = "绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录")
@PostMapping("/bind-mobile") @PostMapping("/bind-mobile")
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
public Result<AuthenticationToken> bindMobile(@Valid @RequestBody WxMaBindMobileReq req) { @RateLimit
public Result<AuthenticationToken> bindMobile(@Valid @RequestBody WxMaBindMobileForm req) {
AuthenticationToken result = wxMaAuthService.bindMobile(req.getOpenid(), req.getMobile(), req.getSmsCode()); AuthenticationToken result = wxMaAuthService.bindMobile(req.getOpenid(), req.getMobile(), req.getSmsCode());
return Result.success(result); return Result.success(result);
} }

View File

@@ -1,19 +1,15 @@
package com.youlai.boot.auth.model; package com.youlai.boot.auth.model.form;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import jakarta.validation.constraints.NotBlank;
/** /**
* 登录请求参数 * 登录表单
*
* @author Ray.Hao
* @since 3.0.0
*/ */
@Schema(description = "登录请求参数") @Schema(description = "登录请求参数")
@Data @Data
public class LoginReq { public class LoginForm {
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin") @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
@NotBlank(message = "用户名不能为空") @NotBlank(message = "用户名不能为空")

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.auth.model; package com.youlai.boot.auth.model.vo;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@@ -8,16 +8,13 @@ import lombok.NoArgsConstructor;
/** /**
* 微信小程序登录响应 * 微信小程序登录响应
*
* @author Ray.Hao
* @since 2.4.0
*/ */
@Data @Data
@Builder @Builder
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Schema(description = "微信小程序登录响应") @Schema(description = "微信小程序登录响应")
public class WxMaLoginResp { public class WxMaLoginVO {
@Schema(description = "是否新用户") @Schema(description = "是否新用户")
private Boolean isNewUser; private Boolean isNewUser;
@@ -25,7 +22,7 @@ public class WxMaLoginResp {
@Schema(description = "是否需要绑定手机号") @Schema(description = "是否需要绑定手机号")
private Boolean needBindMobile; private Boolean needBindMobile;
@Schema(description = "微信openid(绑定手机号时需要)") @Schema(description = "微信openid")
private String openid; private String openid;
@Schema(description = "访问令牌") @Schema(description = "访问令牌")

View File

@@ -1,6 +1,6 @@
package com.youlai.boot.auth.service; package com.youlai.boot.auth.service;
import com.youlai.boot.auth.model.WxMaLoginResp; import com.youlai.boot.auth.model.vo.WxMaLoginVO;
import com.youlai.boot.framework.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.AuthenticationToken;
/** /**
@@ -21,7 +21,7 @@ public interface WxMaAuthService {
* @param code 微信登录凭证wx.login 获取) * @param code 微信登录凭证wx.login 获取)
* @return 登录结果(成功返回 token需绑定返回 openid * @return 登录结果(成功返回 token需绑定返回 openid
*/ */
WxMaLoginResp silentLogin(String code); WxMaLoginVO silentLogin(String code);
/** /**
* 手机号快捷登录 * 手机号快捷登录

View File

@@ -5,7 +5,7 @@ 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.WxMaLoginResp; import com.youlai.boot.auth.model.vo.WxMaLoginVO;
import com.youlai.boot.auth.service.WxMaAuthService; 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.framework.security.exception.NeedBindMobileException; import com.youlai.boot.framework.security.exception.NeedBindMobileException;
@@ -54,14 +54,14 @@ public class WxMaAuthServiceImpl implements WxMaAuthService {
* 静默登录 * 静默登录
*/ */
@Override @Override
public WxMaLoginResp silentLogin(String code) { public WxMaLoginVO silentLogin(String code) {
WxMaAuthenticationToken token = new WxMaAuthenticationToken(code); WxMaAuthenticationToken token = new WxMaAuthenticationToken(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 WxMaLoginResp.builder() return WxMaLoginVO.builder()
.isNewUser(false) .isNewUser(false)
.needBindMobile(false) .needBindMobile(false)
.accessToken(authToken.getAccessToken()) .accessToken(authToken.getAccessToken())
@@ -70,7 +70,7 @@ public class WxMaAuthServiceImpl implements WxMaAuthService {
.expiresIn(authToken.getExpiresIn()) .expiresIn(authToken.getExpiresIn())
.build(); .build();
} catch (NeedBindMobileException e) { } catch (NeedBindMobileException e) {
return WxMaLoginResp.builder() return WxMaLoginVO.builder()
.isNewUser(true) .isNewUser(true)
.needBindMobile(true) .needBindMobile(true)
.openid(e.getOpenid()) .openid(e.getOpenid())

View File

@@ -5,7 +5,7 @@ import java.lang.annotation.*;
/** /**
* 数据权限注解 * 数据权限注解
* *
* @author zc * @author Ray.Hao
* @since 2.0.0 * @since 2.0.0
*/ */
@Documented @Documented

View File

@@ -3,13 +3,17 @@ package com.youlai.boot.common.annotation;
import com.youlai.boot.common.enums.ActionTypeEnum; 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.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** /**
* 日志注解 * 日志注解
* *
* @author Ray * @author Ray.Hao
* @since 2024/6/25 * @since 3.0.0
*/ */
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) @Target(ElementType.METHOD)

View File

@@ -0,0 +1,25 @@
package com.youlai.boot.common.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 接口限流
* <p>标注在 Controller 方法上,基于 Redis 计数窗口实现</p>
*
* @author Ray.Hao
* @since 4.3.1
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
int limit() default 0;
int window() default 0;
TimeUnit timeUnit() default TimeUnit.SECONDS;
String prefix() default "rate_limit:";
}

View File

@@ -1,145 +0,0 @@
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();
// 在方法执行前获取用户信息,避免 logout 等操作清除 SecurityContext 后无法获取
Long userId = SecurityUtils.getUserId();
String username = SecurityUtils.getUsername();
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;
// fallback登录等场景在 proceed() 前未认证,需在 proceed() 后获取
if (userId == null) {
userId = SecurityUtils.getUserId();
username = SecurityUtils.getUsername();
}
try {
saveLogAsync(logAnnotation, executionTime, exception, userId, username);
} catch (Exception ex) {
log.error("保存操作日志失败", ex);
}
}
}
/**
* 异步保存日志
*/
@Async
public void saveLogAsync(Log logAnnotation, long executionTime, Exception exception, Long userId, String username) {
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);
String sn = request.getHeader("X-Device-SN");
// 解析 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);
}
}
// 构建日志实体
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 != null ? userId : 99);
logEntity.setOperatorName(username != null ? username : sn);
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,6 +1,9 @@
package com.youlai.boot.common.base; package com.youlai.boot.common.base;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data; import lombok.Data;
@@ -14,8 +17,8 @@ import java.time.LocalDateTime;
* *
* <p>实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等</p> * <p>实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等</p>
* *
* @author Ray * @author Ray.Hao
* @since 2024/6/23 * @since 4.3.1
*/ */
@Data @Data
public class BaseEntity implements Serializable { public class BaseEntity implements Serializable {

View File

@@ -7,6 +7,12 @@ import lombok.Data;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
/**
* 查询参数基类
*
* @author Ray.Hao
* @since 0.0.1
*/
@Data @Data
@Schema @Schema
public class BaseQuery implements Serializable { public class BaseQuery implements Serializable {

View File

@@ -1,6 +1,5 @@
package com.youlai.boot.common.base; package com.youlai.boot.common.base;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import java.util.EnumSet; import java.util.EnumSet;
@@ -9,8 +8,8 @@ import java.util.Objects;
/** /**
* 枚举通用接口 * 枚举通用接口
* *
* @author haoxr * @author Ray.Hao
* @since 2022/3/27 12:06 * @since 4.3.1
*/ */
public interface IBaseEnum<T> { public interface IBaseEnum<T> {
@@ -37,12 +36,12 @@ public interface IBaseEnum<T> {
} }
/** /**
* 根据文本标签获取 * 根据值获取标签
* *
* @param value * @param value 枚举值
* @param clazz * @param clazz 枚举类型
* @param <E> * @param <E> 枚举
* @return * @return 标签文本
*/ */
static <E extends Enum<E> & IBaseEnum> String getLabelByValue(Object value, Class<E> clazz) { static <E extends Enum<E> & IBaseEnum> String getLabelByValue(Object value, Class<E> clazz) {
Objects.requireNonNull(value); Objects.requireNonNull(value);
@@ -59,14 +58,13 @@ public interface IBaseEnum<T> {
return label; return label;
} }
/** /**
* 根据文本标签获取值 * 根据标签获取值
* *
* @param label * @param label 标签文本
* @param clazz * @param clazz 枚举类型
* @param <E> * @param <E> 枚举
* @return * @return 枚举值
*/ */
static <E extends Enum<E> & IBaseEnum> Object getValueByLabel(String label, Class<E> clazz) { static <E extends Enum<E> & IBaseEnum> Object getValueByLabel(String label, Class<E> clazz) {
Objects.requireNonNull(label); Objects.requireNonNull(label);
@@ -84,5 +82,4 @@ public interface IBaseEnum<T> {
return value; return value;
} }
} }

View File

@@ -4,7 +4,7 @@ package com.youlai.boot.common.constant;
* Redis 常量 * Redis 常量
* *
* @author Theo * @author Theo
* @since 2024-7-29 11:46:08 * @since 4.3.1
*/ */
public interface RedisConstants { public interface RedisConstants {
@@ -12,7 +12,11 @@ public interface RedisConstants {
* 限流相关键 * 限流相关键
*/ */
interface RateLimiter { interface RateLimiter {
String IP = "rate_limiter:ip:{}"; // IP限流示例rate_limiter:ip:192.168.1.1 /** 接口级限流 Key示例login:rate_limit:token:/api/v1/auth/login */
String API = "{}rate_limit:{}:{}";
/** IP 全局限流 Key示例rate_limiter:ip:192.168.1.100 */
String IP = "rate_limiter:ip:{}";
} }
/** /**
@@ -32,7 +36,7 @@ public interface RedisConstants {
String REFRESH_TOKEN_USER = "auth:token:refresh:{}"; String REFRESH_TOKEN_USER = "auth:token:refresh:{}";
// 用户与访问令牌的映射userId -> accessToken // 用户与访问令牌的映射userId -> accessToken
String USER_ACCESS_TOKEN = "auth:user:access:{}"; String USER_ACCESS_TOKEN = "auth:user:access:{}";
// 用户与刷新令牌的映射userId -> refreshToken // 用户与刷新令牌的映射userId -> refreshToken
String USER_REFRESH_TOKEN = "auth:user:refresh:{}"; String USER_REFRESH_TOKEN = "auth:user:refresh:{}";
// 已撤销 Token 的 JTI单端退出/会话注销):如果 jti 在撤销列表中,则 Token 立即无效 // 已撤销 Token 的 JTI单端退出/会话注销):如果 jti 在撤销列表中,则 Token 立即无效
String BLACKLIST_TOKEN = "auth:token:blacklist:{}"; String BLACKLIST_TOKEN = "auth:token:blacklist:{}";

View File

@@ -23,10 +23,4 @@ public interface SystemConstants {
*/ */
String ROOT_ROLE_CODE = "ROOT"; String ROOT_ROLE_CODE = "ROOT";
/**
* 系统配置 IP的QPS限流的KEY
*/
String SYSTEM_CONFIG_IP_QPS_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT";
} }

View File

@@ -1,27 +0,0 @@
package com.youlai.boot.common.enums;
/**
* EasyCaptcha 验证码类型枚举
*
* @author haoxr
* @since 2.5.1
*/
public enum CaptchaTypeEnum {
/**
* 圆圈干扰验证码
*/
CIRCLE,
/**
* GIF验证码
*/
GIF,
/**
* 干扰线验证码
*/
LINE,
/**
* 扭曲干扰验证码
*/
SHEAR
}

View File

@@ -6,7 +6,7 @@ import lombok.Getter;
/** /**
* 环境枚举 * 环境枚举
* *
* @author Ray * @author Ray.Hao
* @since 4.0.0 * @since 4.0.0
*/ */
@Getter @Getter

View File

@@ -6,7 +6,7 @@ import lombok.Getter;
/** /**
* 状态枚举 * 状态枚举
* *
* @author haoxr * @author Ray.Hao
* @since 2022/10/14 * @since 2022/10/14
*/ */
@Getter @Getter
@@ -17,7 +17,6 @@ public enum StatusEnum implements IBaseEnum<Integer> {
private final Integer value; private final Integer value;
private final String label; private final String label;
StatusEnum(Integer value, String label) { StatusEnum(Integer value, String label) {

View File

@@ -7,26 +7,24 @@ import org.slf4j.helpers.MessageFormatter;
/** /**
* 自定义业务异常 * 自定义业务异常
* *
* @author Ray * @author Ray.Hao
* @since 2022/7/31 * @since 0.0.1
*/ */
@Getter @Getter
public class BusinessException extends RuntimeException { public class BusinessException extends RuntimeException {
public IResultCode resultCode; private IResultCode resultCode;
public BusinessException(IResultCode errorCode) { public BusinessException(IResultCode errorCode) {
super(errorCode.getMsg()); super(errorCode.getMsg());
this.resultCode = errorCode; this.resultCode = errorCode;
} }
public BusinessException(IResultCode errorCode, String message) { public BusinessException(IResultCode errorCode, String message) {
super(message); super(message);
this.resultCode = errorCode; this.resultCode = errorCode;
} }
public BusinessException(String message, Throwable cause) { public BusinessException(String message, Throwable cause) {
super(message, cause); super(message, cause);
} }

View File

@@ -1,122 +0,0 @@
package com.youlai.boot.common.result;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import java.nio.charset.StandardCharsets;
/**
* 响应写入器
* <p>
* 用于在过滤器、Security处理器等无法使用 @RestControllerAdvice 的场景中统一写入HTTP响应。
* 支持写入成功响应和错误响应。
* 此类为工具类,所有方法均为静态方法,禁止实例化。
*
* @author Ray.Hao
* @since 2.0.0
*/
@Slf4j
public final class ResponseWriter {
/**
* 私有构造函数,防止实例化
*/
private ResponseWriter() {
throw new UnsupportedOperationException("工具类不允许实例化");
}
/**
* 写入成功响应
*
* @param response HttpServletResponse
* @param data 响应数据(可选)
*/
public static void writeSuccess(HttpServletResponse response, Object data) {
writeResult(response, Result.success(data), HttpStatus.OK.value());
}
/**
* 写入成功响应(无数据)
*
* @param response HttpServletResponse
*/
public static void writeSuccess(HttpServletResponse response) {
writeSuccess(response, null);
}
/**
* 写入错误响应
*
* @param response HttpServletResponse
* @param resultCode 响应结果码
*/
public static void writeError(HttpServletResponse response, ResultCode resultCode) {
writeError(response, resultCode, null);
}
/**
* 写入错误响应(带自定义消息)
*
* @param response HttpServletResponse
* @param resultCode 响应结果码
* @param message 自定义消息(可选,为 null 时使用 resultCode 的默认消息)
*/
public static void writeError(HttpServletResponse response, ResultCode resultCode, String message) {
Result<?> result = message == null
? Result.failed(resultCode)
: Result.failed(resultCode, message);
int httpStatus = mapHttpStatus(resultCode);
writeResult(response, result, httpStatus);
}
/**
* 写入响应结果(通用方法)
*
* @param response HttpServletResponse
* @param result 响应结果对象
* @param httpStatus HTTP状态码
*/
private static void writeResult(HttpServletResponse response, Result<?> result, int httpStatus) {
try {
// 设置HTTP状态码
response.setStatus(httpStatus);
// 设置响应编码和内容类型
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
// 写入响应
JakartaServletUtil.write(response,
JSONUtil.toJsonStr(result),
MediaType.APPLICATION_JSON_VALUE
);
} catch (Exception e) {
log.error("写入响应时发生未知异常: httpStatus={}, result={}", httpStatus, result, e);
}
}
/**
* 根据业务结果码映射HTTP状态码
* 401: 未认证token无效/过期)
* 403: 权限不足
* 400: 其他业务错误
*
* @param resultCode 业务结果码
* @return HTTP状态码
*/
private static int mapHttpStatus(ResultCode resultCode) {
return switch (resultCode) {
case ACCESS_UNAUTHORIZED,
ACCESS_TOKEN_INVALID,
REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
case ACCESS_PERMISSION_EXCEPTION -> HttpStatus.FORBIDDEN.value();
default -> HttpStatus.BAD_REQUEST.value();
};
}
}

View File

@@ -9,7 +9,7 @@ import java.io.Serializable;
* 统一响应结构体 * 统一响应结构体
* *
* @author Ray.Hao * @author Ray.Hao
* @since 2022/1/30 * @since 4.3.1
**/ **/
@Data @Data
public class Result<T> implements Serializable { public class Result<T> implements Serializable {
@@ -64,10 +64,6 @@ public class Result<T> implements Serializable {
return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), data); return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), data);
} }
private static <T> Result<T> result(IResultCode resultCode, T data) {
return result(resultCode.getCode(), resultCode.getMsg(), data);
}
private static <T> Result<T> result(String code, String msg, T data) { private static <T> Result<T> result(String code, String msg, T data) {
Result<T> result = new Result<>(); Result<T> result = new Result<>();
result.setCode(code); result.setCode(code);

View File

@@ -48,7 +48,7 @@ import java.io.Serializable;
* </pre> * </pre>
* *
* @author Ray.Hao * @author Ray.Hao
* @since 2020/6/23 * @since 4.3.1
**/ **/
public enum ResultCode implements IResultCode, Serializable { public enum ResultCode implements IResultCode, Serializable {

View File

@@ -22,33 +22,16 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream; import java.io.InputStream;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/**
* Aliyun 对象存储服务类
*
* @author haoxr
* @since 2.3.0
*/
@Component @Component
@ConditionalOnProperty(value = "oss.type", havingValue = "aliyun") @ConditionalOnProperty(value = "oss.type", havingValue = "aliyun")
@ConfigurationProperties(prefix = "oss.aliyun") @ConfigurationProperties(prefix = "oss.aliyun")
@RequiredArgsConstructor @RequiredArgsConstructor
@Data @Data
public class AliyunFileService implements FileService { public class AliyunFileServiceImpl implements FileService {
/**
* 服务Endpoint
*/
private String endpoint; private String endpoint;
/**
* 访问凭据
*/
private String accessKeyId; private String accessKeyId;
/**
* 凭据密钥
*/
private String accessKeySecret; private String accessKeySecret;
/**
* 存储桶名称
*/
private String bucketName; private String bucketName;
private OSS aliyunOssClient; private OSS aliyunOssClient;
@@ -61,27 +44,18 @@ public class AliyunFileService implements FileService {
@Override @Override
@SneakyThrows @SneakyThrows
public FileInfo uploadFile(MultipartFile file) { public FileInfo uploadFile(MultipartFile file) {
// 获取文件名称
String originalFilename = file.getOriginalFilename(); String originalFilename = file.getOriginalFilename();
// 生成文件名(日期文件夹)
String suffix = FileUtil.getSuffix(originalFilename); String suffix = FileUtil.getSuffix(originalFilename);
String uuid = IdUtil.simpleUUID(); String uuid = IdUtil.simpleUUID();
String fileName = DateUtil.format(LocalDateTime.now(), "yyyyMMdd") + "/" + uuid + "." + suffix; String fileName = DateUtil.format(LocalDateTime.now(), "yyyyMMdd") + "/" + uuid + "." + suffix;
// try-with-resource 语法糖自动释放流
try (InputStream inputStream = file.getInputStream()) { try (InputStream inputStream = file.getInputStream()) {
// 设置上传文件的元信息例如Content-Type
ObjectMetadata metadata = new ObjectMetadata(); ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(file.getContentType()); metadata.setContentType(file.getContentType());
// 创建PutObjectRequest对象指定Bucket名称对象名称和输入流
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName, inputStream, metadata); PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName, inputStream, metadata);
// 上传文件
aliyunOssClient.putObject(putObjectRequest); aliyunOssClient.putObject(putObjectRequest);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("文件上传失败"); throw new RuntimeException("文件上传失败");
} }
// 获取文件访问路径
String fileUrl = "https://" + bucketName + "." + endpoint + "/" + fileName; String fileUrl = "https://" + bucketName + "." + endpoint + "/" + fileName;
FileInfo fileInfo = new FileInfo(); FileInfo fileInfo = new FileInfo();
fileInfo.setName(originalFilename); fileInfo.setName(originalFilename);
@@ -92,8 +66,8 @@ public class AliyunFileService implements FileService {
@Override @Override
public boolean deleteFile(String filePath) { public boolean deleteFile(String filePath) {
Assert.notBlank(filePath, "删除文件路径不能为空"); Assert.notBlank(filePath, "删除文件路径不能为空");
String fileHost = "https://" + bucketName + "." + endpoint; // 文件主机域名 String fileHost = "https://" + bucketName + "." + endpoint;
String fileName = filePath.substring(fileHost.length() + 1); // +1 /占一个字符截断左闭右开 String fileName = filePath.substring(fileHost.length() + 1);
aliyunOssClient.deleteObject(bucketName, fileName); aliyunOssClient.deleteObject(bucketName, fileName);
return true; return true;
} }

View File

@@ -19,49 +19,30 @@ import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/**
* 本地存储服务类
*
* @author Theo
* @since 2024-12-09 17:11
*/
@Data @Data
@Slf4j @Slf4j
@Component @Component
@ConditionalOnProperty(value = "oss.type", havingValue = "local") @ConditionalOnProperty(value = "oss.type", havingValue = "local")
@ConfigurationProperties(prefix = "oss.local") @ConfigurationProperties(prefix = "oss.local")
@RequiredArgsConstructor @RequiredArgsConstructor
public class LocalFileService implements FileService { public class LocalFileServiceImpl implements FileService {
@Value("${oss.local.storage-path}") @Value("${oss.local.storage-path}")
private String storagePath; private String storagePath;
/**
* 上传文件方法
*
* @param file 表单文件对象
* @return 文件信息
*/
@Override @Override
public FileInfo uploadFile(MultipartFile file) { public FileInfo uploadFile(MultipartFile file) {
// 获取文件名
String originalFilename = file.getOriginalFilename(); String originalFilename = file.getOriginalFilename();
// 获取文件后缀
String suffix = FileUtil.getSuffix(originalFilename); String suffix = FileUtil.getSuffix(originalFilename);
// 生成uuid
String fileName = IdUtil.simpleUUID()+ "." + suffix;; String fileName = IdUtil.simpleUUID()+ "." + suffix;;
// 生成文件名(日期文件夹)
String folder = DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN); String folder = DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
String filePrefix = storagePath.endsWith(File.separator) ? storagePath : storagePath + File.separator; String filePrefix = storagePath.endsWith(File.separator) ? storagePath : storagePath + File.separator;
// try-with-resource 语法糖自动释放流
try (InputStream inputStream = file.getInputStream()) { try (InputStream inputStream = file.getInputStream()) {
// 上传文件
FileUtil.writeFromStream(inputStream, filePrefix + folder + File.separator + fileName); FileUtil.writeFromStream(inputStream, filePrefix + folder + File.separator + fileName);
} catch (Exception e) { } catch (Exception e) {
log.error("文件上传失败", e); log.error("文件上传失败", e);
throw new RuntimeException("文件上传失败"); throw new RuntimeException("文件上传失败");
} }
// 获取文件访问路径因为这里是本地存储所以直接返回文件的相对路径需要前端自行处理访问前缀
String fileUrl = File.separator + folder + File.separator + fileName; String fileUrl = File.separator + folder + File.separator + fileName;
FileInfo fileInfo = new FileInfo(); FileInfo fileInfo = new FileInfo();
fileInfo.setName(originalFilename); fileInfo.setName(originalFilename);
@@ -69,24 +50,14 @@ public class LocalFileService implements FileService {
return fileInfo; return fileInfo;
} }
/**
* 删除文件
* @param filePath 文件完整URL
* @return 是否删除成功
*/
@Override @Override
public boolean deleteFile(String filePath) { public boolean deleteFile(String filePath) {
//判断文件是否为空
if (filePath == null || filePath.isEmpty()) { if (filePath == null || filePath.isEmpty()) {
return false; return false;
} }
// 判断filepath是否为文件夹
if (FileUtil.isDirectory(storagePath + filePath)) { if (FileUtil.isDirectory(storagePath + filePath)) {
// 禁止删除文件夹
return false; return false;
} }
// 删除文件
return FileUtil.del(storagePath + filePath); return FileUtil.del(storagePath + filePath);
} }
} }

View File

@@ -24,79 +24,39 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream; import java.io.InputStream;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/**
* MinIO 文件上传服务类
*
* @author Ray.Hao
* @since 2023/6/2
*/
@Component @Component
@ConditionalOnProperty(value = "oss.type", havingValue = "minio") @ConditionalOnProperty(value = "oss.type", havingValue = "minio")
@ConfigurationProperties(prefix = "oss.minio") @ConfigurationProperties(prefix = "oss.minio")
@RequiredArgsConstructor @RequiredArgsConstructor
@Data @Data
@Slf4j @Slf4j
public class MinioFileService implements FileService { public class MinioFileServiceImpl implements FileService {
/**
* 服务Endpoint
*/
private String endpoint; private String endpoint;
/**
* 访问凭据
*/
private String accessKey; private String accessKey;
/**
* 凭据密钥
*/
private String secretKey; private String secretKey;
/**
* 存储桶名称
*/
private String bucketName; private String bucketName;
/**
* 自定义域名
*/
private String customDomain; private String customDomain;
private MinioClient minioClient; private MinioClient minioClient;
// 依赖注入完成之后执行初始化
@PostConstruct @PostConstruct
public void init() { public void init() {
minioClient = MinioClient.builder() minioClient = MinioClient.builder()
.endpoint(endpoint) .endpoint(endpoint)
.credentials(accessKey, secretKey) .credentials(accessKey, secretKey)
.build(); .build();
// 创建存储桶(存储桶不存在)
// createBucketIfAbsent(bucketName);
} }
/**
* 上传文件
*
* @param file 表单文件对象
* @return 文件信息
*/
@Override @Override
public FileInfo uploadFile(MultipartFile file) { public FileInfo uploadFile(MultipartFile file) {
// 创建存储桶(存储桶不存在)如果有搭建好的minio服务建议放在init方法中
createBucketIfAbsent(bucketName); createBucketIfAbsent(bucketName);
// 文件原生名称
String originalFilename = file.getOriginalFilename(); String originalFilename = file.getOriginalFilename();
// 文件后缀
String suffix = FileUtil.getSuffix(originalFilename); String suffix = FileUtil.getSuffix(originalFilename);
// 文件夹名称
String dateFolder = DateUtil.format(LocalDateTime.now(), "yyyyMMdd"); String dateFolder = DateUtil.format(LocalDateTime.now(), "yyyyMMdd");
// 文件名称
String fileName = IdUtil.simpleUUID() + "." + suffix; String fileName = IdUtil.simpleUUID() + "." + suffix;
// try-with-resource 语法糖自动释放流
try (InputStream inputStream = file.getInputStream()) { try (InputStream inputStream = file.getInputStream()) {
// 文件上传
PutObjectArgs putObjectArgs = PutObjectArgs.builder() PutObjectArgs putObjectArgs = PutObjectArgs.builder()
.bucket(bucketName) .bucket(bucketName)
.object(dateFolder + "/"+ fileName) .object(dateFolder + "/"+ fileName)
@@ -105,21 +65,16 @@ public class MinioFileService implements FileService {
.build(); .build();
minioClient.putObject(putObjectArgs); minioClient.putObject(putObjectArgs);
// 返回文件路径
String fileUrl; String fileUrl;
// 未配置自定义域名
if (StrUtil.isBlank(customDomain)) { if (StrUtil.isBlank(customDomain)) {
// 获取文件URL
GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder() GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
.bucket(bucketName) .bucket(bucketName)
.object(dateFolder + "/"+ fileName) .object(dateFolder + "/"+ fileName)
.method(Method.GET) .method(Method.GET)
.build(); .build();
fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs); fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs);
fileUrl = fileUrl.substring(0, fileUrl.indexOf("?")); fileUrl = fileUrl.substring(0, fileUrl.indexOf("?"));
} else { } else {
// 配置自定义文件路径域名
fileUrl = customDomain + "/"+ bucketName + "/"+ dateFolder + "/"+ fileName; fileUrl = customDomain + "/"+ bucketName + "/"+ dateFolder + "/"+ fileName;
} }
@@ -133,31 +88,18 @@ public class MinioFileService implements FileService {
} }
} }
/**
* 删除文件
*
* @param filePath 文件完整路径
* @return 是否删除成功
*/
@Override @Override
public boolean deleteFile(String filePath) { public boolean deleteFile(String filePath) {
Assert.notBlank(filePath, "删除文件路径不能为空"); Assert.notBlank(filePath, "删除文件路径不能为空");
try { try {
String fileName; String fileName;
if (StrUtil.isNotBlank(customDomain)) { if (StrUtil.isNotBlank(customDomain)) {
// https://oss.youlai.tech/default/20221120/test.jpg 20221120/websocket.jpg fileName = filePath.substring(customDomain.length() + 1 + bucketName.length() + 1);
fileName = filePath.substring(customDomain.length() + 1 + bucketName.length() + 1); // 两个/占了2个字符长度
} else { } else {
// http://localhost:9000/default/20221120/test.jpg 20221120/websocket.jpg
fileName = filePath.substring(endpoint.length() + 1 + bucketName.length() + 1); fileName = filePath.substring(endpoint.length() + 1 + bucketName.length() + 1);
} }
RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder() minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(bucketName) .bucket(bucketName).object(fileName).build());
.object(fileName)
.build();
minioClient.removeObject(removeObjectArgs);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
log.error("删除文件失败", e); log.error("删除文件失败", e);
@@ -165,16 +107,7 @@ public class MinioFileService implements FileService {
} }
} }
/**
* PUBLIC桶策略
* 如果不配置则新建的存储桶默认是PRIVATE则存储桶文件会拒绝访问 Access Denied
*
* @param bucketName 存储桶名称
* @return 存储桶策略
*/
private static String publicBucketPolicy(String bucketName) { private static String publicBucketPolicy(String bucketName) {
// AWS的S3存储桶策略 JSON 格式 https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/example-bucket-policies.html
return "{\"Version\":\"2012-10-17\"," return "{\"Version\":\"2012-10-17\","
+ "\"Statement\":[{\"Effect\":\"Allow\"," + "\"Statement\":[{\"Effect\":\"Allow\","
+ "\"Principal\":{\"AWS\":[\"*\"]}," + "\"Principal\":{\"AWS\":[\"*\"]},"
@@ -185,26 +118,12 @@ public class MinioFileService implements FileService {
+ "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}"; + "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}";
} }
/**
* 创建存储桶(存储桶不存在)
*
* @param bucketName 存储桶名称
*/
@SneakyThrows @SneakyThrows
private void createBucketIfAbsent(String bucketName) { private void createBucketIfAbsent(String bucketName) {
BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build(); if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
if (!minioClient.bucketExists(bucketExistsArgs)) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build(); minioClient.setBucketPolicy(SetBucketPolicyArgs.builder()
.bucket(bucketName).config(publicBucketPolicy(bucketName)).build());
minioClient.makeBucket(makeBucketArgs);
// 设置存储桶访问权限为PUBLIC 如果不配置则新建的存储桶默认是PRIVATE则存储桶文件会拒绝访问 Access Denied
SetBucketPolicyArgs setBucketPolicyArgs = SetBucketPolicyArgs
.builder()
.bucket(bucketName)
.config(publicBucketPolicy(bucketName))
.build();
minioClient.setBucketPolicy(setBucketPolicyArgs);
} }
} }
} }

View File

@@ -24,10 +24,12 @@ public class RedisConfig {
* 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer
* *
* @param redisConnectionFactory {@link RedisConnectionFactory} * @param redisConnectionFactory {@link RedisConnectionFactory}
* @param jsonMapper Jackson 序列化器(统一使用不写入类型信息的配置)
* @return {@link RedisTemplate} * @return {@link RedisTemplate}
*/ */
@Bean @Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory,
JsonMapper jsonMapper) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setConnectionFactory(redisConnectionFactory);
@@ -37,10 +39,6 @@ public class RedisConfig {
redisTemplate.setHashKeySerializer(RedisSerializer.string()); redisTemplate.setHashKeySerializer(RedisSerializer.string());
// Value 使用自定义 JSON 序列化(不写入类型信息,避免 HashSet 等集合被序列化成带 @class 的结构) // Value 使用自定义 JSON 序列化(不写入类型信息,避免 HashSet 等集合被序列化成带 @class 的结构)
JsonMapper jsonMapper = JsonMapper.builder()
.disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
JacksonJsonRedisSerializer<Object> jsonSerializer = new JacksonJsonRedisSerializer<>(jsonMapper, Object.class); JacksonJsonRedisSerializer<Object> jsonSerializer = new JacksonJsonRedisSerializer<>(jsonMapper, Object.class);
redisTemplate.setValueSerializer(jsonSerializer); redisTemplate.setValueSerializer(jsonSerializer);
@@ -50,4 +48,17 @@ public class RedisConfig {
return redisTemplate; return redisTemplate;
} }
/**
* 统一的 JsonMapper Bean
* <p>
* 禁止将日期序列化为时间戳;不写入类型信息,保证 Redis 存储的 JSON 纯净可读。
* 需要反序列化到特定类型时,调用方应使用 {@link JsonMapper#convertValue(Object, Class)} 显式转换。
*/
@Bean
public JsonMapper jsonMapper() {
return JsonMapper.builder()
.disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
}
} }

View File

@@ -0,0 +1,14 @@
package com.youlai.boot.framework.captcha.enums;
/**
* EasyCaptcha 验证码类型
*
* @author haoxr
* @since 2.5.1
*/
public enum CaptchaTypeEnum {
CIRCLE,
GIF,
LINE,
SHEAR
}

View File

@@ -6,7 +6,7 @@ import cn.hutool.captcha.generator.CodeGenerator;
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.enums.CaptchaTypeEnum; import com.youlai.boot.framework.captcha.enums.CaptchaTypeEnum;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.framework.captcha.config.CaptchaProperties; import com.youlai.boot.framework.captcha.config.CaptchaProperties;
import com.youlai.boot.framework.captcha.exception.CaptchaException; import com.youlai.boot.framework.captcha.exception.CaptchaException;

View File

@@ -9,11 +9,7 @@ import org.springframework.mail.javamail.JavaMailSenderImpl;
import java.util.Properties; import java.util.Properties;
/** /**
* MailConfig 配置类,用于手动配置和注入 JavaMailSender。 * 邮件发送配置
* 通过读取 MailProperties 类中配置的邮件相关属性来初始化 JavaMailSender。
* <p>
* 手动注入的原因是为了避免在使用 application-dev.yml 或其他非 application.yml 配置文件时,
* IDEA 提示无法找到 JavaMailSender 的 bean。
* *
* @author Ray * @author Ray
* @since 2024/8/17 * @since 2024/8/17
@@ -28,11 +24,6 @@ public class MailConfig {
this.mailProperties = mailProperties; this.mailProperties = mailProperties;
} }
/**
* 创建并配置 JavaMailSender bean。
*
* @return 配置好的 JavaMailSender 实例
*/
@Bean @Bean
public JavaMailSender javaMailSender() { public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); JavaMailSenderImpl mailSender = new JavaMailSenderImpl();

View File

@@ -4,7 +4,7 @@ import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
/** /**
* 邮件配置类,用于接收和存储邮件相关的配置属性 * 邮件配置属性
* *
* @author Ray * @author Ray
* @since 2024/8/17 * @since 2024/8/17
@@ -13,75 +13,38 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@Data @Data
public class MailProperties { public class MailProperties {
/** /** SMTP 服务器地址 */
* 邮件服务器主机名或 IP 地址。
* 例如smtp.example.com
*/
private String host; private String host;
/** /** SMTP 端口 */
* 邮件服务器端口号。
* 例如587
*/
private int port; private int port;
/** /** 发件人账号 */
* 用于连接邮件服务器的用户名。
* 例如your_email@example.com
*/
private String username; private String username;
/** /** 发件人密码 */
* 用于连接邮件服务器的密码。
* 该密码应安全存储,不应在代码中硬编码。
*/
private String password; private String password;
/** /** 发件人地址 */
* 邮件发送者地址。
*/
private String from; private String from;
/** /** SMTP 扩展属性 */
* 邮件服务器的其他属性配置。
* 这些配置通常用于进一步定制邮件发送行为。
*/
private Properties properties = new Properties(); private Properties properties = new Properties();
/**
* 内部类,用于封装邮件服务器的详细配置。
* 包含 SMTP 相关的配置选项。
*/
@Data @Data
public static class Properties { public static class Properties {
/**
* SMTP 配置选项类。
* 包含认证、加密等与 SMTP 协议相关的配置。
*/
private Smtp smtp = new Smtp(); private Smtp smtp = new Smtp();
@Data @Data
public static class Smtp { public static class Smtp {
/** 启用 SMTP 认证 */
/**
* 是否启用 SMTP 认证。
* 如果为 `true`,则需要提供有效的用户名和密码进行认证。
*/
private boolean auth; private boolean auth;
/**
* STARTTLS 加密配置选项。
*/
private StartTls starttls = new StartTls(); private StartTls starttls = new StartTls();
@Data @Data
public static class StartTls { public static class StartTls {
/** 启用 STARTTLS */
/**
* 是否启用 STARTTLS 加密。
* 如果为 `true`,在发送邮件时将启用 STARTTLS 协议进行加密传输。
*/
private boolean enable; private boolean enable;
} }
} }

View File

@@ -17,34 +17,22 @@ import java.util.Map;
@Data @Data
public class AliyunSmsProperties { public class AliyunSmsProperties {
/** /** Access Key ID */
* 阿里云账户的Access Key ID用于API请求认证
*/
private String accessKeyId; private String accessKeyId;
/** /** Access Key Secret */
*阿里云账户的Access Key Secret用于API请求认证
*/
private String accessKeySecret; private String accessKeySecret;
/** /** API 域名,如 dysmsapi.aliyuncs.com */
* 阿里云短信服务API的域名 eg: dysmsapi.aliyuncs.com
*/
private String domain; private String domain;
/** /** 区域,如 cn-shanghai */
* 阿里云服务的区域ID如cn-shanghai
*/
private String regionId; private String regionId;
/** /** 短信签名 */
* 短信签名,必须是已经在阿里云短信服务中注册并通过审核的
*/
private String signName; private String signName;
/** /** 短信模板集合key 为模板用途(login/register)value 为模板CODE */
* 短信模板集合
*/
private Map<String, String> templates; private Map<String, String> templates;
} }

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.framework.integration.wxma; package com.youlai.boot.framework.integration.wxma.config;
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;
@@ -17,21 +17,13 @@ import org.springframework.context.annotation.Configuration;
@EnableConfigurationProperties(WxMaProperties.class) @EnableConfigurationProperties(WxMaProperties.class)
public class WxMaConfig { public class WxMaConfig {
/**
* 微信小程序服务
*
* @param properties 微信小程序配置属性
* @return {@link WxMaService}
*/
@Bean @Bean
public WxMaService wxMaService(WxMaProperties properties) { public WxMaService wxMaService(WxMaProperties properties) {
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl(); WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
config.setAppid(properties.getAppid()); config.setAppid(properties.getAppid());
config.setSecret(properties.getSecret()); config.setSecret(properties.getSecret());
WxMaService service = new WxMaServiceImpl(); WxMaService service = new WxMaServiceImpl();
service.setWxMaConfig(config); service.setWxMaConfig(config);
return service; return service;
} }
} }

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.framework.integration.wxma; package com.youlai.boot.framework.integration.wxma.config;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -10,14 +10,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "wx.miniapp") @ConfigurationProperties(prefix = "wx.miniapp")
public class WxMaProperties { public class WxMaProperties {
/**
* 小程序 AppID
*/
private String appid; private String appid;
/**
* 小程序 AppSecret
*/
private String secret; private String secret;
} }

View File

@@ -5,17 +5,17 @@ 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.framework.mybatis.handler.MyMetaObjectHandler; import com.youlai.boot.framework.mybatis.handler.AutoFillMetaObjectHandler;
import com.youlai.boot.framework.mybatis.interceptor.MyDataPermissionHandler; import com.youlai.boot.framework.mybatis.interceptor.MyDataPermissionHandler;
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.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.EnableTransactionManagement;
/** /**
* mybatis-plus 配置类 * MyBatis-Plus 配置类
* *
* @author Ray.Hao * @author Ray.Hao
* @since 2022/7/2 * @since 4.3.1
*/ */
@Configuration @Configuration
@EnableTransactionManagement @EnableTransactionManagement
@@ -38,12 +38,12 @@ public class MybatisConfig {
} }
/** /**
* 自动填充数据库创建人、创建时间、更新人、更新时间 * 自动填充创建时间和更新时间
*/ */
@Bean @Bean
public GlobalConfig globalConfig() { public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig(); GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setMetaObjectHandler(new MyMetaObjectHandler()); globalConfig.setMetaObjectHandler(new AutoFillMetaObjectHandler());
return globalConfig; return globalConfig;
} }

View File

@@ -1,44 +1,28 @@
package com.youlai.boot.framework.mybatis.handler; 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 org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/** /**
* mybatis-plus 字段自动填充 * MyBatis-Plus 字段自动填充
* <p>
* 支持自动填充创建时间更新时间
* </p>
* *
* @author Ray.Hao * @author Ray.Hao
* @since 2022/10/14 * @since 3.0.0
*/ */
@Component @Component
@RequiredArgsConstructor public class AutoFillMetaObjectHandler implements MetaObjectHandler {
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 新增填充创建时间更新时间
*
* @param metaObject 元数据
*/
@Override @Override
public void insertFill(MetaObject metaObject) { public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class); this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
} }
/**
* 更新填充更新时间
*
* @param metaObject 元数据
*/
@Override @Override
public void updateFill(MetaObject metaObject) { public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
} }
} }

View File

@@ -11,7 +11,8 @@ import com.youlai.boot.framework.security.model.SysUserDetails;
import com.youlai.boot.framework.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.Expression;
import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression; import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
@@ -30,8 +31,8 @@ import java.util.List;
* <p> * <p>
* 使用 JSQLParser 构建 SQL 条件,避免字符串拼接,提高代码安全性和可读性。 * 使用 JSQLParser 构建 SQL 条件,避免字符串拼接,提高代码安全性和可读性。
* *
* @author zc * @author Ray.Hao
* @since 2021-12-10 13:28 * @since 2021/12/10
*/ */
@Slf4j @Slf4j
public class MyDataPermissionHandler implements DataPermissionHandler { public class MyDataPermissionHandler implements DataPermissionHandler {

View File

@@ -1,10 +1,9 @@
package com.youlai.boot.framework.security.config; package com.youlai.boot.framework.security.config;
import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.WxMaService;
import com.youlai.boot.framework.captcha.service.CaptchaService;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import com.youlai.boot.framework.security.filter.MobileApiSignatureFilter; import com.youlai.boot.framework.security.filter.MobileApiSignatureFilter;
import com.youlai.boot.framework.web.filter.RateLimiterFilter; import com.youlai.boot.framework.captcha.service.CaptchaService;
import com.youlai.boot.framework.security.filter.CaptchaValidationFilter; import com.youlai.boot.framework.security.filter.CaptchaValidationFilter;
import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter; import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter;
import com.youlai.boot.framework.security.handler.MyAccessDeniedHandler; import com.youlai.boot.framework.security.handler.MyAccessDeniedHandler;
@@ -13,7 +12,6 @@ import com.youlai.boot.framework.security.provider.SmsAuthenticationProvider;
import com.youlai.boot.framework.security.provider.WxMaAuthenticationProvider; import com.youlai.boot.framework.security.provider.WxMaAuthenticationProvider;
import com.youlai.boot.framework.security.token.TokenManager; import com.youlai.boot.framework.security.token.TokenManager;
import com.youlai.boot.framework.security.service.SysUserDetailsService; import com.youlai.boot.framework.security.service.SysUserDetailsService;
import com.youlai.boot.system.service.ConfigService;
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;
@@ -37,7 +35,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
* Spring Security 配置类 * Spring Security 配置类
* *
* @author Ray.Hao * @author Ray.Hao
* @since 2023/2/17 * @since 4.3.1
*/ */
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -53,7 +51,6 @@ public class SecurityConfig {
private final SysUserDetailsService userDetailsService; private final SysUserDetailsService userDetailsService;
private final CaptchaService captchaService; private final CaptchaService captchaService;
private final ConfigService configService;
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
/** /**
@@ -91,8 +88,6 @@ public class SecurityConfig {
.httpBasic(AbstractHttpConfigurer::disable) // 禁用 HTTP Basic 认证,避免弹窗式登录 .httpBasic(AbstractHttpConfigurer::disable) // 禁用 HTTP Basic 认证,避免弹窗式登录
// 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中 // 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
// 限流过滤器
.addFilterBefore(new RateLimiterFilter(redisTemplate, configService), UsernamePasswordAuthenticationFilter.class)
// 验证码校验过滤器 // 验证码校验过滤器
.addFilterBefore(new CaptchaValidationFilter(captchaService), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new CaptchaValidationFilter(captchaService), UsernamePasswordAuthenticationFilter.class)
// 移动设备API签名验证过滤器仅对 /api/v1/sn/** 路径生效) // 移动设备API签名验证过滤器仅对 /api/v1/sn/** 路径生效)

View File

@@ -3,13 +3,13 @@ package com.youlai.boot.framework.security.exception;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
/** /**
* 验证码校验异常 * 短信验证码异常
* *
* @author Ray.Hao * @author Ray.Hao
* @since 2025/3/1 * @since 2025/3/1
*/ */
public class CaptchaValidationException extends AuthenticationException { public class SmsCaptchaException extends AuthenticationException {
public CaptchaValidationException(String msg) { public SmsCaptchaException(String msg) {
super(msg); super(msg);
} }
} }

View File

@@ -0,0 +1,21 @@
package com.youlai.boot.framework.security.exception;
import com.youlai.boot.common.result.ResultCode;
import lombok.Getter;
/**
* Token 无效异常access_token 或 refresh_token 过期/无效)
*
* @author Ray.Hao
* @since 4.3.1
*/
@Getter
public class TokenInvalidException extends RuntimeException {
private final ResultCode resultCode;
public TokenInvalidException(ResultCode resultCode) {
super(resultCode.getMsg());
this.resultCode = resultCode;
}
}

View File

@@ -5,7 +5,7 @@ import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.result.ResponseWriter; import com.youlai.boot.framework.web.util.ResponseWriter;
import com.youlai.boot.framework.captcha.exception.CaptchaException; import com.youlai.boot.framework.captcha.exception.CaptchaException;
import com.youlai.boot.framework.captcha.service.CaptchaService; import com.youlai.boot.framework.captcha.service.CaptchaService;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;

View File

@@ -3,7 +3,7 @@ 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.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.result.ResponseWriter; import com.youlai.boot.framework.web.util.ResponseWriter;
import com.youlai.boot.framework.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;

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.framework.security.handler; package com.youlai.boot.framework.security.handler;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.result.ResponseWriter; import com.youlai.boot.framework.web.util.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;

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.framework.security.handler; package com.youlai.boot.framework.security.handler;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.result.ResponseWriter; import com.youlai.boot.framework.web.util.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;

View File

@@ -3,7 +3,7 @@ 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.framework.security.exception.CaptchaValidationException; import com.youlai.boot.framework.security.exception.SmsCaptchaException;
import com.youlai.boot.framework.security.model.SmsAuthenticationToken; import com.youlai.boot.framework.security.model.SmsAuthenticationToken;
import com.youlai.boot.framework.security.model.SysUserDetails; import com.youlai.boot.framework.security.model.SysUserDetails;
import com.youlai.boot.framework.security.model.UserAuthInfo; import com.youlai.boot.framework.security.model.UserAuthInfo;
@@ -62,11 +62,11 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
// 参数校验 // 参数校验
if (StrUtil.isBlank(mobile)) { if (StrUtil.isBlank(mobile)) {
log.warn("短信验证码登录失败:手机号为空"); log.warn("短信验证码登录失败:手机号为空");
throw new CaptchaValidationException("手机号不能为空"); throw new SmsCaptchaException("手机号不能为空");
} }
if (StrUtil.isBlank(inputVerifyCode)) { if (StrUtil.isBlank(inputVerifyCode)) {
log.warn("短信验证码登录失败:验证码为空,手机号={}", mobile); log.warn("短信验证码登录失败:验证码为空,手机号={}", mobile);
throw new CaptchaValidationException("验证码不能为空"); throw new SmsCaptchaException("验证码不能为空");
} }
// 根据手机号获取用户信息 // 根据手机号获取用户信息
@@ -89,12 +89,12 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
if (cachedVerifyCode == null) { if (cachedVerifyCode == null) {
log.warn("短信验证码登录失败:验证码已过期,手机号={}", mobile); log.warn("短信验证码登录失败:验证码已过期,手机号={}", mobile);
throw new CaptchaValidationException("验证码已过期,请重新获取"); throw new SmsCaptchaException("验证码已过期,请重新获取");
} }
if (!StrUtil.equals(inputVerifyCode, cachedVerifyCode)) { if (!StrUtil.equals(inputVerifyCode, cachedVerifyCode)) {
log.warn("短信验证码登录失败:验证码错误,手机号={}", mobile); log.warn("短信验证码登录失败:验证码错误,手机号={}", mobile);
throw new CaptchaValidationException("验证码错误"); throw new SmsCaptchaException("验证码错误");
} }
// 验证成功后删除验证码,防止重复使用 // 验证成功后删除验证码,防止重复使用

View File

@@ -12,9 +12,9 @@ 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.common.exception.BusinessException;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.framework.security.config.SecurityProperties; import com.youlai.boot.framework.security.config.SecurityProperties;
import com.youlai.boot.framework.security.exception.TokenInvalidException;
import com.youlai.boot.framework.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.framework.security.model.RoleDataScope; import com.youlai.boot.framework.security.model.RoleDataScope;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -300,7 +300,7 @@ public class JwtTokenManager implements TokenManager {
public AuthenticationToken refreshToken(String refreshToken) { public AuthenticationToken refreshToken(String refreshToken) {
boolean isValid = validateRefreshToken(refreshToken); boolean isValid = validateRefreshToken(refreshToken);
if (!isValid) { if (!isValid) {
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); throw new TokenInvalidException(ResultCode.REFRESH_TOKEN_INVALID);
} }
Authentication authentication = parseToken(refreshToken); Authentication authentication = parseToken(refreshToken);
int accessTokenExpiration = securityProperties.getSession().getAccessTokenTimeToLive(); int accessTokenExpiration = securityProperties.getSession().getAccessTokenTimeToLive();

View File

@@ -5,9 +5,9 @@ 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.common.exception.BusinessException;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.framework.security.config.SecurityProperties; import com.youlai.boot.framework.security.config.SecurityProperties;
import com.youlai.boot.framework.security.exception.TokenInvalidException;
import com.youlai.boot.framework.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.framework.security.model.UserSession; import com.youlai.boot.framework.security.model.UserSession;
import com.youlai.boot.framework.security.model.SysUserDetails; import com.youlai.boot.framework.security.model.SysUserDetails;
@@ -19,6 +19,8 @@ import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import tools.jackson.databind.json.JsonMapper;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -46,10 +48,14 @@ public class RedisTokenManager implements TokenManager {
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
private final RedisTemplate<String, Object> redisTemplate; private final RedisTemplate<String, Object> redisTemplate;
private final JsonMapper jsonMapper;
public RedisTokenManager(SecurityProperties securityProperties, RedisTemplate<String, Object> redisTemplate) { public RedisTokenManager(SecurityProperties securityProperties,
RedisTemplate<String, Object> redisTemplate,
JsonMapper jsonMapper) {
this.securityProperties = securityProperties; this.securityProperties = securityProperties;
this.redisTemplate = redisTemplate; this.redisTemplate = redisTemplate;
this.jsonMapper = jsonMapper;
} }
/** /**
@@ -96,8 +102,9 @@ public class RedisTokenManager implements TokenManager {
*/ */
@Override @Override
public Authentication parseToken(String token) { public Authentication parseToken(String token) {
UserSession userSession = (UserSession) redisTemplate.opsForValue().get(formatTokenKey(token)); Object raw = redisTemplate.opsForValue().get(formatTokenKey(token));
if (userSession == null) return null; if (raw == null) return null;
UserSession userSession = jsonMapper.convertValue(raw, UserSession.class);
// 构建用户权限集合 // 构建用户权限集合
Set<SimpleGrantedAuthority> authorities = null; Set<SimpleGrantedAuthority> authorities = null;
@@ -144,11 +151,12 @@ public class RedisTokenManager implements TokenManager {
*/ */
@Override @Override
public AuthenticationToken refreshToken(String refreshToken) { public AuthenticationToken refreshToken(String refreshToken) {
UserSession userSession = (UserSession) redisTemplate.opsForValue() Object raw = redisTemplate.opsForValue()
.get(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken)); .get(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken));
if (userSession == null) { if (raw == null) {
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); throw new TokenInvalidException(ResultCode.REFRESH_TOKEN_INVALID);
} }
UserSession userSession = jsonMapper.convertValue(raw, UserSession.class);
Object oldAccessTokenValue = redisTemplate.opsForValue().get(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userSession.getUserId())); Object oldAccessTokenValue = redisTemplate.opsForValue().get(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userSession.getUserId()));
// 删除旧的访问令牌记录 // 删除旧的访问令牌记录
Optional.of(oldAccessTokenValue) Optional.of(oldAccessTokenValue)

View File

@@ -1,10 +1,10 @@
package com.youlai.boot.framework.web.advice; 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 com.youlai.boot.common.exception.BusinessException; import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.result.Result; import com.youlai.boot.common.result.Result;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.framework.security.exception.TokenInvalidException;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException; import jakarta.validation.ConstraintViolationException;
@@ -24,6 +24,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.NoHandlerFoundException;
import tools.jackson.core.JacksonException;
import java.sql.SQLIntegrityConstraintViolationException; import java.sql.SQLIntegrityConstraintViolationException;
import java.sql.SQLSyntaxErrorException; import java.sql.SQLSyntaxErrorException;
@@ -33,18 +34,14 @@ import java.util.stream.Collectors;
/** /**
* 全局系统异常处理器 * 全局系统异常处理器
* <p> *
* 调整异常处理的HTTP状态码丰富异常处理类型 * @author Ray.Hao
* @since 4.3.1
*/ */
@RestControllerAdvice @RestControllerAdvice
@Slf4j @Slf4j
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
/**
* 处理绑定异常
* <p>
* 当请求参数绑定到对象时发生错误,会抛出 BindException 异常。
*/
@ExceptionHandler(BindException.class) @ExceptionHandler(BindException.class)
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(BindException e) { public <T> Result<T> processException(BindException e) {
@@ -53,12 +50,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg); return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg);
} }
/**
* 处理 @RequestParam 参数校验异常
* <p>
* 当请求参数在校验过程中发生违反约束条件的异常时(如 @RequestParam 验证不通过),
* 会捕获到 ConstraintViolationException 异常。
*/
@ExceptionHandler(ConstraintViolationException.class) @ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(ConstraintViolationException e) { public <T> Result<T> processException(ConstraintViolationException e) {
@@ -67,12 +58,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.INVALID_USER_INPUT, msg); return Result.failed(ResultCode.INVALID_USER_INPUT, msg);
} }
/**
* 处理方法参数校验异常
* <p>
* 当使用 @Valid 或 @Validated 注解对方法参数进行验证时,如果验证失败,
* 会抛出 MethodArgumentNotValidException 异常。
*/
@ExceptionHandler(MethodArgumentNotValidException.class) @ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(MethodArgumentNotValidException e) { public <T> Result<T> processException(MethodArgumentNotValidException e) {
@@ -81,11 +66,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.INVALID_USER_INPUT, msg); return Result.failed(ResultCode.INVALID_USER_INPUT, msg);
} }
/**
* 处理接口不存在的异常
* <p>
* 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。
*/
@ExceptionHandler(NoHandlerFoundException.class) @ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND) @ResponseStatus(HttpStatus.NOT_FOUND)
public <T> Result<T> processException(NoHandlerFoundException e) { public <T> Result<T> processException(NoHandlerFoundException e) {
@@ -93,11 +73,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.INTERFACE_NOT_EXIST); return Result.failed(ResultCode.INTERFACE_NOT_EXIST);
} }
/**
* 处理缺少请求参数的异常
* <p>
* 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。
*/
@ExceptionHandler(MissingServletRequestParameterException.class) @ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(MissingServletRequestParameterException e) { public <T> Result<T> processException(MissingServletRequestParameterException e) {
@@ -105,11 +80,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY); return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY);
} }
/**
* 处理方法参数类型不匹配的异常
* <p>
* 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class) @ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(MethodArgumentTypeMismatchException e) { public <T> Result<T> processException(MethodArgumentTypeMismatchException e) {
@@ -117,11 +87,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误"); return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误");
} }
/**
* 处理 Servlet 异常
* <p>
* 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。
*/
@ExceptionHandler(ServletException.class) @ExceptionHandler(ServletException.class)
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(ServletException e) { public <T> Result<T> processException(ServletException e) {
@@ -129,11 +94,6 @@ public class GlobalExceptionHandler {
return Result.failed(e.getMessage()); return Result.failed(e.getMessage());
} }
/**
* 处理非法参数异常
* <p>
* 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。
*/
@ExceptionHandler(IllegalArgumentException.class) @ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) { public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) {
@@ -141,11 +101,6 @@ public class GlobalExceptionHandler {
return Result.failed(e.getMessage()); return Result.failed(e.getMessage());
} }
/**
* 处理 JSON 处理异常
* <p>
* 当处理 JSON 数据时发生错误,会抛出 JacksonException 异常。
*/
@ExceptionHandler(JacksonException.class) @ExceptionHandler(JacksonException.class)
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> handleJacksonException(JacksonException e) { public <T> Result<T> handleJacksonException(JacksonException e) {
@@ -153,11 +108,6 @@ public class GlobalExceptionHandler {
return Result.failed(e.getMessage()); return Result.failed(e.getMessage());
} }
/**
* 处理请求体不可读的异常
* <p>
* 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。
*/
@ExceptionHandler(HttpMessageNotReadableException.class) @ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(HttpMessageNotReadableException e) { public <T> Result<T> processException(HttpMessageNotReadableException e) {
@@ -170,11 +120,6 @@ public class GlobalExceptionHandler {
return Result.failed(errorMessage); return Result.failed(errorMessage);
} }
/**
* 处理类型不匹配异常
* <p>
* 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。
*/
@ExceptionHandler(TypeMismatchException.class) @ExceptionHandler(TypeMismatchException.class)
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> processException(TypeMismatchException e) { public <T> Result<T> processException(TypeMismatchException e) {
@@ -182,11 +127,6 @@ public class GlobalExceptionHandler {
return Result.failed(e.getMessage()); return Result.failed(e.getMessage());
} }
/**
* 处理 SQL 语法错误异常
* <p>
* 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。
*/
@ExceptionHandler(BadSqlGrammarException.class) @ExceptionHandler(BadSqlGrammarException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public <T> Result<T> handleBadSqlGrammarException(BadSqlGrammarException e) { public <T> Result<T> handleBadSqlGrammarException(BadSqlGrammarException e) {
@@ -197,11 +137,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.DATABASE_EXECUTION_ERROR); return Result.failed(ResultCode.DATABASE_EXECUTION_ERROR);
} }
/**
* 处理 SQL 语法错误异常
* <p>
* 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。
*/
@ExceptionHandler(SQLSyntaxErrorException.class) @ExceptionHandler(SQLSyntaxErrorException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) { public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
@@ -209,12 +144,6 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.DATABASE_EXECUTION_SYNTAX_ERROR); return Result.failed(ResultCode.DATABASE_EXECUTION_SYNTAX_ERROR);
} }
/**
* 处理 SQL 违反了完整性约束
* <p>
* 当 SQL 违反了完整性约束时,会抛出 SQLIntegrityConstraintViolationException 异常。
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class) @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public <T> Result<T> handleSQLIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException e) { public <T> Result<T> handleSQLIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException e) {
@@ -222,11 +151,12 @@ public class GlobalExceptionHandler {
return Result.failed(ResultCode.INTEGRITY_CONSTRAINT_VIOLATION); return Result.failed(ResultCode.INTEGRITY_CONSTRAINT_VIOLATION);
} }
/** @ExceptionHandler(TokenInvalidException.class)
* 处理业务异常 @ResponseStatus(HttpStatus.UNAUTHORIZED)
* <p> public <T> Result<T> handleTokenInvalidException(TokenInvalidException e) {
* 当业务逻辑发生错误时,会抛出 BusinessException 异常。 return Result.failed(e.getResultCode());
*/ }
@ExceptionHandler(BusinessException.class) @ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.OK) @ResponseStatus(HttpStatus.OK)
public <T> Result<T> handleBizException(BusinessException e) { public <T> Result<T> handleBizException(BusinessException e) {
@@ -237,15 +167,9 @@ public class GlobalExceptionHandler {
return Result.failed(e.getMessage()); return Result.failed(e.getMessage());
} }
/**
* 处理所有未捕获的异常
* <p>
* 当发生未捕获的异常时,会抛出 Exception 异常。
*/
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @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 异常继续抛出,以便交给自定义处理器处理
if (e instanceof AccessDeniedException if (e instanceof AccessDeniedException
|| e instanceof AuthenticationException) { || e instanceof AuthenticationException) {
throw e; throw e;
@@ -254,12 +178,6 @@ public class GlobalExceptionHandler {
return Result.failed(e.getLocalizedMessage()); return Result.failed(e.getLocalizedMessage());
} }
/**
* 传参类型错误时,用于消息转换
*
* @param throwable 异常
* @return 错误信息
*/
private String convertMessage(Throwable throwable) { private String convertMessage(Throwable throwable) {
String error = throwable.toString(); String error = throwable.toString();
String regulation = "\\[\"(.*?)\"]+"; String regulation = "\\[\"(.*?)\"]+";

View File

@@ -0,0 +1,146 @@
package com.youlai.boot.framework.web.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.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.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.concurrent.Executor;
/**
* 操作日志切面
* <p>@Log 标注的方法执行后异步写入 sys_log 表</p>
*
* @author Ray.Hao
* @since 0.0.1
*/
@Aspect
@Component
@Slf4j
public class LogAspect {
private final LogService logService;
private final Executor operationLogExecutor;
public LogAspect(
LogService logService,
@Qualifier("operationLogExecutor") Executor operationLogExecutor
) {
this.logService = logService;
this.operationLogExecutor = operationLogExecutor;
}
@Pointcut("@annotation(logAnnotation)")
public void pointcut(Log logAnnotation) {
}
@Around(value = "pointcut(logAnnotation)", argNames = "jp,logAnnotation")
public Object around(ProceedingJoinPoint jp, Log logAnnotation) throws Throwable {
long start = System.currentTimeMillis();
Long userId = SecurityUtils.getUserId();
String username = SecurityUtils.getUsername();
Throwable failure = null;
try {
return jp.proceed();
} catch (Throwable e) {
failure = e;
throw e;
} finally {
long elapsed = System.currentTimeMillis() - start;
if (userId == null) {
userId = SecurityUtils.getUserId();
username = SecurityUtils.getUsername();
}
try {
SysLog entity = buildEntity(logAnnotation, elapsed, failure, userId, username);
if (entity != null) {
operationLogExecutor.execute(() -> {
try {
logService.save(entity);
} catch (Exception e) {
log.error("保存操作日志失败 title={} uri={}", entity.getTitle(), entity.getRequestUri(), e);
}
});
}
} catch (Exception ex) {
log.error("构建操作日志失败", ex);
}
}
}
private SysLog buildEntity(Log ann, long elapsed, Throwable failure, Long userId, String username) {
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs == null) return null;
HttpServletRequest req = attrs.getRequest();
UserAgent ua = UserAgentUtil.parse(req.getHeader("User-Agent"));
String ip = IPUtils.getIpAddr(req);
IpRegion region = parseRegion(IPUtils.getRegion(ip));
LogModuleEnum module = ann.module();
ActionTypeEnum action = ann.value();
SysLog entity = new SysLog();
entity.setModule(module);
entity.setActionType(action);
entity.setTitle(StrUtil.blankToDefault(ann.title(), module.getLabel() + "-" + action.getLabel()));
entity.setContent(ann.content());
entity.setOperatorId(userId);
entity.setOperatorName(username);
entity.setRequestUri(req.getRequestURI());
entity.setRequestMethod(req.getMethod());
entity.setIp(ip);
entity.setProvince(region.province);
entity.setCity(region.city);
entity.setDevice(getDevice(ua));
entity.setOs(getOs(ua));
entity.setBrowser(getBrowser(ua));
entity.setStatus(failure == null ? 1 : 0);
entity.setErrorMsg(failure == null ? null : failure.getMessage());
entity.setExecutionTime((int) Math.min(elapsed, Integer.MAX_VALUE));
entity.setCreateTime(LocalDateTime.now());
return entity;
}
private IpRegion parseRegion(String region) {
if (StrUtil.isBlank(region)) return IpRegion.EMPTY;
String[] parts = region.split("\\|");
if (parts.length < 4) return IpRegion.EMPTY;
return new IpRegion(StrUtil.blankToDefault(parts[2], null), StrUtil.blankToDefault(parts[3], null));
}
private String getOs(UserAgent ua) {
return Optional.ofNullable(ua).map(UserAgent::getOs).map(os -> os.getName()).orElse(null);
}
private String getDevice(UserAgent ua) {
return Optional.ofNullable(ua).map(UserAgent::getPlatform).map(p -> p.getName()).orElse(null);
}
private String getBrowser(UserAgent ua) {
return Optional.ofNullable(ua).map(UserAgent::getBrowser).map(b -> b.getName()).orElse(null);
}
private record IpRegion(String province, String city) {
static final IpRegion EMPTY = new IpRegion(null, null);
}
}

View File

@@ -0,0 +1,106 @@
package com.youlai.boot.framework.web.aspect;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.youlai.boot.common.annotation.RateLimit;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.util.IPUtils;
import com.youlai.boot.framework.web.config.RateLimitProperties;
import com.youlai.boot.framework.web.ratelimit.SlidingWindowScript;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 接口级限流切面
* <p>
* 对标注了 {@link RateLimit} 的 Controller 方法做 Redis 滑动窗口计数。
* 超阈值抛出 {@link BusinessException}A0502全局异常处理器统一返回 JSON。
* </p>
*
* <h3>升级历史</h3>
* <ul>
* <li>4.3.1 — 初始版本:固定窗口计数器</li>
* <li>4.4.0 — 升级为滑动窗口 + X-RateLimit-* 渐进式响应头</li>
* </ul>
*
* @author Ray.Hao
* @since 4.3.1
*/
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class RateLimitAspect {
private final RedisTemplate<String, Object> redisTemplate;
private final RateLimitProperties rateLimitProperties;
@Around("@annotation(rateLimit)")
public Object handle(ProceedingJoinPoint jp, RateLimit rateLimit) throws Throwable {
HttpServletRequest request =
((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String key = buildKey(request, rateLimit);
int limit = rateLimit.limit() > 0 ? rateLimit.limit() : rateLimitProperties.getDefaultLimit();
int window = rateLimit.window() > 0 ? rateLimit.window() : rateLimitProperties.getDefaultWindow();
long windowMs = rateLimit.timeUnit().toMillis(window);
Long count = SlidingWindowScript.execute(redisTemplate, key, windowMs);
int current = count != null ? count.intValue() : 0;
setRateLimitHeaders(request, limit, current, windowMs);
if (current > limit) {
log.warn("接口限流触发 key={} count={} limit={}", key, current, limit);
throw new BusinessException(ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED);
}
return jp.proceed();
}
private String buildKey(HttpServletRequest request, RateLimit rateLimit) {
String user = resolveUser(request);
return StrUtil.format(RedisConstants.RateLimiter.API,
rateLimit.prefix(), user, request.getRequestURI());
}
private String resolveUser(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StrUtil.isNotBlank(header) && header.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
return DigestUtil.sha256Hex(header.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()));
}
return IPUtils.getIpAddr(request);
}
private void setRateLimitHeaders(HttpServletRequest request,
int limit,
int current,
long windowMs) {
ServletRequestAttributes attrs =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attrs != null) {
HttpServletResponse response = attrs.getResponse();
if (response != null) {
int remaining = Math.max(0, limit - current);
long resetAt = (System.currentTimeMillis() + windowMs) / 1000;
response.setHeader("X-RateLimit-Limit", String.valueOf(limit));
response.setHeader("X-RateLimit-Remaining", String.valueOf(remaining));
response.setHeader("X-RateLimit-Reset", String.valueOf(resetAt));
}
}
}
}

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.common.aspect; package com.youlai.boot.framework.web.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;
@@ -10,7 +10,6 @@ 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;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
@@ -28,26 +27,19 @@ import java.util.concurrent.TimeUnit;
* 防重复提交切面 * 防重复提交切面
* *
* @author Ray.Hao * @author Ray.Hao
* @since 2.3.0 * @since 4.3.1
*/ */
@Aspect @Aspect
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class RepeatSubmitAspect { public class RepeatSubmitAspect {
private final RedissonClient redissonClient; private final RedissonClient redissonClient;
/**
* 防重复提交切点
*/
@Pointcut("@annotation(repeatSubmit)") @Pointcut("@annotation(repeatSubmit)")
public void repeatSubmitPointCut(RepeatSubmit repeatSubmit) { public void repeatSubmitPointCut(RepeatSubmit repeatSubmit) {
} }
/**
* 环绕通知处理防重复提交逻辑
*/
@Around(value = "repeatSubmitPointCut(repeatSubmit)", argNames = "pjp,repeatSubmit") @Around(value = "repeatSubmitPointCut(repeatSubmit)", argNames = "pjp,repeatSubmit")
public Object handleRepeatSubmit(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable { public Object handleRepeatSubmit(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
String lockKey = buildLockKey(); String lockKey = buildLockKey();
@@ -64,13 +56,10 @@ public class RepeatSubmitAspect {
/** /**
* 生成防重复提交锁的 key * 生成防重复提交锁的 key
* @return 锁的 key
*/ */
private String buildLockKey() { private String buildLockKey() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 用户唯一标识
String userIdentifier = getUserIdentifier(request); String userIdentifier = getUserIdentifier(request);
// 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法)
String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI());
return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier);
} }
@@ -79,24 +68,17 @@ public class RepeatSubmitAspect {
* 获取用户唯一标识 * 获取用户唯一标识
* 1. 从请求头中获取 Token使用 SHA-256 加密 Token 作为用户唯一标识 * 1. 从请求头中获取 Token使用 SHA-256 加密 Token 作为用户唯一标识
* 2. 如果 Token 为空使用 IP 作为用户唯一标识 * 2. 如果 Token 为空使用 IP 作为用户唯一标识
*
* @param request 请求对象
* @return 用户唯一标识
*/ */
private String getUserIdentifier(HttpServletRequest request) { private String getUserIdentifier(HttpServletRequest request) {
// 用户身份唯一标识
String userIdentifier; String userIdentifier;
// 从请求头中获取 Token
String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识 userIdentifier = DigestUtil.sha256Hex(rawToken);
} else { } else {
userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识 userIdentifier = IPUtils.getIpAddr(request);
} }
return userIdentifier; return userIdentifier;
} }
} }

View File

@@ -0,0 +1,32 @@
package com.youlai.boot.framework.web.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 操作日志线程池
*
* @author Ray.Hao
* @since 3.0.0
*/
@Configuration
public class OperationLogExecutorConfig {
@Bean
public Executor operationLogExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setThreadNamePrefix("operation-log-");
executor.setCorePoolSize(1);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(1000);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(10);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,57 @@
package com.youlai.boot.framework.web.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 限流配置
* <p>
* 对应 application.yml 中的 {@code rate-limit} 配置节点。
* </p>
*
* @author Ray.Hao
* @since 4.3.1
*/
@Data
@Component
@ConfigurationProperties(prefix = "rate-limit")
public class RateLimitProperties {
/**
* @RateLimit 注解未显式指定 limit 时的默认阈值
*/
private int defaultLimit = 5;
/**
* @RateLimit 注解未显式指定 window 时的默认窗口大小(秒)
*/
private int defaultWindow = 1;
/**
* IP 全局限流配置
*/
private Ip ip = new Ip();
@Data
public static class Ip {
/**
* 是否启用 IP 全局限流。
* 建议生产环境开启,开发/测试环境酌情关闭。
*/
private boolean enabled = true;
/**
* 窗口内最大请求数(默认 1000
*/
private int limit = 1000;
/**
* 窗口大小(秒,默认 60
*/
private int windowSeconds = 60;
}
}

View File

@@ -0,0 +1,106 @@
package com.youlai.boot.framework.web.filter;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.util.IPUtils;
import com.youlai.boot.framework.web.config.RateLimitProperties;
import com.youlai.boot.framework.web.ratelimit.SlidingWindowScript;
import com.youlai.boot.framework.web.util.ResponseWriter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
/**
* IP 全局限流过滤器
* <p>
* 在所有请求进入 Controller 之前,按 IP 维度做滑动窗口限流。
* 与 {@code @RateLimit} 注解级限流叠加:先过 IP 全局,再过接口级。
* </p>
*
* <h3>限流维度</h3>
* <pre>{@code
* Key: rate_limiter:ip:{clientIp}
* 默认: 1000 req / 60s可通过 rate-limit.ip.* 配置)
* }</pre>
*
* <h3>响应头</h3>
* <table>
* <tr><td>X-RateLimit-Limit</td><td>窗口内最大允许请求数</td></tr>
* <tr><td>X-RateLimit-Remaining</td><td>窗口内剩余可用请求数</td></tr>
* <tr><td>X-RateLimit-Reset</td><td>窗口重置时间Unix 秒)</td></tr>
* <tr><td>Retry-After</td><td>超限时建议重试等待秒数(仅 429</td></tr>
* </table>
*
* @author Ray.Hao
* @since 4.4.0
*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 5)
@RequiredArgsConstructor
@Slf4j
public class IpRateLimitFilter extends OncePerRequestFilter {
private final RedisTemplate<String, Object> redisTemplate;
private final RateLimitProperties rateLimitProperties;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 未启用时直接放行
RateLimitProperties.Ip ipConfig = rateLimitProperties.getIp();
if (!ipConfig.isEnabled()) {
filterChain.doFilter(request, response);
return;
}
String ip = IPUtils.getIpAddr(request);
String key = StrUtil.format(RedisConstants.RateLimiter.IP, ip);
long windowMs = ipConfig.getWindowSeconds() * 1000L;
// 执行滑动窗口计数Lua 原子操作)
Long count = SlidingWindowScript.execute(redisTemplate, key, windowMs);
int limit = ipConfig.getLimit();
int current = count != null ? count.intValue() : 0;
setRateLimitHeaders(response, limit, current, windowMs);
if (current > limit) {
log.warn("IP 限流触发 ip={} count={} limit={}", ip, current, limit);
response.setHeader("Retry-After", String.valueOf(ipConfig.getWindowSeconds()));
ResponseWriter.writeError(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED);
return;
}
filterChain.doFilter(request, response);
}
/**
* 设置渐进式限流响应头X-RateLimit-Limit / Remaining / Reset
* 超限时额外返回 Retry-After。
*/
private void setRateLimitHeaders(HttpServletResponse response,
int limit,
int current,
long windowMs) {
int remaining = Math.max(0, limit - current);
long resetAt = (System.currentTimeMillis() + windowMs) / 1000;
response.setHeader("X-RateLimit-Limit", String.valueOf(limit));
response.setHeader("X-RateLimit-Remaining", String.valueOf(remaining));
response.setHeader("X-RateLimit-Reset", String.valueOf(resetAt));
}
}

View File

@@ -1,98 +0,0 @@
package com.youlai.boot.framework.web.filter;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SystemConstants;
import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.util.IPUtils;
import com.youlai.boot.common.result.ResponseWriter;
import com.youlai.boot.system.service.ConfigService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* IP 限流过滤器
*
* @author Theo
* @since 2024/08/10 14:38
*/
@Slf4j
public class RateLimiterFilter extends OncePerRequestFilter {
private final RedisTemplate<String, Object> redisTemplate;
private final ConfigService configService;
private static final long DEFAULT_IP_LIMIT = 10L; // 默认 IP 限流阈值
public RateLimiterFilter(RedisTemplate<String, Object> redisTemplate, ConfigService configService) {
this.redisTemplate = redisTemplate;
this.configService = configService;
}
/**
* 判断 IP 是否触发限流
* 默认限制同一 IP 每秒最多请求 10 次,可通过系统配置调整。
* 如果系统未配置限流阈值,默认跳过限流。
*
* @param ip IP 地址
* @return 是否限流true 表示限流false 表示未限流
*/
public boolean rateLimit(String ip) {
// 限流 Redis 键
String key = StrUtil.format(RedisConstants.RateLimiter.IP, ip);
// 自增请求计数
Long count = redisTemplate.opsForValue().increment(key);
if (count == null || count == 1) {
// 第一次访问时设置过期时间为 1 秒
redisTemplate.expire(key, 1, TimeUnit.SECONDS);
}
// 获取系统配置的限流阈值
Object systemConfig = configService.getSystemConfig(SystemConstants.SYSTEM_CONFIG_IP_QPS_LIMIT_KEY);
if (systemConfig == null) {
// 系统未配置限流,跳过限流逻辑
log.warn("系统未配置限流阈值,跳过限流");
return false;
}
// 转换系统配置为限流值,默认为 10
long limit = Convert.toLong(systemConfig, DEFAULT_IP_LIMIT);
return count != null && count > limit;
}
/**
* 执行 IP 限流逻辑
* 如果 IP 请求超出限制,直接返回限流响应;否则继续执行过滤器链。
*
* @param request 请求体
* @param response 响应体
* @param filterChain 过滤器链
*/
@Override
protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
@NotNull FilterChain filterChain) throws ServletException, IOException {
// 获取请求的 IP 地址
String ip = IPUtils.getIpAddr(request);
// 判断是否限流
if (rateLimit(ip)) {
// 返回限流错误信息
ResponseWriter.writeError(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED);
return;
}
// 未触发限流,继续执行过滤器链
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,69 @@
package com.youlai.boot.framework.web.ratelimit;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import java.util.Collections;
import java.util.UUID;
/**
* 滑动窗口限流 Lua 脚本(共享工具)
* <p>
* 基于 Redis Sorted Set 实现原子化的滑动窗口计数:
* <ol>
* <li>ZREMRANGEBYSCORE 清除窗口外旧请求</li>
* <li>ZADD 添加当前请求</li>
* <li>ZCARD 统计窗口内请求数</li>
* <li>PEXPIRE 设置 Key 过期(窗口 + 1s 冗余)</li>
* </ol>
* 一次网络往返完成全部操作,避免多次 Redis 调用之间的竞态条件。
* </p>
*
* @author Ray.Hao
* @since 4.4.0
*/
public final class SlidingWindowScript {
private SlidingWindowScript() {
throw new UnsupportedOperationException("工具类不允许实例化");
}
private static final DefaultRedisScript<Long> SCRIPT;
static {
SCRIPT = new DefaultRedisScript<>();
SCRIPT.setScriptText(
"local key = KEYS[1] " +
"local now = tonumber(ARGV[1]) " +
"local window = tonumber(ARGV[2]) " +
"local member = ARGV[3] " +
"redis.call('ZREMRANGEBYSCORE', key, 0, now - window) " +
"redis.call('ZADD', key, now, member) " +
"redis.call('PEXPIRE', key, window + 1000) " +
"return redis.call('ZCARD', key)"
);
SCRIPT.setResultType(Long.class);
}
/**
* 执行滑动窗口计数
*
* @param redisTemplate Redis 模板
* @param key 限流 Key如 {@code rate_limiter:ip:192.168.1.1}
* @param windowMs 窗口大小(毫秒)
* @return 当前窗口内请求数(包含本次)
*/
public static Long execute(RedisTemplate<String, Object> redisTemplate,
String key,
long windowMs) {
// 传 Long 而非 String避免 Jackson 序列化器加双引号导致 Lua tonumber() 返回 nil
return redisTemplate.execute(
SCRIPT,
Collections.singletonList(key),
System.currentTimeMillis(),
windowMs,
UUID.randomUUID().toString()
);
}
}

View File

@@ -0,0 +1,63 @@
package com.youlai.boot.framework.web.util;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.common.result.ResultCode;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import java.nio.charset.StandardCharsets;
/**
* 响应写入器
* <p>用于过滤器、Security处理器等场景统一写入HTTP响应</p>
*/
@Slf4j
public final class ResponseWriter {
private ResponseWriter() {
throw new UnsupportedOperationException("工具类不允许实例化");
}
public static void writeSuccess(HttpServletResponse response, Object data) {
writeResult(response, Result.success(data), HttpStatus.OK.value());
}
public static void writeSuccess(HttpServletResponse response) {
writeSuccess(response, null);
}
public static void writeError(HttpServletResponse response, ResultCode resultCode) {
writeError(response, resultCode, null);
}
public static void writeError(HttpServletResponse response, ResultCode resultCode, String message) {
Result<?> result = message == null
? Result.failed(resultCode)
: Result.failed(resultCode, message);
int httpStatus = mapHttpStatus(resultCode);
writeResult(response, result, httpStatus);
}
private static void writeResult(HttpServletResponse response, Result<?> result, int httpStatus) {
try {
response.setStatus(httpStatus);
response.setCharacterEncoding(StandardCharsets.UTF_8.toString());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
JakartaServletUtil.write(response, JSONUtil.toJsonStr(result), MediaType.APPLICATION_JSON_VALUE);
} catch (Exception e) {
log.error("写入响应异常 httpStatus={}", httpStatus, e);
}
}
private static int mapHttpStatus(ResultCode resultCode) {
return switch (resultCode) {
case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
case ACCESS_PERMISSION_EXCEPTION -> HttpStatus.FORBIDDEN.value();
default -> HttpStatus.BAD_REQUEST.value();
};
}
}

View File

@@ -8,31 +8,22 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
* 在线用户统计定时任务 * 在线用户统计
* <p> * <p>定时统计并广播当前在线用户数量</p>
* 定时统计并广播当前在线用户数量到所有 SSE 客户端
*/ */
@Component @Component
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class OnlineUserCountJob { public class OnlineUserCountTask {
private final SseSessionRegistry sessionRegistry; private final SseSessionRegistry sessionRegistry;
private final SseService sseService; private final SseService sseService;
/**
* 定时统计在线用户数并广播
* <p>
* 每3分钟执行一次推送当前在线用户数量
*/
@Scheduled(cron = "0 */3 * * * ?") @Scheduled(cron = "0 */3 * * * ?")
public void execute() { public void execute() {
int onlineCount = sessionRegistry.getOnlineUserCount(); int onlineCount = sessionRegistry.getOnlineUserCount();
int connectionCount = sessionRegistry.getTotalConnectionCount(); int connectionCount = sessionRegistry.getTotalConnectionCount();
log.debug("在线用户数={} 总连接数={}", onlineCount, connectionCount);
log.debug("定时统计:在线用户数={}, 总连接数={}", onlineCount, connectionCount);
// 发送在线用户数量
sseService.sendOnlineCount(); sseService.sendOnlineCount();
} }
} }

View File

@@ -2,11 +2,11 @@ package com.youlai.boot.system.controller;
import cn.idev.excel.EasyExcel; import cn.idev.excel.EasyExcel;
import cn.idev.excel.ExcelWriter; import cn.idev.excel.ExcelWriter;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.youlai.boot.common.annotation.Log; import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.common.annotation.RepeatSubmit; import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.common.enums.ActionTypeEnum; 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.common.enums.StatusEnum;
import com.youlai.boot.framework.security.util.SecurityUtils; import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.common.model.Option; import com.youlai.boot.common.model.Option;
import com.youlai.boot.common.result.ExcelResult; import com.youlai.boot.common.result.ExcelResult;
@@ -14,10 +14,14 @@ import com.youlai.boot.common.result.PageResult;
import com.youlai.boot.common.result.Result; import com.youlai.boot.common.result.Result;
import com.youlai.boot.common.util.ExcelUtils; import com.youlai.boot.common.util.ExcelUtils;
import com.youlai.boot.system.listener.UserImportListener; import com.youlai.boot.system.listener.UserImportListener;
import com.youlai.boot.system.model.vo.UserExportVO; import com.youlai.boot.system.model.form.EmailUpdateForm;
import com.youlai.boot.system.model.form.MobileUpdateForm;
import com.youlai.boot.system.model.form.PasswordUpdateForm;
import com.youlai.boot.system.model.form.PasswordVerifyForm;
import com.youlai.boot.system.model.form.UserForm;
import com.youlai.boot.system.model.form.UserImportForm; import com.youlai.boot.system.model.form.UserImportForm;
import com.youlai.boot.system.model.entity.SysUser; import com.youlai.boot.system.model.form.UserProfileForm;
import com.youlai.boot.system.model.form.*; import com.youlai.boot.system.model.vo.UserExportVO;
import com.youlai.boot.system.model.query.UserQuery; import com.youlai.boot.system.model.query.UserQuery;
import com.youlai.boot.system.model.vo.CurrentUserVO; import com.youlai.boot.system.model.vo.CurrentUserVO;
import com.youlai.boot.system.model.vo.UserPageVO; import com.youlai.boot.system.model.vo.UserPageVO;
@@ -33,7 +37,16 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.File;
@@ -47,7 +60,7 @@ import java.util.List;
* 用户控制层 * 用户控制层
* *
* @author Ray.Hao * @author Ray.Hao
* @since 2022/10/16 * @since 4.3.1
*/ */
@Tag(name = "02.用户接口") @Tag(name = "02.用户接口")
@RestController @RestController
@@ -121,12 +134,9 @@ public class UserController {
@Parameter(description = "用户ID") @PathVariable Long userId, @Parameter(description = "用户ID") @PathVariable Long userId,
@Parameter(description = "用户状态(1:启用;0:禁用)") @RequestParam Integer status @Parameter(description = "用户状态(1:启用;0:禁用)") @RequestParam Integer status
) { ) {
boolean result = userService.update(new LambdaUpdateWrapper<SysUser>() boolean result = userService.updateUserStatus(userId, status);
.eq(SysUser::getId, userId)
.set(SysUser::getStatus, status)
);
// 用户禁用时立即失效其会话 // 用户禁用时立即失效其会话
if (result && status == 0) { if (result && StatusEnum.DISABLE.getValue().equals(status)) {
tokenManager.invalidateUserSessions(userId); tokenManager.invalidateUserSessions(userId);
} }
return Result.judge(result); return Result.judge(result);

View File

@@ -9,6 +9,7 @@ import lombok.Getter;
* *
* C目录 * C目录
* M菜单 * M菜单
* E外链
* B按钮 * B按钮
*/ */
@Getter @Getter
@@ -16,6 +17,7 @@ public enum MenuTypeEnum implements IBaseEnum<String> {
CATALOG("C", "目录"), CATALOG("C", "目录"),
MENU("M", "菜单"), MENU("M", "菜单"),
EXTERNAL("E", "外链"),
BUTTON("B", "按钮"); BUTTON("B", "按钮");
/** /**

View File

@@ -13,7 +13,7 @@ import lombok.EqualsAndHashCode;
* @since 2024-07-29 11:17:26 * @since 2024-07-29 11:17:26
*/ */
@Data @Data
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = true)
@Schema(description = "系统配置") @Schema(description = "系统配置")
@TableName("sys_config") @TableName("sys_config")
public class Config extends BaseEntity { public class Config extends BaseEntity {

View File

@@ -4,8 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.youlai.boot.common.base.BaseEntity; import com.youlai.boot.common.base.BaseEntity;
import lombok.Getter; import lombok.Data;
import lombok.Setter; import lombok.EqualsAndHashCode;
/** /**
* 部门实体对象 * 部门实体对象
@@ -14,8 +14,8 @@ import lombok.Setter;
* @since 2024/06/23 * @since 2024/06/23
*/ */
@TableName("sys_dept") @TableName("sys_dept")
@Getter @Data
@Setter @EqualsAndHashCode(callSuper = true)
public class Dept extends BaseEntity { public class Dept extends BaseEntity {
/** /**

View File

@@ -11,7 +11,7 @@ import lombok.EqualsAndHashCode;
* @author Ray.Hao * @author Ray.Hao
* @since 2022/12/17 * @since 2022/12/17
*/ */
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = true)
@TableName("sys_dict") @TableName("sys_dict")
@Data @Data
public class Dict extends BaseEntity { public class Dict extends BaseEntity {

View File

@@ -11,7 +11,7 @@ import lombok.EqualsAndHashCode;
* @author Ray.Hao * @author Ray.Hao
* @since 2022/12/17 * @since 2022/12/17
*/ */
@EqualsAndHashCode(callSuper = false) @EqualsAndHashCode(callSuper = true)
@TableName("sys_dict_item") @TableName("sys_dict_item")
@Data @Data
public class DictItem extends BaseEntity { public class DictItem extends BaseEntity {

View File

@@ -2,10 +2,10 @@ package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Getter; import com.youlai.boot.common.base.BaseEntity;
import lombok.Setter; import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
import java.util.Map; import java.util.Map;
/** /**
@@ -15,15 +15,9 @@ import java.util.Map;
* @since 2023/3/6 * @since 2023/3/6
*/ */
@TableName(value = "sys_menu", autoResultMap = true) @TableName(value = "sys_menu", autoResultMap = true)
@Getter @Data
@Setter @EqualsAndHashCode(callSuper = true)
public class Menu { public class Menu extends BaseEntity {
/**
* 菜单ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/** /**
* 父菜单ID * 父菜单ID
*/ */
@@ -35,7 +29,7 @@ public class Menu {
private String name; private String name;
/** /**
* 菜单类型(C-目录 M-菜单 B-按钮) * 菜单类型(C-目录 M-菜单 E-外链 B-按钮)
*/ */
private String type; private String type;
@@ -54,6 +48,11 @@ public class Menu {
*/ */
private String component; private String component;
/**
* 外链地址
*/
private String externalUrl;
/** /**
* 权限标识 * 权限标识
*/ */
@@ -100,14 +99,4 @@ public class Menu {
@TableField(updateStrategy = FieldStrategy.ALWAYS, typeHandler = JacksonTypeHandler.class) @TableField(updateStrategy = FieldStrategy.ALWAYS, typeHandler = JacksonTypeHandler.class)
private Map<String, Object> params; private Map<String, Object> params;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
} }

View File

@@ -4,8 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.youlai.boot.common.base.BaseEntity; import com.youlai.boot.common.base.BaseEntity;
import lombok.Getter; import lombok.Data;
import lombok.Setter; import lombok.EqualsAndHashCode;
import java.io.Serial; import java.io.Serial;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -16,8 +16,8 @@ import java.time.LocalDateTime;
* @author Kylin * @author Kylin
* @since 2024-08-27 10:31 * @since 2024-08-27 10:31
*/ */
@Getter @Data
@Setter @EqualsAndHashCode(callSuper = true)
@TableName("sys_notice") @TableName("sys_notice")
public class Notice extends BaseEntity { public class Notice extends BaseEntity {

View File

@@ -4,8 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.youlai.boot.common.base.BaseEntity; import com.youlai.boot.common.base.BaseEntity;
import lombok.Getter; import lombok.Data;
import lombok.Setter; import lombok.EqualsAndHashCode;
/** /**
* 角色实体 * 角色实体
@@ -14,8 +14,8 @@ import lombok.Setter;
* @since 2024/6/23 * @since 2024/6/23
*/ */
@TableName("sys_role") @TableName("sys_role")
@Getter @Data
@Setter @EqualsAndHashCode(callSuper = true)
public class Role extends BaseEntity { public class Role extends BaseEntity {
/** /**

View File

@@ -2,15 +2,15 @@ package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.youlai.boot.common.base.BaseEntity; import com.youlai.boot.common.base.BaseEntity;
import lombok.Getter; import lombok.Data;
import lombok.Setter; import lombok.EqualsAndHashCode;
/** /**
* 用户实体 * 用户实体
*/ */
@TableName("sys_user") @TableName("sys_user")
@Getter @Data
@Setter @EqualsAndHashCode(callSuper = true)
public class SysUser extends BaseEntity { public class SysUser extends BaseEntity {
/** /**
@@ -24,7 +24,7 @@ public class SysUser extends BaseEntity {
private String nickname; private String nickname;
/** /**
* 性别((1-男 2-女 0-保密) * 性别(1-男 2-女 0-保密)
*/ */
private Integer gender; private Integer gender;
@@ -49,7 +49,7 @@ public class SysUser extends BaseEntity {
private String mobile; private String mobile;
/** /**
* 状态((1-正常 0-禁用) * 状态(1-正常 0-禁用)
*/ */
private Integer status; private Integer status;

View File

@@ -1,14 +1,12 @@
package com.youlai.boot.system.model.entity; package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.youlai.boot.common.base.BaseEntity; import com.youlai.boot.common.base.BaseEntity;
import lombok.Getter; import lombok.Data;
import lombok.Setter; import lombok.EqualsAndHashCode;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -18,37 +16,21 @@ import java.time.LocalDateTime;
* @author Kylin * @author Kylin
* @since 2024-08-28 16:56 * @since 2024-08-28 16:56
*/ */
@Getter @Data
@Setter @EqualsAndHashCode(callSuper = true)
@TableName("sys_user_notice") @TableName("sys_user_notice")
public class UserNotice extends BaseEntity { public class UserNotice extends BaseEntity {
/** /** 公共通知id */
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 公共通知id
*/
private Long noticeId; private Long noticeId;
/** /** 用户id */
* 用户id
*/
private Long userId; private Long userId;
/** /** 读取状态0未读1已读 */
* 读取状态0未读1已读
*/
private Integer isRead; private Integer isRead;
/** /** 用户阅读时间 */
* 用户阅读时间
*/
private LocalDateTime readTime; private LocalDateTime readTime;
/** 逻辑删除标识(0-未删除 1-已删除) */
/**
* 逻辑删除标识(0-未删除 1-已删除)
*/
@TableLogic(value = "0", delval = "1") @TableLogic(value = "0", delval = "1")
private Integer isDeleted; private Integer isDeleted;
} }

View File

@@ -26,7 +26,7 @@ public class MenuForm {
@Schema(description = "菜单名称") @Schema(description = "菜单名称")
private String name; private String name;
@Schema(description = "菜单类型C-目录 M-菜单 B-按钮)") @Schema(description = "菜单类型C-目录 M-菜单 E-外链 B-按钮)")
private String type; private String type;
@Schema(description = "路由名称") @Schema(description = "路由名称")
@@ -38,6 +38,9 @@ public class MenuForm {
@Schema(description = "组件路径(vue页面完整路径省略.vue后缀)") @Schema(description = "组件路径(vue页面完整路径省略.vue后缀)")
private String component; private String component;
@Schema(description = "外链地址")
private String externalUrl;
@Schema(description = "权限标识") @Schema(description = "权限标识")
private String perm; private String perm;

View File

@@ -19,7 +19,7 @@ public class MenuVO {
@Schema(description = "菜单名称") @Schema(description = "菜单名称")
private String name; private String name;
@Schema(description="菜单类型C-目录 M-菜单 B-按钮)") @Schema(description="菜单类型C-目录 M-菜单 E-外链 B-按钮)")
private String type; private String type;
@Schema(description = "路由名称") @Schema(description = "路由名称")
@@ -31,6 +31,9 @@ public class MenuVO {
@Schema(description = "组件路径") @Schema(description = "组件路径")
private String component; private String component;
@Schema(description = "外链地址")
private String externalUrl;
@Schema(description = "菜单排序(数字越小排名越靠前)") @Schema(description = "菜单排序(数字越小排名越靠前)")
private Integer sort; private Integer sort;

View File

@@ -54,6 +54,10 @@ public class RouteVO {
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
private Boolean alwaysShow; private Boolean alwaysShow;
@Schema(description = "外链地址(内嵌模式用)", example = "https://www.youlai.tech")
@JsonInclude(JsonInclude.Include.NON_NULL)
private String externalUrl;
@Schema(description = "路由参数") @Schema(description = "路由参数")
private Map<String,String> params; private Map<String,String> params;
} }

View File

@@ -58,11 +58,4 @@ public interface ConfigService extends IService<Config> {
*/ */
boolean refreshCache(); boolean refreshCache();
/**
* 获取系统配置
* @param key 配置键
* @return 配置值
*/
Object getSystemConfig(String key);
} }

View File

@@ -55,6 +55,15 @@ public interface UserService extends IService<SysUser> {
*/ */
boolean updateUser(Long userId, UserForm userForm); boolean updateUser(Long userId, UserForm userForm);
/**
* 修改用户状态
*
* @param userId 用户ID
* @param status 状态值
* @return {@link Boolean} 是否修改成功
*/
boolean updateUserStatus(Long userId, Integer status);
/** /**
* 删除用户 * 删除用户

View File

@@ -142,18 +142,5 @@ public class ConfigServiceImpl extends ServiceImpl<ConfigMapper, Config> impleme
return false; return false;
} }
/**
* 获取系统配置
*
* @param key 配置键
* @return 配置值
*/
@Override
public Object getSystemConfig(String key) {
if (StringUtils.isNotBlank(key)) {
return redisTemplate.opsForHash().get(RedisConstants.System.CONFIG, key);
}
return null;
} }
}

View File

@@ -227,36 +227,50 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
private RouteVO toRouteVo(Menu menu) { private RouteVO toRouteVo(Menu menu) {
RouteVO routeVo = new RouteVO(); RouteVO routeVo = new RouteVO();
String routePath = menu.getRoutePath(); String routePath = menu.getRoutePath();
boolean externalLink = StrUtil.startWithAny(routePath, "http://", "https://"); String externalUrl = menu.getExternalUrl();
// 获取路由名称 // 外链(type=E)component 为空是新标签页component=iframe 是系统内嵌
boolean isExternal = MenuTypeEnum.EXTERNAL.getValue().equals(menu.getType());
boolean isEmbedded = isExternal && "iframe".equals(menu.getComponent());
// 新标签页path 用外链地址内嵌path 用内部路由路径
String path = isExternal && !isEmbedded && StrUtil.isNotBlank(externalUrl)
? externalUrl
: routePath;
routeVo.setPath(path);
// 路由名称:外链不设 name前端 filterRoutes 会过滤出路由注册表)
String routeName = menu.getRouteName(); String routeName = menu.getRouteName();
if (StrUtil.isBlank(routeName)) { if (StrUtil.isBlank(routeName) && !isExternal) {
// 外链不做驼峰转换,使用唯一占位,避免 http:// 被解析异常 routeName = StringUtils.capitalize(StrUtil.toCamelCase(path, '-'));
routeName = externalLink
? "ext-" + menu.getId()
: StringUtils.capitalize(StrUtil.toCamelCase(routePath, '-'));
} }
// 根据name路由跳转 this.$router.push({name:xxx})
routeVo.setName(routeName); routeVo.setName(routeName);
// 根据path路由跳转 this.$router.push({path:xxx})
routeVo.setPath(routePath);
routeVo.setRedirect(menu.getRedirect()); routeVo.setRedirect(menu.getRedirect());
// 外链无组件
routeVo.setComponent(externalLink ? null : menu.getComponent()); if (isEmbedded) {
routeVo.setComponent("iframe");
} else if (isExternal) {
routeVo.setComponent(null);
} else {
routeVo.setComponent(menu.getComponent());
}
RouteVO.Meta meta = new RouteVO.Meta(); RouteVO.Meta meta = new RouteVO.Meta();
meta.setTitle(menu.getName()); meta.setTitle(menu.getName());
meta.setIcon(menu.getIcon()); meta.setIcon(menu.getIcon());
meta.setHidden(StatusEnum.DISABLE.getValue().equals(menu.getVisible())); meta.setHidden(StatusEnum.DISABLE.getValue().equals(menu.getVisible()));
// 【菜单】是否开启页面缓存 // 【菜单/内嵌外链】是否开启页面缓存
if (MenuTypeEnum.MENU.getValue().equals(menu.getType()) if ((MenuTypeEnum.MENU.getValue().equals(menu.getType()) || isEmbedded)
&& ObjectUtil.equals(menu.getKeepAlive(), 1)) { && ObjectUtil.equals(menu.getKeepAlive(), 1)) {
meta.setKeepAlive(true); meta.setKeepAlive(true);
} }
meta.setAlwaysShow(ObjectUtil.equals(menu.getAlwaysShow(), 1)); meta.setAlwaysShow(ObjectUtil.equals(menu.getAlwaysShow(), 1));
if (isEmbedded && StrUtil.isNotBlank(externalUrl)) {
meta.setExternalUrl(externalUrl);
}
Map<String, Object> paramsMap = menu.getParams(); Map<String, Object> paramsMap = menu.getParams();
if (paramsMap != null && !paramsMap.isEmpty()) { if (paramsMap != null && !paramsMap.isEmpty()) {
Map<String, String> paramMap = paramsMap.entrySet().stream() Map<String, String> paramMap = paramsMap.entrySet().stream()
@@ -275,8 +289,8 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
public boolean saveMenu(MenuForm menuForm) { public boolean saveMenu(MenuForm menuForm) {
String menuType = menuForm.getType(); String menuType = menuForm.getType();
boolean isExternalLink = MenuTypeEnum.MENU.getValue().equals(menuType) boolean isExternal = MenuTypeEnum.EXTERNAL.getValue().equals(menuType);
&& StrUtil.startWithAny(menuForm.getRoutePath(), "http://", "https://"); boolean isEmbedded = isExternal && "iframe".equals(menuForm.getComponent());
if (MenuTypeEnum.CATALOG.getValue().equals(menuType)) { // 如果是目录 if (MenuTypeEnum.CATALOG.getValue().equals(menuType)) { // 如果是目录
String path = menuForm.getRoutePath(); String path = menuForm.getRoutePath();
@@ -284,8 +298,8 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
menuForm.setRoutePath("/" + path); // 一级目录需以 / 开头 menuForm.setRoutePath("/" + path); // 一级目录需以 / 开头
} }
menuForm.setComponent("Layout"); menuForm.setComponent("Layout");
} else if (isExternalLink) { } else if (isExternal && !isEmbedded) {
// 外链菜单组件设置为 null通过 routePath 判断外链 // 外链新标签页component 留空
menuForm.setComponent(null); menuForm.setComponent(null);
} }
if (Objects.equals(menuForm.getParentId(), menuForm.getId())) { if (Objects.equals(menuForm.getParentId(), menuForm.getId())) {
@@ -302,8 +316,9 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
} else { } else {
entity.setParams(null); entity.setParams(null);
} }
// 新增类型为菜单时候 路由名称唯一 // 菜单(M)和内嵌外链(E+iframe)需要路由名称唯一
if (MenuTypeEnum.MENU.getValue().equals(menuType) && !isExternalLink) { boolean needsRouteName = MenuTypeEnum.MENU.getValue().equals(menuType) || isEmbedded;
if (needsRouteName) {
Assert.isFalse(this.exists(new LambdaQueryWrapper<Menu>() Assert.isFalse(this.exists(new LambdaQueryWrapper<Menu>()
.eq(Menu::getRouteName, entity.getRouteName()) .eq(Menu::getRouteName, entity.getRouteName())
.ne(menuForm.getId() != null, Menu::getId, menuForm.getId()) .ne(menuForm.getId() != null, Menu::getId, menuForm.getId())

View File

@@ -22,13 +22,13 @@ import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.system.converter.UserConverter; import com.youlai.boot.system.converter.UserConverter;
import com.youlai.boot.system.enums.DictCodeEnum; import com.youlai.boot.system.enums.DictCodeEnum;
import com.youlai.boot.system.mapper.UserMapper; import com.youlai.boot.system.mapper.UserMapper;
import com.youlai.boot.system.model.form.*;
import com.youlai.boot.system.model.vo.CurrentUserVO; import com.youlai.boot.system.model.vo.CurrentUserVO;
import com.youlai.boot.system.model.vo.UserExportVO; import com.youlai.boot.system.model.vo.UserExportVO;
import com.youlai.boot.system.model.entity.Dept; import com.youlai.boot.system.model.entity.Dept;
import com.youlai.boot.system.model.entity.DictItem; import com.youlai.boot.system.model.entity.DictItem;
import com.youlai.boot.system.model.entity.Role; import com.youlai.boot.system.model.entity.Role;
import com.youlai.boot.system.model.entity.SysUser; import com.youlai.boot.system.model.entity.SysUser;
import com.youlai.boot.system.model.form.*;
import com.youlai.boot.system.model.query.UserQuery; import com.youlai.boot.system.model.query.UserQuery;
import com.youlai.boot.system.model.vo.UserPageVO; import com.youlai.boot.system.model.vo.UserPageVO;
import com.youlai.boot.system.model.vo.UserProfileVO; import com.youlai.boot.system.model.vo.UserProfileVO;
@@ -664,6 +664,14 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, SysUser> implements
); );
} }
@Override
public boolean updateUserStatus(Long userId, Integer status) {
return this.update(new LambdaUpdateWrapper<SysUser>()
.eq(SysUser::getId, userId)
.set(SysUser::getStatus, status)
);
}
/** /**
* 获取用户选项列表 * 获取用户选项列表
* *

View File

@@ -80,6 +80,13 @@ mybatis-plus:
# 这个配置会将执行的sql打印出来在开发或测试的时候可以用 # 这个配置会将执行的sql打印出来在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 接口限流
rate-limit:
default-limit: 60
default-window: 60
ip:
enabled: false # 开发环境关闭 IP 全局限流
# 安全配置 # 安全配置
security: security:
session: session:
@@ -181,14 +188,16 @@ springdoc:
packages-to-scan: # 扫描的 Controller 包,限制只生成指定包的接口文档 packages-to-scan: # 扫描的 Controller 包,限制只生成指定包的接口文档
- com.youlai.boot.auth.controller - com.youlai.boot.auth.controller
- com.youlai.boot.system.controller - com.youlai.boot.system.controller
- com.youlai.boot.module.file.controller - com.youlai.boot.file.controller
- com.youlai.boot.module.codegen.controller - com.youlai.boot.codegen.controller
default-flat-param-object: true # 将对象参数扁平化显示在文档中 - com.youlai.boot.message.controller
default-flat-param-object: true
# knife4j 配置 # knife4j 配置
knife4j: knife4j:
enable: true # 是否启用 Knife4j 增强功能 enable: true
production: false # 是否启用生产环境保护true=生产环境隐藏文档false=开发环境可访问) # 生产环境设置为 true 关闭文档
production: false
setting: setting:
language: zh_cn language: zh_cn

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