Merge branch 'master' of https://gitee.com/youlaiorg/youlai-boot
# Conflicts: # pom.xml # src/main/java/com/youlai/boot/common/aspect/LogAspect.java # src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java
|
Before Width: | Height: | Size: 487 KiB After Width: | Height: | Size: 504 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 609 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 115 KiB |
BIN
docs/images/qr/wechat-app.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 43 KiB |
26
pom.xml
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.youlai</groupId>
|
||||
<artifactId>youlai-boot</artifactId>
|
||||
<version>4.3.0</version>
|
||||
<version>4.3.2</version>
|
||||
<description>基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。</description>
|
||||
|
||||
<parent>
|
||||
@@ -26,9 +26,9 @@
|
||||
<druid.version>1.2.24</druid.version>
|
||||
<!-- Spring Boot 4.x 必须使用更新的版本 -->
|
||||
<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>
|
||||
<springdoc.version>2.8.9</springdoc.version>
|
||||
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
|
||||
@@ -54,8 +54,8 @@
|
||||
<ip2region.version>2.7.0</ip2region.version>
|
||||
|
||||
<!-- 阿里云短信 -->
|
||||
<aliyun.java.sdk.core.version>4.7.6</aliyun.java.sdk.core.version>
|
||||
<aliyun.java.sdk.dysmsapi.version>2.2.1</aliyun.java.sdk.dysmsapi.version>
|
||||
<aliyun-sdk-core.version>4.7.6</aliyun-sdk-core.version>
|
||||
<aliyun-sdk-dysmsapi.version>2.2.1</aliyun-sdk-dysmsapi.version>
|
||||
|
||||
<caffeine.version>2.9.3</caffeine.version>
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
|
||||
<weixin-java-miniapp.version>4.8.1.B</weixin-java-miniapp.version>
|
||||
|
||||
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -81,7 +80,7 @@
|
||||
<version>${hutool.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- TransmittableThreadLocal: 支持异步场景的租户上下文传递 -->
|
||||
<!-- TransmittableThreadLocal -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>transmittable-thread-local</artifactId>
|
||||
@@ -183,7 +182,7 @@
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.8.9</version>
|
||||
<version>${springdoc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MapStruct 对象映射 -->
|
||||
@@ -206,7 +205,7 @@
|
||||
<version>${xxl-job.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Excel 工具(EasyExcel-PLus ) -->
|
||||
<!-- Excel 工具(EasyExcel-Plus) -->
|
||||
<dependency>
|
||||
<groupId>cn.idev.excel</groupId>
|
||||
<artifactId>fastexcel</artifactId>
|
||||
@@ -258,13 +257,13 @@
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>aliyun-java-sdk-core</artifactId>
|
||||
<version>${aliyun.java.sdk.core.version}</version>
|
||||
<version>${aliyun-sdk-core.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
|
||||
<version>${aliyun.java.sdk.dysmsapi.version}</version>
|
||||
<version>${aliyun-sdk-dysmsapi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 本地缓存 -->
|
||||
@@ -281,13 +280,6 @@
|
||||
<version>${weixin-java-miniapp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 动态多数据源 -->
|
||||
<!--<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
|
||||
<version>${dynamic-datasource.version}</version>
|
||||
</dependency>-->
|
||||
|
||||
<!--以5.3.0版本为例-->
|
||||
<!-- jiguang-sdk -->
|
||||
<dependency>
|
||||
|
||||
@@ -116,10 +116,11 @@ CREATE TABLE `sys_menu` (
|
||||
`parent_id` bigint NOT NULL COMMENT '父菜单ID',
|
||||
`tree_path` varchar(255) COMMENT '父节点ID路径',
|
||||
`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_path` varchar(128) COMMENT '路由路径(Vue Router 中定义的 URL 路径)',
|
||||
`component` varchar(128) COMMENT '组件路径(组件页面完整路径,相对于 src/views/,缺省后缀 .vue)',
|
||||
`external_url` varchar(512) COMMENT '外链地址',
|
||||
`perm` varchar(128) COMMENT '【按钮】权限标识',
|
||||
`always_show` 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
|
||||
-- ----------------------------
|
||||
-- 顶级目录(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` 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` 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` 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` 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 (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 (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 (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 (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 (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 (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 (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 (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` 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` 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` 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` 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 (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 (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 (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 (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 (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 (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 (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 (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` 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` 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` 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 (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 (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 (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 (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 (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 (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` 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` 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 (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 (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 (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 (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 (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` 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` 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 (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 (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 (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 (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 (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` 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` 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 (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 (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 (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 (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 (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` 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` 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 (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 (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 (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 (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 (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` 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 (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 (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` 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` 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` 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 (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 (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 (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 (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 (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 (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` 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` 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` 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 (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 (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 (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 (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 (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 (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 (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` 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` 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` 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 (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` (`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` (`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` (`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` 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` 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` 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` 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 (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 (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 (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 (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 (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 (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 (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 (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 (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` 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` 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 (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 (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 (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 (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` VALUES (911, 910, '0,8,910', '菜单二级', 'C', NULL, 'multi-level2', '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` 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 (910, 8, '0,8', '一级菜单', 'C', NULL, 'level-one', '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 (911, 910, '0,8,910', '二级菜单', 'C', NULL, 'level-two', '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 (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` (`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` 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 (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 (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
|
||||
@@ -553,16 +554,39 @@ CREATE TABLE `sys_user_notice` (
|
||||
) 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 (2, 2, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (3, 3, 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, 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 (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 (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 (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);
|
||||
|
||||
-- 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
|
||||
-- ----------------------------
|
||||
@@ -584,3 +608,4 @@ CREATE TABLE `sys_user_social` (
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_unionid` (`unionid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户第三方账号绑定表';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
* 应用启动类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 0.0.1
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@EnableScheduling
|
||||
@SpringBootApplication
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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.LogModuleEnum;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.auth.service.AuthService;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.annotation.RateLimit;
|
||||
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -14,7 +15,13 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
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 = "账号密码登录")
|
||||
@PostMapping("/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());
|
||||
return Result.success(authenticationToken);
|
||||
}
|
||||
@@ -59,6 +67,7 @@ public class AuthController {
|
||||
|
||||
@Operation(summary = "发送登录短信验证码")
|
||||
@PostMapping("/sms/code")
|
||||
@RateLimit(limit = 1, window = 60)
|
||||
public Result<Void> sendSmsCode(
|
||||
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile
|
||||
) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package com.youlai.boot.auth.controller;
|
||||
|
||||
import com.youlai.boot.auth.model.WxMaBindMobileReq;
|
||||
import com.youlai.boot.auth.model.WxMaPhoneLoginReq;
|
||||
import com.youlai.boot.auth.model.WxMaLoginResp;
|
||||
import com.youlai.boot.auth.model.form.WxMaBindMobileForm;
|
||||
import com.youlai.boot.auth.model.form.WxMaPhoneLoginForm;
|
||||
import com.youlai.boot.auth.model.vo.WxMaLoginVO;
|
||||
import com.youlai.boot.auth.service.WxMaAuthService;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.annotation.RateLimit;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
@@ -49,11 +50,12 @@ public class WxMaAuthController {
|
||||
@Operation(summary = "静默登录", description = "通过微信 code 登录,已绑定用户直接返回 token,未绑定用户返回 openid 需绑定手机号")
|
||||
@PostMapping("/silent-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")
|
||||
@RequestParam String code
|
||||
) {
|
||||
WxMaLoginResp result = wxMaAuthService.silentLogin(code);
|
||||
WxMaLoginVO result = wxMaAuthService.silentLogin(code);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@@ -67,7 +69,8 @@ public class WxMaAuthController {
|
||||
@Operation(summary = "手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序")
|
||||
@PostMapping("/phone-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());
|
||||
return Result.success(result);
|
||||
}
|
||||
@@ -82,7 +85,8 @@ public class WxMaAuthController {
|
||||
@Operation(summary = "绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录")
|
||||
@PostMapping("/bind-mobile")
|
||||
@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());
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@@ -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 jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 登录请求参数
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
* 登录表单
|
||||
*/
|
||||
@Schema(description = "登录请求参数")
|
||||
@Data
|
||||
public class LoginReq {
|
||||
public class LoginForm {
|
||||
|
||||
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@@ -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 jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 微信小程序绑定手机号请求
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
* 微信小程序绑定手机号表单
|
||||
*/
|
||||
@Schema(description = "微信小程序绑定手机号请求")
|
||||
@Data
|
||||
public class WxMaBindMobileReq {
|
||||
public class WxMaBindMobileForm {
|
||||
|
||||
@NotBlank(message = "openid 不能为空")
|
||||
@Schema(description = "微信用户唯一标识(静默登录返回)", example = "oVBkZ0aYgDMDIywRdgPW8-joxXc4")
|
||||
@Schema(description = "微信用户唯一标识", example = "oVBkZ0aYgDMDIywRdgPW8-joxXc4")
|
||||
private String openid;
|
||||
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@@ -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 jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 微信小程序手机号快捷登录请求
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
* 微信小程序手机号快捷登录表单
|
||||
*/
|
||||
@Schema(description = "微信小程序手机号快捷登录请求")
|
||||
@Data
|
||||
public class WxMaPhoneLoginReq {
|
||||
public class WxMaPhoneLoginForm {
|
||||
|
||||
@NotBlank(message = "微信登录凭证不能为空")
|
||||
@Schema(description = "微信登录凭证(wx.login 获取)", example = "0xxx")
|
||||
@@ -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 lombok.AllArgsConstructor;
|
||||
@@ -8,16 +8,13 @@ import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 微信小程序登录响应
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "微信小程序登录响应")
|
||||
public class WxMaLoginResp {
|
||||
public class WxMaLoginVO {
|
||||
|
||||
@Schema(description = "是否新用户")
|
||||
private Boolean isNewUser;
|
||||
@@ -25,7 +22,7 @@ public class WxMaLoginResp {
|
||||
@Schema(description = "是否需要绑定手机号")
|
||||
private Boolean needBindMobile;
|
||||
|
||||
@Schema(description = "微信openid(绑定手机号时需要)")
|
||||
@Schema(description = "微信openid")
|
||||
private String openid;
|
||||
|
||||
@Schema(description = "访问令牌")
|
||||
@@ -1,6 +1,6 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
@@ -21,7 +21,7 @@ public interface WxMaAuthService {
|
||||
* @param code 微信登录凭证(wx.login 获取)
|
||||
* @return 登录结果(成功返回 token,需绑定返回 openid)
|
||||
*/
|
||||
WxMaLoginResp silentLogin(String code);
|
||||
WxMaLoginVO silentLogin(String code);
|
||||
|
||||
/**
|
||||
* 手机号快捷登录
|
||||
|
||||
@@ -5,7 +5,7 @@ import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
|
||||
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.auth.model.WxMaLoginResp;
|
||||
import com.youlai.boot.auth.model.vo.WxMaLoginVO;
|
||||
import com.youlai.boot.auth.service.WxMaAuthService;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.framework.security.exception.NeedBindMobileException;
|
||||
@@ -54,14 +54,14 @@ public class WxMaAuthServiceImpl implements WxMaAuthService {
|
||||
* 静默登录
|
||||
*/
|
||||
@Override
|
||||
public WxMaLoginResp silentLogin(String code) {
|
||||
public WxMaLoginVO silentLogin(String code) {
|
||||
WxMaAuthenticationToken token = new WxMaAuthenticationToken(code);
|
||||
|
||||
try {
|
||||
Authentication authentication = authenticationManager.authenticate(token);
|
||||
AuthenticationToken authToken = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return WxMaLoginResp.builder()
|
||||
return WxMaLoginVO.builder()
|
||||
.isNewUser(false)
|
||||
.needBindMobile(false)
|
||||
.accessToken(authToken.getAccessToken())
|
||||
@@ -70,7 +70,7 @@ public class WxMaAuthServiceImpl implements WxMaAuthService {
|
||||
.expiresIn(authToken.getExpiresIn())
|
||||
.build();
|
||||
} catch (NeedBindMobileException e) {
|
||||
return WxMaLoginResp.builder()
|
||||
return WxMaLoginVO.builder()
|
||||
.isNewUser(true)
|
||||
.needBindMobile(true)
|
||||
.openid(e.getOpenid())
|
||||
|
||||
@@ -5,7 +5,7 @@ import java.lang.annotation.*;
|
||||
/**
|
||||
* 数据权限注解
|
||||
*
|
||||
* @author zc
|
||||
* @author Ray.Hao
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Documented
|
||||
|
||||
@@ -3,13 +3,17 @@ package com.youlai.boot.common.annotation;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
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
|
||||
* @since 2024/6/25
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
|
||||
@@ -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:";
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
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.JsonInclude;
|
||||
import lombok.Data;
|
||||
@@ -14,8 +17,8 @@ import java.time.LocalDateTime;
|
||||
*
|
||||
* <p>实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等</p>
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2024/6/23
|
||||
* @author Ray.Hao
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@Data
|
||||
public class BaseEntity implements Serializable {
|
||||
|
||||
@@ -7,6 +7,12 @@ import lombok.Data;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 查询参数基类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Data
|
||||
@Schema
|
||||
public class BaseQuery implements Serializable {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.youlai.boot.common.base;
|
||||
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
|
||||
import java.util.EnumSet;
|
||||
@@ -9,8 +8,8 @@ import java.util.Objects;
|
||||
/**
|
||||
* 枚举通用接口
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/3/27 12:06
|
||||
* @author Ray.Hao
|
||||
* @since 4.3.1
|
||||
*/
|
||||
public interface IBaseEnum<T> {
|
||||
|
||||
@@ -37,12 +36,12 @@ public interface IBaseEnum<T> {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文本标签获取值
|
||||
* 根据值获取标签
|
||||
*
|
||||
* @param value
|
||||
* @param clazz
|
||||
* @param <E>
|
||||
* @return
|
||||
* @param value 枚举值
|
||||
* @param clazz 枚举类型
|
||||
* @param <E> 枚举
|
||||
* @return 标签文本
|
||||
*/
|
||||
static <E extends Enum<E> & IBaseEnum> String getLabelByValue(Object value, Class<E> clazz) {
|
||||
Objects.requireNonNull(value);
|
||||
@@ -59,14 +58,13 @@ public interface IBaseEnum<T> {
|
||||
return label;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据文本标签获取值
|
||||
* 根据标签获取值
|
||||
*
|
||||
* @param label
|
||||
* @param clazz
|
||||
* @param <E>
|
||||
* @return
|
||||
* @param label 标签文本
|
||||
* @param clazz 枚举类型
|
||||
* @param <E> 枚举
|
||||
* @return 枚举值
|
||||
*/
|
||||
static <E extends Enum<E> & IBaseEnum> Object getValueByLabel(String label, Class<E> clazz) {
|
||||
Objects.requireNonNull(label);
|
||||
@@ -84,5 +82,4 @@ public interface IBaseEnum<T> {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ package com.youlai.boot.common.constant;
|
||||
* Redis 常量
|
||||
*
|
||||
* @author Theo
|
||||
* @since 2024-7-29 11:46:08
|
||||
* @since 4.3.1
|
||||
*/
|
||||
public interface RedisConstants {
|
||||
|
||||
@@ -12,7 +12,11 @@ public interface RedisConstants {
|
||||
* 限流相关键
|
||||
*/
|
||||
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:{}";
|
||||
// 用户与访问令牌的映射(userId -> accessToken)
|
||||
String USER_ACCESS_TOKEN = "auth:user:access:{}";
|
||||
// 用户与刷新令牌的映射(userId -> refreshToken
|
||||
// 用户与刷新令牌的映射(userId -> refreshToken))
|
||||
String USER_REFRESH_TOKEN = "auth:user:refresh:{}";
|
||||
// 已撤销 Token 的 JTI(单端退出/会话注销):如果 jti 在撤销列表中,则 Token 立即无效
|
||||
String BLACKLIST_TOKEN = "auth:token:blacklist:{}";
|
||||
|
||||
@@ -23,10 +23,4 @@ public interface SystemConstants {
|
||||
*/
|
||||
String ROOT_ROLE_CODE = "ROOT";
|
||||
|
||||
|
||||
/**
|
||||
* 系统配置 IP的QPS限流的KEY
|
||||
*/
|
||||
String SYSTEM_CONFIG_IP_QPS_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT";
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import lombok.Getter;
|
||||
/**
|
||||
* 环境枚举
|
||||
*
|
||||
* @author Ray
|
||||
* @author Ray.Hao
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@Getter
|
||||
|
||||
@@ -6,18 +6,17 @@ import lombok.Getter;
|
||||
/**
|
||||
* 状态枚举
|
||||
*
|
||||
* @author haoxr
|
||||
* @author Ray.Hao
|
||||
* @since 2022/10/14
|
||||
*/
|
||||
@Getter
|
||||
public enum StatusEnum implements IBaseEnum<Integer> {
|
||||
|
||||
ENABLE(1, "启用"),
|
||||
DISABLE (0, "禁用");
|
||||
DISABLE(0, "禁用");
|
||||
|
||||
private final Integer value;
|
||||
|
||||
|
||||
private final String label;
|
||||
|
||||
StatusEnum(Integer value, String label) {
|
||||
|
||||
@@ -7,26 +7,24 @@ import org.slf4j.helpers.MessageFormatter;
|
||||
/**
|
||||
* 自定义业务异常
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2022/7/31
|
||||
* @author Ray.Hao
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
public IResultCode resultCode;
|
||||
private IResultCode resultCode;
|
||||
|
||||
public BusinessException(IResultCode errorCode) {
|
||||
super(errorCode.getMsg());
|
||||
this.resultCode = errorCode;
|
||||
}
|
||||
|
||||
|
||||
public BusinessException(IResultCode errorCode,String message) {
|
||||
public BusinessException(IResultCode errorCode, String message) {
|
||||
super(message);
|
||||
this.resultCode = errorCode;
|
||||
}
|
||||
|
||||
|
||||
public BusinessException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import java.io.Serializable;
|
||||
* 统一响应结构体
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2022/1/30
|
||||
* @since 4.3.1
|
||||
**/
|
||||
@Data
|
||||
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);
|
||||
}
|
||||
|
||||
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) {
|
||||
Result<T> result = new Result<>();
|
||||
result.setCode(code);
|
||||
|
||||
@@ -48,7 +48,7 @@ import java.io.Serializable;
|
||||
* </pre>
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2020/6/23
|
||||
* @since 4.3.1
|
||||
**/
|
||||
public enum ResultCode implements IResultCode, Serializable {
|
||||
|
||||
|
||||
@@ -22,33 +22,16 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Aliyun 对象存储服务类
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(value = "oss.type", havingValue = "aliyun")
|
||||
@ConfigurationProperties(prefix = "oss.aliyun")
|
||||
@RequiredArgsConstructor
|
||||
@Data
|
||||
public class AliyunFileService implements FileService {
|
||||
/**
|
||||
* 服务Endpoint
|
||||
*/
|
||||
public class AliyunFileServiceImpl implements FileService {
|
||||
|
||||
private String endpoint;
|
||||
/**
|
||||
* 访问凭据
|
||||
*/
|
||||
private String accessKeyId;
|
||||
/**
|
||||
* 凭据密钥
|
||||
*/
|
||||
private String accessKeySecret;
|
||||
/**
|
||||
* 存储桶名称
|
||||
*/
|
||||
private String bucketName;
|
||||
|
||||
private OSS aliyunOssClient;
|
||||
@@ -61,27 +44,18 @@ public class AliyunFileService implements FileService {
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public FileInfo uploadFile(MultipartFile file) {
|
||||
|
||||
// 获取文件名称
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
// 生成文件名(日期文件夹)
|
||||
String suffix = FileUtil.getSuffix(originalFilename);
|
||||
String uuid = IdUtil.simpleUUID();
|
||||
String fileName = DateUtil.format(LocalDateTime.now(), "yyyyMMdd") + "/" + uuid + "." + suffix;
|
||||
// try-with-resource 语法糖自动释放流
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
|
||||
// 设置上传文件的元信息,例如Content-Type
|
||||
ObjectMetadata metadata = new ObjectMetadata();
|
||||
metadata.setContentType(file.getContentType());
|
||||
// 创建PutObjectRequest对象,指定Bucket名称、对象名称和输入流
|
||||
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName, inputStream, metadata);
|
||||
// 上传文件
|
||||
aliyunOssClient.putObject(putObjectRequest);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("文件上传失败");
|
||||
}
|
||||
// 获取文件访问路径
|
||||
String fileUrl = "https://" + bucketName + "." + endpoint + "/" + fileName;
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.setName(originalFilename);
|
||||
@@ -92,8 +66,8 @@ public class AliyunFileService implements FileService {
|
||||
@Override
|
||||
public boolean deleteFile(String filePath) {
|
||||
Assert.notBlank(filePath, "删除文件路径不能为空");
|
||||
String fileHost = "https://" + bucketName + "." + endpoint; // 文件主机域名
|
||||
String fileName = filePath.substring(fileHost.length() + 1); // +1 是/占一个字符,截断左闭右开
|
||||
String fileHost = "https://" + bucketName + "." + endpoint;
|
||||
String fileName = filePath.substring(fileHost.length() + 1);
|
||||
aliyunOssClient.deleteObject(bucketName, fileName);
|
||||
return true;
|
||||
}
|
||||
@@ -19,49 +19,30 @@ import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 本地存储服务类
|
||||
*
|
||||
* @author Theo
|
||||
* @since 2024-12-09 17:11
|
||||
*/
|
||||
@Data
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnProperty(value = "oss.type", havingValue = "local")
|
||||
@ConfigurationProperties(prefix = "oss.local")
|
||||
@RequiredArgsConstructor
|
||||
public class LocalFileService implements FileService {
|
||||
public class LocalFileServiceImpl implements FileService {
|
||||
|
||||
@Value("${oss.local.storage-path}")
|
||||
private String storagePath;
|
||||
|
||||
/**
|
||||
* 上传文件方法
|
||||
*
|
||||
* @param file 表单文件对象
|
||||
* @return 文件信息
|
||||
*/
|
||||
@Override
|
||||
public FileInfo uploadFile(MultipartFile file) {
|
||||
// 获取文件名
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
// 获取文件后缀
|
||||
String suffix = FileUtil.getSuffix(originalFilename);
|
||||
// 生成uuid
|
||||
String fileName = IdUtil.simpleUUID()+ "." + suffix;;
|
||||
// 生成文件名(日期文件夹)
|
||||
String folder = DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
|
||||
String filePrefix = storagePath.endsWith(File.separator) ? storagePath : storagePath + File.separator;
|
||||
// try-with-resource 语法糖自动释放流
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
// 上传文件
|
||||
FileUtil.writeFromStream(inputStream, filePrefix + folder + File.separator + fileName);
|
||||
} catch (Exception e) {
|
||||
log.error("文件上传失败", e);
|
||||
throw new RuntimeException("文件上传失败");
|
||||
}
|
||||
// 获取文件访问路径,因为这里是本地存储,所以直接返回文件的相对路径,需要前端自行处理访问前缀
|
||||
String fileUrl = File.separator + folder + File.separator + fileName;
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
fileInfo.setName(originalFilename);
|
||||
@@ -69,24 +50,14 @@ public class LocalFileService implements FileService {
|
||||
return fileInfo;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param filePath 文件完整URL
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
@Override
|
||||
public boolean deleteFile(String filePath) {
|
||||
//判断文件是否为空
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// 判断filepath是否为文件夹
|
||||
if (FileUtil.isDirectory(storagePath + filePath)) {
|
||||
// 禁止删除文件夹
|
||||
return false;
|
||||
}
|
||||
// 删除文件
|
||||
return FileUtil.del(storagePath + filePath);
|
||||
}
|
||||
}
|
||||
@@ -24,79 +24,39 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MinIO 文件上传服务类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2023/6/2
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(value = "oss.type", havingValue = "minio")
|
||||
@ConfigurationProperties(prefix = "oss.minio")
|
||||
@RequiredArgsConstructor
|
||||
@Data
|
||||
@Slf4j
|
||||
public class MinioFileService implements FileService {
|
||||
public class MinioFileServiceImpl implements FileService {
|
||||
|
||||
/**
|
||||
* 服务Endpoint
|
||||
*/
|
||||
private String endpoint;
|
||||
/**
|
||||
* 访问凭据
|
||||
*/
|
||||
private String accessKey;
|
||||
/**
|
||||
* 凭据密钥
|
||||
*/
|
||||
private String secretKey;
|
||||
/**
|
||||
* 存储桶名称
|
||||
*/
|
||||
private String bucketName;
|
||||
/**
|
||||
* 自定义域名
|
||||
*/
|
||||
private String customDomain;
|
||||
|
||||
private MinioClient minioClient;
|
||||
|
||||
// 依赖注入完成之后执行初始化
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
minioClient = MinioClient.builder()
|
||||
.endpoint(endpoint)
|
||||
.credentials(accessKey, secretKey)
|
||||
.build();
|
||||
// 创建存储桶(存储桶不存在)
|
||||
// createBucketIfAbsent(bucketName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
* @param file 表单文件对象
|
||||
* @return 文件信息
|
||||
*/
|
||||
@Override
|
||||
public FileInfo uploadFile(MultipartFile file) {
|
||||
|
||||
// 创建存储桶(存储桶不存在),如果有搭建好的minio服务,建议放在init方法中
|
||||
createBucketIfAbsent(bucketName);
|
||||
|
||||
// 文件原生名称
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
// 文件后缀
|
||||
String suffix = FileUtil.getSuffix(originalFilename);
|
||||
// 文件夹名称
|
||||
String dateFolder = DateUtil.format(LocalDateTime.now(), "yyyyMMdd");
|
||||
// 文件名称
|
||||
String fileName = IdUtil.simpleUUID() + "." + suffix;
|
||||
|
||||
// try-with-resource 语法糖自动释放流
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
// 文件上传
|
||||
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.object(dateFolder + "/"+ fileName)
|
||||
@@ -105,21 +65,16 @@ public class MinioFileService implements FileService {
|
||||
.build();
|
||||
minioClient.putObject(putObjectArgs);
|
||||
|
||||
// 返回文件路径
|
||||
String fileUrl;
|
||||
// 未配置自定义域名
|
||||
if (StrUtil.isBlank(customDomain)) {
|
||||
// 获取文件URL
|
||||
GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.object(dateFolder + "/"+ fileName)
|
||||
.method(Method.GET)
|
||||
.build();
|
||||
|
||||
fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs);
|
||||
fileUrl = fileUrl.substring(0, fileUrl.indexOf("?"));
|
||||
} else {
|
||||
// 配置自定义文件路径域名
|
||||
fileUrl = customDomain + "/"+ bucketName + "/"+ dateFolder + "/"+ fileName;
|
||||
}
|
||||
|
||||
@@ -133,31 +88,18 @@ public class MinioFileService implements FileService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param filePath 文件完整路径
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
@Override
|
||||
public boolean deleteFile(String filePath) {
|
||||
Assert.notBlank(filePath, "删除文件路径不能为空");
|
||||
try {
|
||||
String fileName;
|
||||
if (StrUtil.isNotBlank(customDomain)) {
|
||||
// https://oss.youlai.tech/default/20221120/test.jpg → 20221120/websocket.jpg
|
||||
fileName = filePath.substring(customDomain.length() + 1 + bucketName.length() + 1); // 两个/占了2个字符长度
|
||||
fileName = filePath.substring(customDomain.length() + 1 + bucketName.length() + 1);
|
||||
} else {
|
||||
// http://localhost:9000/default/20221120/test.jpg → 20221120/websocket.jpg
|
||||
fileName = filePath.substring(endpoint.length() + 1 + bucketName.length() + 1);
|
||||
}
|
||||
RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.object(fileName)
|
||||
.build();
|
||||
|
||||
minioClient.removeObject(removeObjectArgs);
|
||||
minioClient.removeObject(RemoveObjectArgs.builder()
|
||||
.bucket(bucketName).object(fileName).build());
|
||||
return true;
|
||||
} catch (Exception 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) {
|
||||
// AWS的S3存储桶策略 JSON 格式 https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/example-bucket-policies.html
|
||||
return "{\"Version\":\"2012-10-17\","
|
||||
+ "\"Statement\":[{\"Effect\":\"Allow\","
|
||||
+ "\"Principal\":{\"AWS\":[\"*\"]},"
|
||||
@@ -185,26 +118,12 @@ public class MinioFileService implements FileService {
|
||||
+ "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建存储桶(存储桶不存在)
|
||||
*
|
||||
* @param bucketName 存储桶名称
|
||||
*/
|
||||
@SneakyThrows
|
||||
private void createBucketIfAbsent(String bucketName) {
|
||||
BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build();
|
||||
if (!minioClient.bucketExists(bucketExistsArgs)) {
|
||||
MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build();
|
||||
|
||||
minioClient.makeBucket(makeBucketArgs);
|
||||
|
||||
// 设置存储桶访问权限为PUBLIC, 如果不配置,则新建的存储桶默认是PRIVATE,则存储桶文件会拒绝访问 Access Denied
|
||||
SetBucketPolicyArgs setBucketPolicyArgs = SetBucketPolicyArgs
|
||||
.builder()
|
||||
.bucket(bucketName)
|
||||
.config(publicBucketPolicy(bucketName))
|
||||
.build();
|
||||
minioClient.setBucketPolicy(setBucketPolicyArgs);
|
||||
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
|
||||
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
|
||||
minioClient.setBucketPolicy(SetBucketPolicyArgs.builder()
|
||||
.bucket(bucketName).config(publicBucketPolicy(bucketName)).build());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,10 +24,12 @@ public class RedisConfig {
|
||||
* 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer
|
||||
*
|
||||
* @param redisConnectionFactory {@link RedisConnectionFactory}
|
||||
* @param jsonMapper Jackson 序列化器(统一使用不写入类型信息的配置)
|
||||
* @return {@link RedisTemplate}
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory,
|
||||
JsonMapper jsonMapper) {
|
||||
|
||||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
@@ -37,10 +39,6 @@ public class RedisConfig {
|
||||
redisTemplate.setHashKeySerializer(RedisSerializer.string());
|
||||
|
||||
// Value 使用自定义 JSON 序列化(不写入类型信息,避免 HashSet 等集合被序列化成带 @class 的结构)
|
||||
JsonMapper jsonMapper = JsonMapper.builder()
|
||||
.disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
.build();
|
||||
|
||||
JacksonJsonRedisSerializer<Object> jsonSerializer = new JacksonJsonRedisSerializer<>(jsonMapper, Object.class);
|
||||
|
||||
redisTemplate.setValueSerializer(jsonSerializer);
|
||||
@@ -50,4 +48,17 @@ public class RedisConfig {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.enums.CaptchaTypeEnum;
|
||||
import com.youlai.boot.framework.captcha.enums.CaptchaTypeEnum;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.captcha.config.CaptchaProperties;
|
||||
import com.youlai.boot.framework.captcha.exception.CaptchaException;
|
||||
|
||||
@@ -9,11 +9,7 @@ import org.springframework.mail.javamail.JavaMailSenderImpl;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* MailConfig 配置类,用于手动配置和注入 JavaMailSender。
|
||||
* 通过读取 MailProperties 类中配置的邮件相关属性来初始化 JavaMailSender。
|
||||
* <p>
|
||||
* 手动注入的原因是为了避免在使用 application-dev.yml 或其他非 application.yml 配置文件时,
|
||||
* IDEA 提示无法找到 JavaMailSender 的 bean。
|
||||
* 邮件发送配置
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2024/8/17
|
||||
@@ -28,11 +24,6 @@ public class MailConfig {
|
||||
this.mailProperties = mailProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并配置 JavaMailSender bean。
|
||||
*
|
||||
* @return 配置好的 JavaMailSender 实例
|
||||
*/
|
||||
@Bean
|
||||
public JavaMailSender javaMailSender() {
|
||||
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
|
||||
|
||||
@@ -4,7 +4,7 @@ import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 邮件配置类,用于接收和存储邮件相关的配置属性。
|
||||
* 邮件配置属性
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2024/8/17
|
||||
@@ -13,75 +13,38 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@Data
|
||||
public class MailProperties {
|
||||
|
||||
/**
|
||||
* 邮件服务器主机名或 IP 地址。
|
||||
* 例如:smtp.example.com
|
||||
*/
|
||||
/** SMTP 服务器地址 */
|
||||
private String host;
|
||||
|
||||
/**
|
||||
* 邮件服务器端口号。
|
||||
* 例如:587
|
||||
*/
|
||||
/** SMTP 端口 */
|
||||
private int port;
|
||||
|
||||
/**
|
||||
* 用于连接邮件服务器的用户名。
|
||||
* 例如:your_email@example.com
|
||||
*/
|
||||
/** 发件人账号 */
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 用于连接邮件服务器的密码。
|
||||
* 该密码应安全存储,不应在代码中硬编码。
|
||||
*/
|
||||
/** 发件人密码 */
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 邮件发送者地址。
|
||||
*/
|
||||
/** 发件人地址 */
|
||||
private String from;
|
||||
|
||||
/**
|
||||
* 邮件服务器的其他属性配置。
|
||||
* 这些配置通常用于进一步定制邮件发送行为。
|
||||
*/
|
||||
/** SMTP 扩展属性 */
|
||||
private Properties properties = new Properties();
|
||||
|
||||
/**
|
||||
* 内部类,用于封装邮件服务器的详细配置。
|
||||
* 包含 SMTP 相关的配置选项。
|
||||
*/
|
||||
@Data
|
||||
public static class Properties {
|
||||
|
||||
/**
|
||||
* SMTP 配置选项类。
|
||||
* 包含认证、加密等与 SMTP 协议相关的配置。
|
||||
*/
|
||||
private Smtp smtp = new Smtp();
|
||||
|
||||
@Data
|
||||
public static class Smtp {
|
||||
|
||||
/**
|
||||
* 是否启用 SMTP 认证。
|
||||
* 如果为 `true`,则需要提供有效的用户名和密码进行认证。
|
||||
*/
|
||||
/** 启用 SMTP 认证 */
|
||||
private boolean auth;
|
||||
|
||||
/**
|
||||
* STARTTLS 加密配置选项。
|
||||
*/
|
||||
private StartTls starttls = new StartTls();
|
||||
|
||||
@Data
|
||||
public static class StartTls {
|
||||
|
||||
/**
|
||||
* 是否启用 STARTTLS 加密。
|
||||
* 如果为 `true`,在发送邮件时将启用 STARTTLS 协议进行加密传输。
|
||||
*/
|
||||
/** 启用 STARTTLS */
|
||||
private boolean enable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,34 +17,22 @@ import java.util.Map;
|
||||
@Data
|
||||
public class AliyunSmsProperties {
|
||||
|
||||
/**
|
||||
* 阿里云账户的Access Key ID,用于API请求认证
|
||||
*/
|
||||
/** Access Key ID */
|
||||
private String accessKeyId;
|
||||
|
||||
/**
|
||||
*阿里云账户的Access Key Secret,用于API请求认证
|
||||
*/
|
||||
/** Access Key Secret */
|
||||
private String accessKeySecret;
|
||||
|
||||
/**
|
||||
* 阿里云短信服务API的域名 eg: dysmsapi.aliyuncs.com
|
||||
*/
|
||||
/** API 域名,如 dysmsapi.aliyuncs.com */
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* 阿里云服务的区域ID,如cn-shanghai
|
||||
*/
|
||||
/** 区域,如 cn-shanghai */
|
||||
private String regionId;
|
||||
|
||||
/**
|
||||
* 短信签名,必须是已经在阿里云短信服务中注册并通过审核的
|
||||
*/
|
||||
/** 短信签名 */
|
||||
private String signName;
|
||||
|
||||
/**
|
||||
* 短信模板集合
|
||||
*/
|
||||
/** 短信模板集合,key 为模板用途(login/register),value 为模板CODE */
|
||||
private Map<String, String> templates;
|
||||
|
||||
}
|
||||
|
||||
@@ -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.impl.WxMaServiceImpl;
|
||||
@@ -17,21 +17,13 @@ import org.springframework.context.annotation.Configuration;
|
||||
@EnableConfigurationProperties(WxMaProperties.class)
|
||||
public class WxMaConfig {
|
||||
|
||||
/**
|
||||
* 微信小程序服务
|
||||
*
|
||||
* @param properties 微信小程序配置属性
|
||||
* @return {@link WxMaService}
|
||||
*/
|
||||
@Bean
|
||||
public WxMaService wxMaService(WxMaProperties properties) {
|
||||
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
|
||||
config.setAppid(properties.getAppid());
|
||||
config.setSecret(properties.getSecret());
|
||||
|
||||
WxMaService service = new WxMaServiceImpl();
|
||||
service.setWxMaConfig(config);
|
||||
return service;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.framework.integration.wxma;
|
||||
package com.youlai.boot.framework.integration.wxma.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -10,14 +10,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@ConfigurationProperties(prefix = "wx.miniapp")
|
||||
public class WxMaProperties {
|
||||
|
||||
/**
|
||||
* 小程序 AppID
|
||||
*/
|
||||
private String appid;
|
||||
|
||||
/**
|
||||
* 小程序 AppSecret
|
||||
*/
|
||||
private String secret;
|
||||
|
||||
}
|
||||
@@ -5,17 +5,17 @@ import com.baomidou.mybatisplus.core.config.GlobalConfig;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
||||
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 org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* mybatis-plus 配置类
|
||||
* MyBatis-Plus 配置类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2022/7/2
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
@@ -38,12 +38,12 @@ public class MybatisConfig {
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动填充数据库创建人、创建时间、更新人、更新时间
|
||||
* 自动填充创建时间和更新时间
|
||||
*/
|
||||
@Bean
|
||||
public GlobalConfig globalConfig() {
|
||||
GlobalConfig globalConfig = new GlobalConfig();
|
||||
globalConfig.setMetaObjectHandler(new MyMetaObjectHandler());
|
||||
globalConfig.setMetaObjectHandler(new AutoFillMetaObjectHandler());
|
||||
return globalConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,28 @@
|
||||
package com.youlai.boot.framework.mybatis.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* mybatis-plus 字段自动填充
|
||||
* <p>
|
||||
* 支持自动填充创建时间、更新时间
|
||||
* </p>
|
||||
* MyBatis-Plus 字段自动填充
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2022/10/14
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class MyMetaObjectHandler implements MetaObjectHandler {
|
||||
public class AutoFillMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
/**
|
||||
* 新增填充创建时间、更新时间
|
||||
*
|
||||
* @param metaObject 元数据
|
||||
*/
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
|
||||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新填充更新时间
|
||||
*
|
||||
* @param metaObject 元数据
|
||||
*/
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,7 +11,8 @@ import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import lombok.SneakyThrows;
|
||||
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.OrExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
@@ -30,8 +31,8 @@ import java.util.List;
|
||||
* <p>
|
||||
* 使用 JSQLParser 构建 SQL 条件,避免字符串拼接,提高代码安全性和可读性。
|
||||
*
|
||||
* @author zc
|
||||
* @since 2021-12-10 13:28
|
||||
* @author Ray.Hao
|
||||
* @since 2021/12/10
|
||||
*/
|
||||
@Slf4j
|
||||
public class MyDataPermissionHandler implements DataPermissionHandler {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package com.youlai.boot.framework.security.config;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import com.youlai.boot.framework.captcha.service.CaptchaService;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
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.TokenAuthenticationFilter;
|
||||
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.token.TokenManager;
|
||||
import com.youlai.boot.framework.security.service.SysUserDetailsService;
|
||||
import com.youlai.boot.system.service.ConfigService;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -37,7 +35,7 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
|
||||
* Spring Security 配置类
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2023/2/17
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@@ -53,7 +51,6 @@ public class SecurityConfig {
|
||||
private final SysUserDetailsService userDetailsService;
|
||||
|
||||
private final CaptchaService captchaService;
|
||||
private final ConfigService configService;
|
||||
private final SecurityProperties securityProperties;
|
||||
|
||||
/**
|
||||
@@ -91,8 +88,6 @@ public class SecurityConfig {
|
||||
.httpBasic(AbstractHttpConfigurer::disable) // 禁用 HTTP Basic 认证,避免弹窗式登录
|
||||
// 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中
|
||||
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
|
||||
// 限流过滤器
|
||||
.addFilterBefore(new RateLimiterFilter(redisTemplate, configService), UsernamePasswordAuthenticationFilter.class)
|
||||
// 验证码校验过滤器
|
||||
.addFilterBefore(new CaptchaValidationFilter(captchaService), UsernamePasswordAuthenticationFilter.class)
|
||||
// 移动设备API签名验证过滤器(仅对 /api/v1/sn/** 路径生效)
|
||||
@@ -158,7 +153,7 @@ public class SecurityConfig {
|
||||
return new ProviderManager(
|
||||
daoAuthenticationProvider,
|
||||
smsAuthenticationProvider,
|
||||
wxMaAuthenticationProvider
|
||||
wxMaAuthenticationProvider
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ package com.youlai.boot.framework.security.exception;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
/**
|
||||
* 验证码校验异常
|
||||
* 短信验证码异常
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2025/3/1
|
||||
*/
|
||||
public class CaptchaValidationException extends AuthenticationException {
|
||||
public CaptchaValidationException(String msg) {
|
||||
public class SmsCaptchaException extends AuthenticationException {
|
||||
public SmsCaptchaException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
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.service.CaptchaService;
|
||||
import jakarta.servlet.FilterChain;
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.youlai.boot.framework.security.filter;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
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 jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.framework.security.handler;
|
||||
|
||||
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.web.access.AccessDeniedHandler;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.framework.security.handler;
|
||||
|
||||
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.InsufficientAuthenticationException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.youlai.boot.framework.security.provider;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.model.UserAuthInfo;
|
||||
@@ -62,11 +62,11 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
|
||||
// 参数校验
|
||||
if (StrUtil.isBlank(mobile)) {
|
||||
log.warn("短信验证码登录失败:手机号为空");
|
||||
throw new CaptchaValidationException("手机号不能为空");
|
||||
throw new SmsCaptchaException("手机号不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(inputVerifyCode)) {
|
||||
log.warn("短信验证码登录失败:验证码为空,手机号={}", mobile);
|
||||
throw new CaptchaValidationException("验证码不能为空");
|
||||
throw new SmsCaptchaException("验证码不能为空");
|
||||
}
|
||||
|
||||
// 根据手机号获取用户信息
|
||||
@@ -89,12 +89,12 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
if (cachedVerifyCode == null) {
|
||||
log.warn("短信验证码登录失败:验证码已过期,手机号={}", mobile);
|
||||
throw new CaptchaValidationException("验证码已过期,请重新获取");
|
||||
throw new SmsCaptchaException("验证码已过期,请重新获取");
|
||||
}
|
||||
|
||||
if (!StrUtil.equals(inputVerifyCode, cachedVerifyCode)) {
|
||||
log.warn("短信验证码登录失败:验证码错误,手机号={}", mobile);
|
||||
throw new CaptchaValidationException("验证码错误");
|
||||
throw new SmsCaptchaException("验证码错误");
|
||||
}
|
||||
|
||||
// 验证成功后删除验证码,防止重复使用
|
||||
|
||||
@@ -12,9 +12,9 @@ import cn.hutool.jwt.JWTUtil;
|
||||
import com.youlai.boot.common.constant.JwtClaimConstants;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.security.config.SecurityProperties;
|
||||
import com.youlai.boot.framework.security.exception.TokenInvalidException;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.RoleDataScope;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -300,7 +300,7 @@ public class JwtTokenManager implements TokenManager {
|
||||
public AuthenticationToken refreshToken(String refreshToken) {
|
||||
boolean isValid = validateRefreshToken(refreshToken);
|
||||
if (!isValid) {
|
||||
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
|
||||
throw new TokenInvalidException(ResultCode.REFRESH_TOKEN_INVALID);
|
||||
}
|
||||
Authentication authentication = parseToken(refreshToken);
|
||||
int accessTokenExpiration = securityProperties.getSession().getAccessTokenTimeToLive();
|
||||
|
||||
@@ -5,9 +5,9 @@ import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.security.config.SecurityProperties;
|
||||
import com.youlai.boot.framework.security.exception.TokenInvalidException;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.UserSession;
|
||||
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.stereotype.Service;
|
||||
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -46,10 +48,14 @@ public class RedisTokenManager implements TokenManager {
|
||||
|
||||
private final SecurityProperties securityProperties;
|
||||
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.redisTemplate = redisTemplate;
|
||||
this.jsonMapper = jsonMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,8 +102,9 @@ public class RedisTokenManager implements TokenManager {
|
||||
*/
|
||||
@Override
|
||||
public Authentication parseToken(String token) {
|
||||
UserSession userSession = (UserSession) redisTemplate.opsForValue().get(formatTokenKey(token));
|
||||
if (userSession == null) return null;
|
||||
Object raw = redisTemplate.opsForValue().get(formatTokenKey(token));
|
||||
if (raw == null) return null;
|
||||
UserSession userSession = jsonMapper.convertValue(raw, UserSession.class);
|
||||
|
||||
// 构建用户权限集合
|
||||
Set<SimpleGrantedAuthority> authorities = null;
|
||||
@@ -144,11 +151,12 @@ public class RedisTokenManager implements TokenManager {
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken refreshToken(String refreshToken) {
|
||||
UserSession userSession = (UserSession) redisTemplate.opsForValue()
|
||||
Object raw = redisTemplate.opsForValue()
|
||||
.get(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken));
|
||||
if (userSession == null) {
|
||||
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
|
||||
if (raw == null) {
|
||||
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()));
|
||||
// 删除旧的访问令牌记录
|
||||
Optional.of(oldAccessTokenValue)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.youlai.boot.framework.web.advice;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import tools.jackson.core.JacksonException;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.security.exception.TokenInvalidException;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
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.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
import tools.jackson.core.JacksonException;
|
||||
|
||||
import java.sql.SQLIntegrityConstraintViolationException;
|
||||
import java.sql.SQLSyntaxErrorException;
|
||||
@@ -33,18 +34,14 @@ import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局系统异常处理器
|
||||
* <p>
|
||||
* 调整异常处理的HTTP状态码,丰富异常处理类型
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理绑定异常
|
||||
* <p>
|
||||
* 当请求参数绑定到对象时发生错误,会抛出 BindException 异常。
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(BindException e) {
|
||||
@@ -53,12 +50,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 @RequestParam 参数校验异常
|
||||
* <p>
|
||||
* 当请求参数在校验过程中发生违反约束条件的异常时(如 @RequestParam 验证不通过),
|
||||
* 会捕获到 ConstraintViolationException 异常。
|
||||
*/
|
||||
@ExceptionHandler(ConstraintViolationException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(ConstraintViolationException e) {
|
||||
@@ -67,12 +58,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.INVALID_USER_INPUT, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理方法参数校验异常
|
||||
* <p>
|
||||
* 当使用 @Valid 或 @Validated 注解对方法参数进行验证时,如果验证失败,
|
||||
* 会抛出 MethodArgumentNotValidException 异常。
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(MethodArgumentNotValidException e) {
|
||||
@@ -81,11 +66,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.INVALID_USER_INPUT, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接口不存在的异常
|
||||
* <p>
|
||||
* 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。
|
||||
*/
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public <T> Result<T> processException(NoHandlerFoundException e) {
|
||||
@@ -93,11 +73,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.INTERFACE_NOT_EXIST);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理缺少请求参数的异常
|
||||
* <p>
|
||||
* 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。
|
||||
*/
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(MissingServletRequestParameterException e) {
|
||||
@@ -105,11 +80,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理方法参数类型不匹配的异常
|
||||
* <p>
|
||||
* 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(MethodArgumentTypeMismatchException e) {
|
||||
@@ -117,11 +87,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Servlet 异常
|
||||
* <p>
|
||||
* 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。
|
||||
*/
|
||||
@ExceptionHandler(ServletException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(ServletException e) {
|
||||
@@ -129,11 +94,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理非法参数异常
|
||||
* <p>
|
||||
* 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。
|
||||
*/
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) {
|
||||
@@ -141,11 +101,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 JSON 处理异常
|
||||
* <p>
|
||||
* 当处理 JSON 数据时发生错误,会抛出 JacksonException 异常。
|
||||
*/
|
||||
@ExceptionHandler(JacksonException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> handleJacksonException(JacksonException e) {
|
||||
@@ -153,11 +108,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求体不可读的异常
|
||||
* <p>
|
||||
* 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(HttpMessageNotReadableException e) {
|
||||
@@ -170,11 +120,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理类型不匹配异常
|
||||
* <p>
|
||||
* 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。
|
||||
*/
|
||||
@ExceptionHandler(TypeMismatchException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> processException(TypeMismatchException e) {
|
||||
@@ -182,11 +127,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SQL 语法错误异常
|
||||
* <p>
|
||||
* 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。
|
||||
*/
|
||||
@ExceptionHandler(BadSqlGrammarException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public <T> Result<T> handleBadSqlGrammarException(BadSqlGrammarException e) {
|
||||
@@ -197,11 +137,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.DATABASE_EXECUTION_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SQL 语法错误异常
|
||||
* <p>
|
||||
* 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。
|
||||
*/
|
||||
@ExceptionHandler(SQLSyntaxErrorException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
|
||||
@@ -209,12 +144,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.DATABASE_EXECUTION_SYNTAX_ERROR);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理 SQL 违反了完整性约束
|
||||
* <p>
|
||||
* 当 SQL 违反了完整性约束时,会抛出 SQLIntegrityConstraintViolationException 异常。
|
||||
*/
|
||||
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public <T> Result<T> handleSQLIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException e) {
|
||||
@@ -222,11 +151,12 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.INTEGRITY_CONSTRAINT_VIOLATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
* <p>
|
||||
* 当业务逻辑发生错误时,会抛出 BusinessException 异常。
|
||||
*/
|
||||
@ExceptionHandler(TokenInvalidException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
public <T> Result<T> handleTokenInvalidException(TokenInvalidException e) {
|
||||
return Result.failed(e.getResultCode());
|
||||
}
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public <T> Result<T> handleBizException(BusinessException e) {
|
||||
@@ -237,15 +167,9 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理所有未捕获的异常
|
||||
* <p>
|
||||
* 当发生未捕获的异常时,会抛出 Exception 异常。
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public <T> Result<T> handleException(Exception e) throws Exception {
|
||||
// 将 Spring Security 异常继续抛出,以便交给自定义处理器处理
|
||||
if (e instanceof AccessDeniedException
|
||||
|| e instanceof AuthenticationException) {
|
||||
throw e;
|
||||
@@ -254,12 +178,6 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(e.getLocalizedMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 传参类型错误时,用于消息转换
|
||||
*
|
||||
* @param throwable 异常
|
||||
* @return 错误信息
|
||||
*/
|
||||
private String convertMessage(Throwable throwable) {
|
||||
String error = throwable.toString();
|
||||
String regulation = "\\[\"(.*?)\"]+";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.crypto.digest.DigestUtil;
|
||||
@@ -10,7 +10,6 @@ import com.youlai.boot.common.annotation.RepeatSubmit;
|
||||
import com.youlai.boot.common.util.IPUtils;
|
||||
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;
|
||||
@@ -28,26 +27,19 @@ import java.util.concurrent.TimeUnit;
|
||||
* 防重复提交切面
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.3.0
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class RepeatSubmitAspect {
|
||||
|
||||
private final RedissonClient redissonClient;
|
||||
|
||||
/**
|
||||
* 防重复提交切点
|
||||
*/
|
||||
@Pointcut("@annotation(repeatSubmit)")
|
||||
public void repeatSubmitPointCut(RepeatSubmit repeatSubmit) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 环绕通知:处理防重复提交逻辑
|
||||
*/
|
||||
@Around(value = "repeatSubmitPointCut(repeatSubmit)", argNames = "pjp,repeatSubmit")
|
||||
public Object handleRepeatSubmit(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
|
||||
String lockKey = buildLockKey();
|
||||
@@ -64,39 +56,29 @@ public class RepeatSubmitAspect {
|
||||
|
||||
/**
|
||||
* 生成防重复提交锁的 key
|
||||
* @return 锁的 key
|
||||
*/
|
||||
private String buildLockKey() {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
|
||||
// 用户唯一标识
|
||||
String userIdentifier = getUserIdentifier(request);
|
||||
// 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法)
|
||||
String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI());
|
||||
return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户唯一标识
|
||||
* 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识
|
||||
* 2. 如果 Token 为空,使用 IP 作为用户唯一标识
|
||||
*
|
||||
* @param request 请求对象
|
||||
* @return 用户唯一标识
|
||||
* 获取用户唯一标识
|
||||
* 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识
|
||||
* 2. 如果 Token 为空,使用 IP 作为用户唯一标识
|
||||
*/
|
||||
private String getUserIdentifier(HttpServletRequest request) {
|
||||
// 用户身份唯一标识
|
||||
String userIdentifier;
|
||||
// 从请求头中获取 Token
|
||||
String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) {
|
||||
String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token
|
||||
userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识
|
||||
String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length());
|
||||
userIdentifier = DigestUtil.sha256Hex(rawToken);
|
||||
} else {
|
||||
userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识
|
||||
userIdentifier = IPUtils.getIpAddr(request);
|
||||
}
|
||||
return userIdentifier;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -8,31 +8,22 @@ import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 在线用户数统计定时任务
|
||||
* <p>
|
||||
* 定时统计并广播当前在线用户数量到所有 SSE 客户端
|
||||
* 在线用户统计
|
||||
* <p>定时统计并广播当前在线用户数量</p>
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class OnlineUserCountJob {
|
||||
public class OnlineUserCountTask {
|
||||
|
||||
private final SseSessionRegistry sessionRegistry;
|
||||
private final SseService sseService;
|
||||
|
||||
/**
|
||||
* 定时统计在线用户数并广播
|
||||
* <p>
|
||||
* 每3分钟执行一次,推送当前在线用户数量
|
||||
*/
|
||||
@Scheduled(cron = "0 */3 * * * ?")
|
||||
public void execute() {
|
||||
int onlineCount = sessionRegistry.getOnlineUserCount();
|
||||
int connectionCount = sessionRegistry.getTotalConnectionCount();
|
||||
|
||||
log.debug("定时统计:在线用户数={}, 总连接数={}", onlineCount, connectionCount);
|
||||
|
||||
// 发送在线用户数量
|
||||
log.debug("在线用户数={} 总连接数={}", onlineCount, connectionCount);
|
||||
sseService.sendOnlineCount();
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,11 @@ package com.youlai.boot.system.controller;
|
||||
|
||||
import cn.idev.excel.EasyExcel;
|
||||
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.RepeatSubmit;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
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.common.model.Option;
|
||||
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.util.ExcelUtils;
|
||||
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.entity.SysUser;
|
||||
import com.youlai.boot.system.model.form.*;
|
||||
import com.youlai.boot.system.model.form.UserProfileForm;
|
||||
import com.youlai.boot.system.model.vo.UserExportVO;
|
||||
import com.youlai.boot.system.model.query.UserQuery;
|
||||
import com.youlai.boot.system.model.vo.CurrentUserVO;
|
||||
import com.youlai.boot.system.model.vo.UserPageVO;
|
||||
@@ -33,7 +37,16 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
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 java.io.File;
|
||||
@@ -47,7 +60,7 @@ import java.util.List;
|
||||
* 用户控制层
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2022/10/16
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@Tag(name = "02.用户接口")
|
||||
@RestController
|
||||
@@ -121,12 +134,9 @@ public class UserController {
|
||||
@Parameter(description = "用户ID") @PathVariable Long userId,
|
||||
@Parameter(description = "用户状态(1:启用;0:禁用)") @RequestParam Integer status
|
||||
) {
|
||||
boolean result = userService.update(new LambdaUpdateWrapper<SysUser>()
|
||||
.eq(SysUser::getId, userId)
|
||||
.set(SysUser::getStatus, status)
|
||||
);
|
||||
boolean result = userService.updateUserStatus(userId, status);
|
||||
// 用户禁用时立即失效其会话
|
||||
if (result && status == 0) {
|
||||
if (result && StatusEnum.DISABLE.getValue().equals(status)) {
|
||||
tokenManager.invalidateUserSessions(userId);
|
||||
}
|
||||
return Result.judge(result);
|
||||
|
||||
@@ -9,6 +9,7 @@ import lombok.Getter;
|
||||
*
|
||||
* C:目录
|
||||
* M:菜单
|
||||
* E:外链
|
||||
* B:按钮
|
||||
*/
|
||||
@Getter
|
||||
@@ -16,6 +17,7 @@ public enum MenuTypeEnum implements IBaseEnum<String> {
|
||||
|
||||
CATALOG("C", "目录"),
|
||||
MENU("M", "菜单"),
|
||||
EXTERNAL("E", "外链"),
|
||||
BUTTON("B", "按钮");
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,7 @@ import lombok.EqualsAndHashCode;
|
||||
* @since 2024-07-29 11:17:26
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(description = "系统配置")
|
||||
@TableName("sys_config")
|
||||
public class Config extends BaseEntity {
|
||||
|
||||
@@ -4,8 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 部门实体对象
|
||||
@@ -14,8 +14,8 @@ import lombok.Setter;
|
||||
* @since 2024/06/23
|
||||
*/
|
||||
@TableName("sys_dept")
|
||||
@Getter
|
||||
@Setter
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class Dept extends BaseEntity {
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@ import lombok.EqualsAndHashCode;
|
||||
* @author Ray.Hao
|
||||
* @since 2022/12/17
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_dict")
|
||||
@Data
|
||||
public class Dict extends BaseEntity {
|
||||
|
||||
@@ -11,7 +11,7 @@ import lombok.EqualsAndHashCode;
|
||||
* @author Ray.Hao
|
||||
* @since 2022/12/17
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_dict_item")
|
||||
@Data
|
||||
public class DictItem extends BaseEntity {
|
||||
|
||||
@@ -2,10 +2,10 @@ package com.youlai.boot.system.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -15,15 +15,9 @@ import java.util.Map;
|
||||
* @since 2023/3/6
|
||||
*/
|
||||
@TableName(value = "sys_menu", autoResultMap = true)
|
||||
@Getter
|
||||
@Setter
|
||||
public class Menu {
|
||||
/**
|
||||
* 菜单ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class Menu extends BaseEntity {
|
||||
/**
|
||||
* 父菜单ID
|
||||
*/
|
||||
@@ -35,7 +29,7 @@ public class Menu {
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 菜单类型(C-目录 M-菜单 B-按钮)
|
||||
* 菜单类型(C-目录 M-菜单 E-外链 B-按钮)
|
||||
*/
|
||||
private String type;
|
||||
|
||||
@@ -54,6 +48,11 @@ public class Menu {
|
||||
*/
|
||||
private String component;
|
||||
|
||||
/**
|
||||
* 外链地址
|
||||
*/
|
||||
private String externalUrl;
|
||||
|
||||
/**
|
||||
* 权限标识
|
||||
*/
|
||||
@@ -100,14 +99,4 @@ public class Menu {
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS, typeHandler = JacksonTypeHandler.class)
|
||||
private Map<String, Object> params;
|
||||
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -16,8 +16,8 @@ import java.time.LocalDateTime;
|
||||
* @author Kylin
|
||||
* @since 2024-08-27 10:31
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_notice")
|
||||
public class Notice extends BaseEntity {
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 角色实体
|
||||
@@ -14,8 +14,8 @@ import lombok.Setter;
|
||||
* @since 2024/6/23
|
||||
*/
|
||||
@TableName("sys_role")
|
||||
@Getter
|
||||
@Setter
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class Role extends BaseEntity {
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,15 +2,15 @@ package com.youlai.boot.system.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 用户实体
|
||||
*/
|
||||
@TableName("sys_user")
|
||||
@Getter
|
||||
@Setter
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SysUser extends BaseEntity {
|
||||
|
||||
/**
|
||||
@@ -24,7 +24,7 @@ public class SysUser extends BaseEntity {
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 性别((1-男 2-女 0-保密)
|
||||
* 性别(1-男 2-女 0-保密)
|
||||
*/
|
||||
private Integer gender;
|
||||
|
||||
@@ -49,7 +49,7 @@ public class SysUser extends BaseEntity {
|
||||
private String mobile;
|
||||
|
||||
/**
|
||||
* 状态((1-正常 0-禁用)
|
||||
* 状态(1-正常 0-禁用)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package com.youlai.boot.system.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -18,37 +16,21 @@ import java.time.LocalDateTime;
|
||||
* @author Kylin
|
||||
* @since 2024-08-28 16:56
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_user_notice")
|
||||
public class UserNotice extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 公共通知id
|
||||
*/
|
||||
/** 公共通知id */
|
||||
private Long noticeId;
|
||||
/**
|
||||
* 用户id
|
||||
*/
|
||||
/** 用户id */
|
||||
private Long userId;
|
||||
/**
|
||||
* 读取状态,0未读,1已读
|
||||
*/
|
||||
/** 读取状态,0未读,1已读 */
|
||||
private Integer isRead;
|
||||
/**
|
||||
* 用户阅读时间
|
||||
*/
|
||||
/** 用户阅读时间 */
|
||||
private LocalDateTime readTime;
|
||||
|
||||
/**
|
||||
* 逻辑删除标识(0-未删除 1-已删除)
|
||||
*/
|
||||
/** 逻辑删除标识(0-未删除 1-已删除) */
|
||||
@TableLogic(value = "0", delval = "1")
|
||||
private Integer isDeleted;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ public class MenuForm {
|
||||
@Schema(description = "菜单名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "菜单类型(C-目录 M-菜单 B-按钮)")
|
||||
@Schema(description = "菜单类型(C-目录 M-菜单 E-外链 B-按钮)")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "路由名称")
|
||||
@@ -38,6 +38,9 @@ public class MenuForm {
|
||||
@Schema(description = "组件路径(vue页面完整路径,省略.vue后缀)")
|
||||
private String component;
|
||||
|
||||
@Schema(description = "外链地址")
|
||||
private String externalUrl;
|
||||
|
||||
@Schema(description = "权限标识")
|
||||
private String perm;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ public class MenuVO {
|
||||
@Schema(description = "菜单名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description="菜单类型(C-目录 M-菜单 B-按钮)")
|
||||
@Schema(description="菜单类型(C-目录 M-菜单 E-外链 B-按钮)")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "路由名称")
|
||||
@@ -31,6 +31,9 @@ public class MenuVO {
|
||||
@Schema(description = "组件路径")
|
||||
private String component;
|
||||
|
||||
@Schema(description = "外链地址")
|
||||
private String externalUrl;
|
||||
|
||||
@Schema(description = "菜单排序(数字越小排名越靠前)")
|
||||
private Integer sort;
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ public class RouteVO {
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private Boolean alwaysShow;
|
||||
|
||||
@Schema(description = "外链地址(内嵌模式用)", example = "https://www.youlai.tech")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private String externalUrl;
|
||||
|
||||
@Schema(description = "路由参数")
|
||||
private Map<String,String> params;
|
||||
}
|
||||
|
||||
@@ -58,11 +58,4 @@ public interface ConfigService extends IService<Config> {
|
||||
*/
|
||||
boolean refreshCache();
|
||||
|
||||
/**
|
||||
* 获取系统配置
|
||||
* @param key 配置键
|
||||
* @return 配置值
|
||||
*/
|
||||
Object getSystemConfig(String key);
|
||||
|
||||
}
|
||||
|
||||
@@ -55,6 +55,15 @@ public interface UserService extends IService<SysUser> {
|
||||
*/
|
||||
boolean updateUser(Long userId, UserForm userForm);
|
||||
|
||||
/**
|
||||
* 修改用户状态
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param status 状态值
|
||||
* @return {@link Boolean} 是否修改成功
|
||||
*/
|
||||
boolean updateUserStatus(Long userId, Integer status);
|
||||
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
|
||||
@@ -142,18 +142,5 @@ public class ConfigServiceImpl extends ServiceImpl<ConfigMapper, Config> impleme
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -227,36 +227,50 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
||||
private RouteVO toRouteVo(Menu menu) {
|
||||
RouteVO routeVo = new RouteVO();
|
||||
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();
|
||||
if (StrUtil.isBlank(routeName)) {
|
||||
// 外链不做驼峰转换,使用唯一占位,避免 http:// 被解析异常
|
||||
routeName = externalLink
|
||||
? "ext-" + menu.getId()
|
||||
: StringUtils.capitalize(StrUtil.toCamelCase(routePath, '-'));
|
||||
if (StrUtil.isBlank(routeName) && !isExternal) {
|
||||
routeName = StringUtils.capitalize(StrUtil.toCamelCase(path, '-'));
|
||||
}
|
||||
// 根据name路由跳转 this.$router.push({name:xxx})
|
||||
routeVo.setName(routeName);
|
||||
|
||||
// 根据path路由跳转 this.$router.push({path:xxx})
|
||||
routeVo.setPath(routePath);
|
||||
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();
|
||||
meta.setTitle(menu.getName());
|
||||
meta.setIcon(menu.getIcon());
|
||||
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)) {
|
||||
meta.setKeepAlive(true);
|
||||
}
|
||||
meta.setAlwaysShow(ObjectUtil.equals(menu.getAlwaysShow(), 1));
|
||||
|
||||
if (isEmbedded && StrUtil.isNotBlank(externalUrl)) {
|
||||
meta.setExternalUrl(externalUrl);
|
||||
}
|
||||
|
||||
Map<String, Object> paramsMap = menu.getParams();
|
||||
if (paramsMap != null && !paramsMap.isEmpty()) {
|
||||
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) {
|
||||
|
||||
String menuType = menuForm.getType();
|
||||
boolean isExternalLink = MenuTypeEnum.MENU.getValue().equals(menuType)
|
||||
&& StrUtil.startWithAny(menuForm.getRoutePath(), "http://", "https://");
|
||||
boolean isExternal = MenuTypeEnum.EXTERNAL.getValue().equals(menuType);
|
||||
boolean isEmbedded = isExternal && "iframe".equals(menuForm.getComponent());
|
||||
|
||||
if (MenuTypeEnum.CATALOG.getValue().equals(menuType)) { // 如果是目录
|
||||
String path = menuForm.getRoutePath();
|
||||
@@ -284,8 +298,8 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
||||
menuForm.setRoutePath("/" + path); // 一级目录需以 / 开头
|
||||
}
|
||||
menuForm.setComponent("Layout");
|
||||
} else if (isExternalLink) {
|
||||
// 外链菜单组件设置为 null,通过 routePath 判断外链
|
||||
} else if (isExternal && !isEmbedded) {
|
||||
// 外链新标签页:component 留空
|
||||
menuForm.setComponent(null);
|
||||
}
|
||||
if (Objects.equals(menuForm.getParentId(), menuForm.getId())) {
|
||||
@@ -302,8 +316,9 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
||||
} else {
|
||||
entity.setParams(null);
|
||||
}
|
||||
// 新增类型为菜单时候 路由名称唯一
|
||||
if (MenuTypeEnum.MENU.getValue().equals(menuType) && !isExternalLink) {
|
||||
// 菜单(M)和内嵌外链(E+iframe)需要路由名称唯一
|
||||
boolean needsRouteName = MenuTypeEnum.MENU.getValue().equals(menuType) || isEmbedded;
|
||||
if (needsRouteName) {
|
||||
Assert.isFalse(this.exists(new LambdaQueryWrapper<Menu>()
|
||||
.eq(Menu::getRouteName, entity.getRouteName())
|
||||
.ne(menuForm.getId() != null, Menu::getId, menuForm.getId())
|
||||
|
||||
@@ -22,13 +22,13 @@ import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import com.youlai.boot.system.converter.UserConverter;
|
||||
import com.youlai.boot.system.enums.DictCodeEnum;
|
||||
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.UserExportVO;
|
||||
import com.youlai.boot.system.model.entity.Dept;
|
||||
import com.youlai.boot.system.model.entity.DictItem;
|
||||
import com.youlai.boot.system.model.entity.Role;
|
||||
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.vo.UserPageVO;
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户选项列表
|
||||
*
|
||||
|
||||
@@ -80,6 +80,13 @@ mybatis-plus:
|
||||
# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
||||
# 接口限流
|
||||
rate-limit:
|
||||
default-limit: 60
|
||||
default-window: 60
|
||||
ip:
|
||||
enabled: false # 开发环境关闭 IP 全局限流
|
||||
|
||||
# 安全配置
|
||||
security:
|
||||
session:
|
||||
@@ -181,14 +188,16 @@ springdoc:
|
||||
packages-to-scan: # 扫描的 Controller 包,限制只生成指定包的接口文档
|
||||
- com.youlai.boot.auth.controller
|
||||
- com.youlai.boot.system.controller
|
||||
- com.youlai.boot.module.file.controller
|
||||
- com.youlai.boot.module.codegen.controller
|
||||
default-flat-param-object: true # 将对象参数扁平化显示在文档中
|
||||
- com.youlai.boot.file.controller
|
||||
- com.youlai.boot.codegen.controller
|
||||
- com.youlai.boot.message.controller
|
||||
default-flat-param-object: true
|
||||
|
||||
# knife4j 配置
|
||||
knife4j:
|
||||
enable: true # 是否启用 Knife4j 增强功能
|
||||
production: false # 是否启用生产环境保护(true=生产环境隐藏文档,false=开发环境可访问)
|
||||
enable: true
|
||||
# 生产环境设置为 true 关闭文档
|
||||
production: false
|
||||
setting:
|
||||
language: zh_cn
|
||||
|
||||
|
||||