diff --git a/docs/images/preview/pc-01.png b/docs/images/preview/pc-01.png index 9a55511b..44ca22c0 100644 Binary files a/docs/images/preview/pc-01.png and b/docs/images/preview/pc-01.png differ diff --git a/docs/images/preview/pc-02.png b/docs/images/preview/pc-02.png index 396b6b99..0fdd0c59 100644 Binary files a/docs/images/preview/pc-02.png and b/docs/images/preview/pc-02.png differ diff --git a/docs/images/preview/pc-03.png b/docs/images/preview/pc-03.png index 8e36c3ec..1505c3ba 100644 Binary files a/docs/images/preview/pc-03.png and b/docs/images/preview/pc-03.png differ diff --git a/docs/images/preview/pc-04.png b/docs/images/preview/pc-04.png index 99e65803..527a16fa 100644 Binary files a/docs/images/preview/pc-04.png and b/docs/images/preview/pc-04.png differ diff --git a/docs/images/preview/pc-05.png b/docs/images/preview/pc-05.png index acade2d2..a1d98ddb 100644 Binary files a/docs/images/preview/pc-05.png and b/docs/images/preview/pc-05.png differ diff --git a/docs/images/preview/pc-06.png b/docs/images/preview/pc-06.png index 9a479f36..e9367977 100644 Binary files a/docs/images/preview/pc-06.png and b/docs/images/preview/pc-06.png differ diff --git a/docs/images/qr/wechat-app.jpg b/docs/images/qr/wechat-app.jpg new file mode 100644 index 00000000..73a907b2 Binary files /dev/null and b/docs/images/qr/wechat-app.jpg differ diff --git a/docs/images/qr/wechat-mp.jpg b/docs/images/qr/wechat-mp.jpg deleted file mode 100644 index 8c06ee37..00000000 Binary files a/docs/images/qr/wechat-mp.jpg and /dev/null differ diff --git a/pom.xml b/pom.xml index 9ef8272c..da445d70 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.youlai youlai-boot - 4.3.0 + 4.3.2 基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。 @@ -26,9 +26,9 @@ 1.2.24 3.5.15 - 4.3.1 4.5.0 + 2.8.9 1.6.3 0.2.0 @@ -54,8 +54,8 @@ 2.7.0 - 4.7.6 - 2.2.1 + 4.7.6 + 2.2.1 2.9.3 @@ -64,7 +64,6 @@ 4.8.1.B - @@ -81,7 +80,7 @@ ${hutool.version} - + com.alibaba transmittable-thread-local @@ -183,7 +182,7 @@ org.springdoc springdoc-openapi-starter-webmvc-ui - 2.8.9 + ${springdoc.version} @@ -206,7 +205,7 @@ ${xxl-job.version} - + cn.idev.excel fastexcel @@ -258,13 +257,13 @@ com.aliyun aliyun-java-sdk-core - ${aliyun.java.sdk.core.version} + ${aliyun-sdk-core.version} com.aliyun aliyun-java-sdk-dysmsapi - ${aliyun.java.sdk.dysmsapi.version} + ${aliyun-sdk-dysmsapi.version} @@ -281,13 +280,6 @@ ${weixin-java-miniapp.version} - - - diff --git a/sql/youlai-admin.sql b/sql/youlai-admin.sql index 2bf42096..84ca0a63 100644 --- a/sql/youlai-admin.sql +++ b/sql/youlai-admin.sql @@ -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='用户第三方账号绑定表'; + diff --git a/src/main/java/com/youlai/boot/YouLaiBootApplication.java b/src/main/java/com/youlai/boot/YouLaiBootApplication.java index a40fcaaa..1b2761fd 100644 --- a/src/main/java/com/youlai/boot/YouLaiBootApplication.java +++ b/src/main/java/com/youlai/boot/YouLaiBootApplication.java @@ -8,7 +8,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; * 应用启动类 * * @author Ray.Hao - * @since 0.0.1 + * @since 3.0.0 */ @EnableScheduling @SpringBootApplication diff --git a/src/main/java/com/youlai/boot/auth/controller/AuthController.java b/src/main/java/com/youlai/boot/auth/controller/AuthController.java index 7a7975cf..f3884f0e 100644 --- a/src/main/java/com/youlai/boot/auth/controller/AuthController.java +++ b/src/main/java/com/youlai/boot/auth/controller/AuthController.java @@ -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 login(@RequestBody @Valid LoginReq request) { + @RateLimit + public Result 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 sendSmsCode( @Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile ) { diff --git a/src/main/java/com/youlai/boot/auth/controller/WxMaAuthController.java b/src/main/java/com/youlai/boot/auth/controller/WxMaAuthController.java index 60bc3eaa..760295fe 100644 --- a/src/main/java/com/youlai/boot/auth/controller/WxMaAuthController.java +++ b/src/main/java/com/youlai/boot/auth/controller/WxMaAuthController.java @@ -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 silentLogin( + @RateLimit + public Result 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 phoneLogin(@Valid @RequestBody WxMaPhoneLoginReq req) { + @RateLimit + public Result 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 bindMobile(@Valid @RequestBody WxMaBindMobileReq req) { + @RateLimit + public Result bindMobile(@Valid @RequestBody WxMaBindMobileForm req) { AuthenticationToken result = wxMaAuthService.bindMobile(req.getOpenid(), req.getMobile(), req.getSmsCode()); return Result.success(result); } diff --git a/src/main/java/com/youlai/boot/auth/model/LoginReq.java b/src/main/java/com/youlai/boot/auth/model/form/LoginForm.java similarity index 85% rename from src/main/java/com/youlai/boot/auth/model/LoginReq.java rename to src/main/java/com/youlai/boot/auth/model/form/LoginForm.java index 7faaa424..a4f7d752 100644 --- a/src/main/java/com/youlai/boot/auth/model/LoginReq.java +++ b/src/main/java/com/youlai/boot/auth/model/form/LoginForm.java @@ -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 = "用户名不能为空") @@ -28,4 +24,4 @@ public class LoginReq { @Schema(description = "验证码", example = "123456") private String captchaCode; -} \ No newline at end of file +} diff --git a/src/main/java/com/youlai/boot/auth/model/WxMaBindMobileReq.java b/src/main/java/com/youlai/boot/auth/model/form/WxMaBindMobileForm.java similarity index 67% rename from src/main/java/com/youlai/boot/auth/model/WxMaBindMobileReq.java rename to src/main/java/com/youlai/boot/auth/model/form/WxMaBindMobileForm.java index cbd3d729..118abb39 100644 --- a/src/main/java/com/youlai/boot/auth/model/WxMaBindMobileReq.java +++ b/src/main/java/com/youlai/boot/auth/model/form/WxMaBindMobileForm.java @@ -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 = "手机号不能为空") diff --git a/src/main/java/com/youlai/boot/auth/model/WxMaPhoneLoginReq.java b/src/main/java/com/youlai/boot/auth/model/form/WxMaPhoneLoginForm.java similarity index 78% rename from src/main/java/com/youlai/boot/auth/model/WxMaPhoneLoginReq.java rename to src/main/java/com/youlai/boot/auth/model/form/WxMaPhoneLoginForm.java index cf3ad390..731adb1c 100644 --- a/src/main/java/com/youlai/boot/auth/model/WxMaPhoneLoginReq.java +++ b/src/main/java/com/youlai/boot/auth/model/form/WxMaPhoneLoginForm.java @@ -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") diff --git a/src/main/java/com/youlai/boot/auth/model/WxMaLoginResp.java b/src/main/java/com/youlai/boot/auth/model/vo/WxMaLoginVO.java similarity index 82% rename from src/main/java/com/youlai/boot/auth/model/WxMaLoginResp.java rename to src/main/java/com/youlai/boot/auth/model/vo/WxMaLoginVO.java index 30e45ad5..9616a755 100644 --- a/src/main/java/com/youlai/boot/auth/model/WxMaLoginResp.java +++ b/src/main/java/com/youlai/boot/auth/model/vo/WxMaLoginVO.java @@ -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 = "访问令牌") @@ -39,4 +36,4 @@ public class WxMaLoginResp { @Schema(description = "过期时间(秒)") private Integer expiresIn; -} \ No newline at end of file +} diff --git a/src/main/java/com/youlai/boot/auth/service/WxMaAuthService.java b/src/main/java/com/youlai/boot/auth/service/WxMaAuthService.java index 4e8db970..2ead8689 100644 --- a/src/main/java/com/youlai/boot/auth/service/WxMaAuthService.java +++ b/src/main/java/com/youlai/boot/auth/service/WxMaAuthService.java @@ -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); /** * 手机号快捷登录 diff --git a/src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java b/src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java index 02d85bbf..9a6e0aa6 100644 --- a/src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java +++ b/src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java @@ -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()) diff --git a/src/main/java/com/youlai/boot/common/annotation/DataPermission.java b/src/main/java/com/youlai/boot/common/annotation/DataPermission.java index 10f0d00a..f63832c8 100644 --- a/src/main/java/com/youlai/boot/common/annotation/DataPermission.java +++ b/src/main/java/com/youlai/boot/common/annotation/DataPermission.java @@ -5,7 +5,7 @@ import java.lang.annotation.*; /** * 数据权限注解 * - * @author zc + * @author Ray.Hao * @since 2.0.0 */ @Documented diff --git a/src/main/java/com/youlai/boot/common/annotation/Log.java b/src/main/java/com/youlai/boot/common/annotation/Log.java index a9caa742..c02d5608 100644 --- a/src/main/java/com/youlai/boot/common/annotation/Log.java +++ b/src/main/java/com/youlai/boot/common/annotation/Log.java @@ -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) diff --git a/src/main/java/com/youlai/boot/common/annotation/RateLimit.java b/src/main/java/com/youlai/boot/common/annotation/RateLimit.java new file mode 100644 index 00000000..f53893bc --- /dev/null +++ b/src/main/java/com/youlai/boot/common/annotation/RateLimit.java @@ -0,0 +1,25 @@ +package com.youlai.boot.common.annotation; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * 接口限流 + *

标注在 Controller 方法上,基于 Redis 计数窗口实现

+ * + * @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:"; +} diff --git a/src/main/java/com/youlai/boot/common/aspect/LogAspect.java b/src/main/java/com/youlai/boot/common/aspect/LogAspect.java deleted file mode 100644 index 81d31082..00000000 --- a/src/main/java/com/youlai/boot/common/aspect/LogAspect.java +++ /dev/null @@ -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()); - } - } -} diff --git a/src/main/java/com/youlai/boot/common/base/BaseEntity.java b/src/main/java/com/youlai/boot/common/base/BaseEntity.java index 9c6a2812..70cd1a92 100644 --- a/src/main/java/com/youlai/boot/common/base/BaseEntity.java +++ b/src/main/java/com/youlai/boot/common/base/BaseEntity.java @@ -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; * *

实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等

* - * @author Ray - * @since 2024/6/23 + * @author Ray.Hao + * @since 4.3.1 */ @Data public class BaseEntity implements Serializable { diff --git a/src/main/java/com/youlai/boot/common/base/BaseQuery.java b/src/main/java/com/youlai/boot/common/base/BaseQuery.java index 026d9673..7548937d 100644 --- a/src/main/java/com/youlai/boot/common/base/BaseQuery.java +++ b/src/main/java/com/youlai/boot/common/base/BaseQuery.java @@ -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 { diff --git a/src/main/java/com/youlai/boot/common/base/IBaseEnum.java b/src/main/java/com/youlai/boot/common/base/IBaseEnum.java index 0a44cd81..facc8635 100644 --- a/src/main/java/com/youlai/boot/common/base/IBaseEnum.java +++ b/src/main/java/com/youlai/boot/common/base/IBaseEnum.java @@ -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 { @@ -37,12 +36,12 @@ public interface IBaseEnum { } /** - * 根据文本标签获取值 + * 根据值获取标签 * - * @param value - * @param clazz - * @param - * @return + * @param value 枚举值 + * @param clazz 枚举类型 + * @param 枚举 + * @return 标签文本 */ static & IBaseEnum> String getLabelByValue(Object value, Class clazz) { Objects.requireNonNull(value); @@ -59,14 +58,13 @@ public interface IBaseEnum { return label; } - /** - * 根据文本标签获取值 + * 根据标签获取值 * - * @param label - * @param clazz - * @param - * @return + * @param label 标签文本 + * @param clazz 枚举类型 + * @param 枚举 + * @return 枚举值 */ static & IBaseEnum> Object getValueByLabel(String label, Class clazz) { Objects.requireNonNull(label); @@ -84,5 +82,4 @@ public interface IBaseEnum { return value; } - } diff --git a/src/main/java/com/youlai/boot/common/constant/RedisConstants.java b/src/main/java/com/youlai/boot/common/constant/RedisConstants.java index b54c0455..035d6792 100644 --- a/src/main/java/com/youlai/boot/common/constant/RedisConstants.java +++ b/src/main/java/com/youlai/boot/common/constant/RedisConstants.java @@ -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:{}"; diff --git a/src/main/java/com/youlai/boot/common/constant/SystemConstants.java b/src/main/java/com/youlai/boot/common/constant/SystemConstants.java index 43489745..9933de48 100644 --- a/src/main/java/com/youlai/boot/common/constant/SystemConstants.java +++ b/src/main/java/com/youlai/boot/common/constant/SystemConstants.java @@ -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"; - } diff --git a/src/main/java/com/youlai/boot/common/enums/CaptchaTypeEnum.java b/src/main/java/com/youlai/boot/common/enums/CaptchaTypeEnum.java deleted file mode 100644 index 8d3fe10e..00000000 --- a/src/main/java/com/youlai/boot/common/enums/CaptchaTypeEnum.java +++ /dev/null @@ -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 -} diff --git a/src/main/java/com/youlai/boot/common/enums/EnvEnum.java b/src/main/java/com/youlai/boot/common/enums/EnvEnum.java index 9f502fab..bfc1b8a5 100644 --- a/src/main/java/com/youlai/boot/common/enums/EnvEnum.java +++ b/src/main/java/com/youlai/boot/common/enums/EnvEnum.java @@ -6,7 +6,7 @@ import lombok.Getter; /** * 环境枚举 * - * @author Ray + * @author Ray.Hao * @since 4.0.0 */ @Getter diff --git a/src/main/java/com/youlai/boot/common/enums/StatusEnum.java b/src/main/java/com/youlai/boot/common/enums/StatusEnum.java index 229086fa..7650dd02 100644 --- a/src/main/java/com/youlai/boot/common/enums/StatusEnum.java +++ b/src/main/java/com/youlai/boot/common/enums/StatusEnum.java @@ -6,18 +6,17 @@ import lombok.Getter; /** * 状态枚举 * - * @author haoxr + * @author Ray.Hao * @since 2022/10/14 */ @Getter public enum StatusEnum implements IBaseEnum { ENABLE(1, "启用"), - DISABLE (0, "禁用"); + DISABLE(0, "禁用"); private final Integer value; - private final String label; StatusEnum(Integer value, String label) { diff --git a/src/main/java/com/youlai/boot/common/exception/BusinessException.java b/src/main/java/com/youlai/boot/common/exception/BusinessException.java index 06467091..35286cea 100644 --- a/src/main/java/com/youlai/boot/common/exception/BusinessException.java +++ b/src/main/java/com/youlai/boot/common/exception/BusinessException.java @@ -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); } diff --git a/src/main/java/com/youlai/boot/common/result/ResponseWriter.java b/src/main/java/com/youlai/boot/common/result/ResponseWriter.java deleted file mode 100644 index 9b033990..00000000 --- a/src/main/java/com/youlai/boot/common/result/ResponseWriter.java +++ /dev/null @@ -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; - -/** - * 响应写入器 - *

- * 用于在过滤器、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(); - }; - } -} diff --git a/src/main/java/com/youlai/boot/common/result/Result.java b/src/main/java/com/youlai/boot/common/result/Result.java index 94153792..154b07f0 100644 --- a/src/main/java/com/youlai/boot/common/result/Result.java +++ b/src/main/java/com/youlai/boot/common/result/Result.java @@ -9,7 +9,7 @@ import java.io.Serializable; * 统一响应结构体 * * @author Ray.Hao - * @since 2022/1/30 + * @since 4.3.1 **/ @Data public class Result implements Serializable { @@ -64,10 +64,6 @@ public class Result implements Serializable { return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), data); } - private static Result result(IResultCode resultCode, T data) { - return result(resultCode.getCode(), resultCode.getMsg(), data); - } - private static Result result(String code, String msg, T data) { Result result = new Result<>(); result.setCode(code); diff --git a/src/main/java/com/youlai/boot/common/result/ResultCode.java b/src/main/java/com/youlai/boot/common/result/ResultCode.java index 0754f44c..af2ebb8a 100644 --- a/src/main/java/com/youlai/boot/common/result/ResultCode.java +++ b/src/main/java/com/youlai/boot/common/result/ResultCode.java @@ -48,7 +48,7 @@ import java.io.Serializable; * * * @author Ray.Hao - * @since 2020/6/23 + * @since 4.3.1 **/ public enum ResultCode implements IResultCode, Serializable { diff --git a/src/main/java/com/youlai/boot/file/config/AliyunOssProperties.java b/src/main/java/com/youlai/boot/file/config/AliyunOssProperties.java new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/youlai/boot/file/config/LocalFileProperties.java b/src/main/java/com/youlai/boot/file/config/LocalFileProperties.java new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/youlai/boot/file/config/MinioProperties.java b/src/main/java/com/youlai/boot/file/config/MinioProperties.java new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/youlai/boot/file/service/impl/AliyunFileService.java b/src/main/java/com/youlai/boot/file/service/impl/AliyunFileServiceImpl.java similarity index 79% rename from src/main/java/com/youlai/boot/file/service/impl/AliyunFileService.java rename to src/main/java/com/youlai/boot/file/service/impl/AliyunFileServiceImpl.java index 3d699ff3..55b6ee90 100644 --- a/src/main/java/com/youlai/boot/file/service/impl/AliyunFileService.java +++ b/src/main/java/com/youlai/boot/file/service/impl/AliyunFileServiceImpl.java @@ -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; } diff --git a/src/main/java/com/youlai/boot/file/service/impl/LocalFileService.java b/src/main/java/com/youlai/boot/file/service/impl/LocalFileServiceImpl.java similarity index 73% rename from src/main/java/com/youlai/boot/file/service/impl/LocalFileService.java rename to src/main/java/com/youlai/boot/file/service/impl/LocalFileServiceImpl.java index c9239dbb..555408e6 100644 --- a/src/main/java/com/youlai/boot/file/service/impl/LocalFileService.java +++ b/src/main/java/com/youlai/boot/file/service/impl/LocalFileServiceImpl.java @@ -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); } } diff --git a/src/main/java/com/youlai/boot/file/service/impl/MinioFileService.java b/src/main/java/com/youlai/boot/file/service/impl/MinioFileServiceImpl.java similarity index 62% rename from src/main/java/com/youlai/boot/file/service/impl/MinioFileService.java rename to src/main/java/com/youlai/boot/file/service/impl/MinioFileServiceImpl.java index 03e5a32b..3a0d6caa 100644 --- a/src/main/java/com/youlai/boot/file/service/impl/MinioFileService.java +++ b/src/main/java/com/youlai/boot/file/service/impl/MinioFileServiceImpl.java @@ -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()); } } } diff --git a/src/main/java/com/youlai/boot/framework/cache/RedisConfig.java b/src/main/java/com/youlai/boot/framework/cache/RedisConfig.java index da7eda28..610380d9 100644 --- a/src/main/java/com/youlai/boot/framework/cache/RedisConfig.java +++ b/src/main/java/com/youlai/boot/framework/cache/RedisConfig.java @@ -24,10 +24,12 @@ public class RedisConfig { * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer * * @param redisConnectionFactory {@link RedisConnectionFactory} + * @param jsonMapper Jackson 序列化器(统一使用不写入类型信息的配置) * @return {@link RedisTemplate} */ @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory, + JsonMapper jsonMapper) { RedisTemplate 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 jsonSerializer = new JacksonJsonRedisSerializer<>(jsonMapper, Object.class); redisTemplate.setValueSerializer(jsonSerializer); @@ -50,4 +48,17 @@ public class RedisConfig { return redisTemplate; } + /** + * 统一的 JsonMapper Bean + *

+ * 禁止将日期序列化为时间戳;不写入类型信息,保证 Redis 存储的 JSON 纯净可读。 + * 需要反序列化到特定类型时,调用方应使用 {@link JsonMapper#convertValue(Object, Class)} 显式转换。 + */ + @Bean + public JsonMapper jsonMapper() { + return JsonMapper.builder() + .disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + } + } diff --git a/src/main/java/com/youlai/boot/framework/captcha/enums/CaptchaTypeEnum.java b/src/main/java/com/youlai/boot/framework/captcha/enums/CaptchaTypeEnum.java new file mode 100644 index 00000000..5a603983 --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/captcha/enums/CaptchaTypeEnum.java @@ -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 +} diff --git a/src/main/java/com/youlai/boot/framework/captcha/service/CaptchaService.java b/src/main/java/com/youlai/boot/framework/captcha/service/CaptchaService.java index 32ff0416..6e9d9859 100644 --- a/src/main/java/com/youlai/boot/framework/captcha/service/CaptchaService.java +++ b/src/main/java/com/youlai/boot/framework/captcha/service/CaptchaService.java @@ -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; diff --git a/src/main/java/com/youlai/boot/framework/integration/mail/config/MailConfig.java b/src/main/java/com/youlai/boot/framework/integration/mail/config/MailConfig.java index 687d2751..cebcd62d 100644 --- a/src/main/java/com/youlai/boot/framework/integration/mail/config/MailConfig.java +++ b/src/main/java/com/youlai/boot/framework/integration/mail/config/MailConfig.java @@ -9,11 +9,7 @@ import org.springframework.mail.javamail.JavaMailSenderImpl; import java.util.Properties; /** - * MailConfig 配置类,用于手动配置和注入 JavaMailSender。 - * 通过读取 MailProperties 类中配置的邮件相关属性来初始化 JavaMailSender。 - *

- * 手动注入的原因是为了避免在使用 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(); diff --git a/src/main/java/com/youlai/boot/framework/integration/mail/config/MailProperties.java b/src/main/java/com/youlai/boot/framework/integration/mail/config/MailProperties.java index b8588b45..27e1ef64 100644 --- a/src/main/java/com/youlai/boot/framework/integration/mail/config/MailProperties.java +++ b/src/main/java/com/youlai/boot/framework/integration/mail/config/MailProperties.java @@ -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; } } diff --git a/src/main/java/com/youlai/boot/framework/integration/sms/config/AliyunSmsProperties.java b/src/main/java/com/youlai/boot/framework/integration/sms/config/AliyunSmsProperties.java index 5bb590c9..ad2b3426 100644 --- a/src/main/java/com/youlai/boot/framework/integration/sms/config/AliyunSmsProperties.java +++ b/src/main/java/com/youlai/boot/framework/integration/sms/config/AliyunSmsProperties.java @@ -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 templates; } diff --git a/src/main/java/com/youlai/boot/framework/integration/wxma/WxMaConfig.java b/src/main/java/com/youlai/boot/framework/integration/wxma/config/WxMaConfig.java similarity index 82% rename from src/main/java/com/youlai/boot/framework/integration/wxma/WxMaConfig.java rename to src/main/java/com/youlai/boot/framework/integration/wxma/config/WxMaConfig.java index b7062694..93df531b 100644 --- a/src/main/java/com/youlai/boot/framework/integration/wxma/WxMaConfig.java +++ b/src/main/java/com/youlai/boot/framework/integration/wxma/config/WxMaConfig.java @@ -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; } - } diff --git a/src/main/java/com/youlai/boot/framework/integration/wxma/WxMaProperties.java b/src/main/java/com/youlai/boot/framework/integration/wxma/config/WxMaProperties.java similarity index 67% rename from src/main/java/com/youlai/boot/framework/integration/wxma/WxMaProperties.java rename to src/main/java/com/youlai/boot/framework/integration/wxma/config/WxMaProperties.java index 4ed4f468..3c7626a1 100644 --- a/src/main/java/com/youlai/boot/framework/integration/wxma/WxMaProperties.java +++ b/src/main/java/com/youlai/boot/framework/integration/wxma/config/WxMaProperties.java @@ -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; - } diff --git a/src/main/java/com/youlai/boot/framework/job/handler/XxlJobSampleHandler.java b/src/main/java/com/youlai/boot/framework/job/handler/XxlJobSampleHandler.java new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/youlai/boot/framework/mybatis/config/MybatisConfig.java b/src/main/java/com/youlai/boot/framework/mybatis/config/MybatisConfig.java index 76ff4115..b06bb4cb 100644 --- a/src/main/java/com/youlai/boot/framework/mybatis/config/MybatisConfig.java +++ b/src/main/java/com/youlai/boot/framework/mybatis/config/MybatisConfig.java @@ -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; } diff --git a/src/main/java/com/youlai/boot/framework/mybatis/exception/DataPermissionException.java b/src/main/java/com/youlai/boot/framework/mybatis/exception/DataPermissionException.java new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/youlai/boot/framework/mybatis/handler/MyMetaObjectHandler.java b/src/main/java/com/youlai/boot/framework/mybatis/handler/AutoFillMetaObjectHandler.java similarity index 62% rename from src/main/java/com/youlai/boot/framework/mybatis/handler/MyMetaObjectHandler.java rename to src/main/java/com/youlai/boot/framework/mybatis/handler/AutoFillMetaObjectHandler.java index 75b6f41f..f5e9d893 100644 --- a/src/main/java/com/youlai/boot/framework/mybatis/handler/MyMetaObjectHandler.java +++ b/src/main/java/com/youlai/boot/framework/mybatis/handler/AutoFillMetaObjectHandler.java @@ -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 字段自动填充 - *

- * 支持自动填充创建时间、更新时间 - *

+ * 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); } - } diff --git a/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java b/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java index d2bd122e..7dd4e003 100644 --- a/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java +++ b/src/main/java/com/youlai/boot/framework/mybatis/interceptor/MyDataPermissionHandler.java @@ -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; *

* 使用 JSQLParser 构建 SQL 条件,避免字符串拼接,提高代码安全性和可读性。 * - * @author zc - * @since 2021-12-10 13:28 + * @author Ray.Hao + * @since 2021/12/10 */ @Slf4j public class MyDataPermissionHandler implements DataPermissionHandler { diff --git a/src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java b/src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java index 5df0d6ad..da39053e 100644 --- a/src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java +++ b/src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java @@ -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 ); } diff --git a/src/main/java/com/youlai/boot/framework/security/exception/CaptchaValidationException.java b/src/main/java/com/youlai/boot/framework/security/exception/SmsCaptchaException.java similarity index 56% rename from src/main/java/com/youlai/boot/framework/security/exception/CaptchaValidationException.java rename to src/main/java/com/youlai/boot/framework/security/exception/SmsCaptchaException.java index b7c68cc4..8fca2cd9 100644 --- a/src/main/java/com/youlai/boot/framework/security/exception/CaptchaValidationException.java +++ b/src/main/java/com/youlai/boot/framework/security/exception/SmsCaptchaException.java @@ -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); } -} \ No newline at end of file +} diff --git a/src/main/java/com/youlai/boot/framework/security/exception/TokenInvalidException.java b/src/main/java/com/youlai/boot/framework/security/exception/TokenInvalidException.java new file mode 100644 index 00000000..a4bb93c2 --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/security/exception/TokenInvalidException.java @@ -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; + } +} diff --git a/src/main/java/com/youlai/boot/framework/security/filter/CaptchaValidationFilter.java b/src/main/java/com/youlai/boot/framework/security/filter/CaptchaValidationFilter.java index 4f1d6fcb..30504bc2 100644 --- a/src/main/java/com/youlai/boot/framework/security/filter/CaptchaValidationFilter.java +++ b/src/main/java/com/youlai/boot/framework/security/filter/CaptchaValidationFilter.java @@ -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; diff --git a/src/main/java/com/youlai/boot/framework/security/filter/TokenAuthenticationFilter.java b/src/main/java/com/youlai/boot/framework/security/filter/TokenAuthenticationFilter.java index d2514c12..4aad3db0 100644 --- a/src/main/java/com/youlai/boot/framework/security/filter/TokenAuthenticationFilter.java +++ b/src/main/java/com/youlai/boot/framework/security/filter/TokenAuthenticationFilter.java @@ -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; diff --git a/src/main/java/com/youlai/boot/framework/security/handler/MyAccessDeniedHandler.java b/src/main/java/com/youlai/boot/framework/security/handler/MyAccessDeniedHandler.java index 712cc0a4..b4b28ab4 100644 --- a/src/main/java/com/youlai/boot/framework/security/handler/MyAccessDeniedHandler.java +++ b/src/main/java/com/youlai/boot/framework/security/handler/MyAccessDeniedHandler.java @@ -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; diff --git a/src/main/java/com/youlai/boot/framework/security/handler/MyAuthenticationEntryPoint.java b/src/main/java/com/youlai/boot/framework/security/handler/MyAuthenticationEntryPoint.java index b5110142..4f4257c1 100644 --- a/src/main/java/com/youlai/boot/framework/security/handler/MyAuthenticationEntryPoint.java +++ b/src/main/java/com/youlai/boot/framework/security/handler/MyAuthenticationEntryPoint.java @@ -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; diff --git a/src/main/java/com/youlai/boot/framework/security/model/WxMaBindInfo.java b/src/main/java/com/youlai/boot/framework/security/model/WxMaBindInfo.java new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/youlai/boot/framework/security/provider/SmsAuthenticationProvider.java b/src/main/java/com/youlai/boot/framework/security/provider/SmsAuthenticationProvider.java index ec5c63f1..3450bd7a 100644 --- a/src/main/java/com/youlai/boot/framework/security/provider/SmsAuthenticationProvider.java +++ b/src/main/java/com/youlai/boot/framework/security/provider/SmsAuthenticationProvider.java @@ -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("验证码错误"); } // 验证成功后删除验证码,防止重复使用 diff --git a/src/main/java/com/youlai/boot/framework/security/service/RolePermissionService.java b/src/main/java/com/youlai/boot/framework/security/service/RolePermissionService.java new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/youlai/boot/framework/security/service/UserAuthQueryService.java b/src/main/java/com/youlai/boot/framework/security/service/UserAuthQueryService.java new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/youlai/boot/framework/security/service/WxMaUserAuthQueryService.java b/src/main/java/com/youlai/boot/framework/security/service/WxMaUserAuthQueryService.java new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java b/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java index 10966bd5..1b153f64 100644 --- a/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java +++ b/src/main/java/com/youlai/boot/framework/security/token/JwtTokenManager.java @@ -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(); diff --git a/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java b/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java index 1616bbee..52481ad4 100644 --- a/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java +++ b/src/main/java/com/youlai/boot/framework/security/token/RedisTokenManager.java @@ -5,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 redisTemplate; + private final JsonMapper jsonMapper; - public RedisTokenManager(SecurityProperties securityProperties, RedisTemplate redisTemplate) { + public RedisTokenManager(SecurityProperties securityProperties, + RedisTemplate 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 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) diff --git a/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java b/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java index 7be6bbce..0692e0de 100644 --- a/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java +++ b/src/main/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandler.java @@ -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; /** * 全局系统异常处理器 - *

- * 调整异常处理的HTTP状态码,丰富异常处理类型 + * + * @author Ray.Hao + * @since 4.3.1 */ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { - /** - * 处理绑定异常 - *

- * 当请求参数绑定到对象时发生错误,会抛出 BindException 异常。 - */ @ExceptionHandler(BindException.class) @ResponseStatus(HttpStatus.OK) public Result processException(BindException e) { @@ -53,12 +50,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.USER_REQUEST_PARAMETER_ERROR, msg); } - /** - * 处理 @RequestParam 参数校验异常 - *

- * 当请求参数在校验过程中发生违反约束条件的异常时(如 @RequestParam 验证不通过), - * 会捕获到 ConstraintViolationException 异常。 - */ @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.OK) public Result processException(ConstraintViolationException e) { @@ -67,12 +58,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.INVALID_USER_INPUT, msg); } - /** - * 处理方法参数校验异常 - *

- * 当使用 @Valid 或 @Validated 注解对方法参数进行验证时,如果验证失败, - * 会抛出 MethodArgumentNotValidException 异常。 - */ @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.OK) public Result processException(MethodArgumentNotValidException e) { @@ -81,11 +66,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.INVALID_USER_INPUT, msg); } - /** - * 处理接口不存在的异常 - *

- * 当客户端请求一个不存在的路径时,会抛出 NoHandlerFoundException 异常。 - */ @ExceptionHandler(NoHandlerFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public Result processException(NoHandlerFoundException e) { @@ -93,11 +73,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.INTERFACE_NOT_EXIST); } - /** - * 处理缺少请求参数的异常 - *

- * 当请求缺少必需的参数时,会抛出 MissingServletRequestParameterException 异常。 - */ @ExceptionHandler(MissingServletRequestParameterException.class) @ResponseStatus(HttpStatus.OK) public Result processException(MissingServletRequestParameterException e) { @@ -105,11 +80,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.REQUEST_REQUIRED_PARAMETER_IS_EMPTY); } - /** - * 处理方法参数类型不匹配的异常 - *

- * 当请求参数类型不匹配时,会抛出 MethodArgumentTypeMismatchException 异常。 - */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) @ResponseStatus(HttpStatus.OK) public Result processException(MethodArgumentTypeMismatchException e) { @@ -117,11 +87,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.PARAMETER_FORMAT_MISMATCH, "类型错误"); } - /** - * 处理 Servlet 异常 - *

- * 当 Servlet 处理请求时发生异常时,会抛出 ServletException 异常。 - */ @ExceptionHandler(ServletException.class) @ResponseStatus(HttpStatus.OK) public Result processException(ServletException e) { @@ -129,11 +94,6 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } - /** - * 处理非法参数异常 - *

- * 当方法接收到非法参数时,会抛出 IllegalArgumentException 异常。 - */ @ExceptionHandler(IllegalArgumentException.class) @ResponseStatus(HttpStatus.OK) public Result handleIllegalArgumentException(IllegalArgumentException e) { @@ -141,11 +101,6 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } - /** - * 处理 JSON 处理异常 - *

- * 当处理 JSON 数据时发生错误,会抛出 JacksonException 异常。 - */ @ExceptionHandler(JacksonException.class) @ResponseStatus(HttpStatus.OK) public Result handleJacksonException(JacksonException e) { @@ -153,11 +108,6 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } - /** - * 处理请求体不可读的异常 - *

- * 当请求体不可读时,会抛出 HttpMessageNotReadableException 异常。 - */ @ExceptionHandler(HttpMessageNotReadableException.class) @ResponseStatus(HttpStatus.OK) public Result processException(HttpMessageNotReadableException e) { @@ -170,11 +120,6 @@ public class GlobalExceptionHandler { return Result.failed(errorMessage); } - /** - * 处理类型不匹配异常 - *

- * 当方法参数类型不匹配时,会抛出 TypeMismatchException 异常。 - */ @ExceptionHandler(TypeMismatchException.class) @ResponseStatus(HttpStatus.OK) public Result processException(TypeMismatchException e) { @@ -182,11 +127,6 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } - /** - * 处理 SQL 语法错误异常 - *

- * 当 SQL 语法错误时,会抛出 BadSqlGrammarException 异常。 - */ @ExceptionHandler(BadSqlGrammarException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result handleBadSqlGrammarException(BadSqlGrammarException e) { @@ -197,11 +137,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.DATABASE_EXECUTION_ERROR); } - /** - * 处理 SQL 语法错误异常 - *

- * 当 SQL 语法错误时,会抛出 SQLSyntaxErrorException 异常。 - */ @ExceptionHandler(SQLSyntaxErrorException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { @@ -209,12 +144,6 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.DATABASE_EXECUTION_SYNTAX_ERROR); } - - /** - * 处理 SQL 违反了完整性约束 - *

- * 当 SQL 违反了完整性约束时,会抛出 SQLIntegrityConstraintViolationException 异常。 - */ @ExceptionHandler(SQLIntegrityConstraintViolationException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result handleSQLIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException e) { @@ -222,11 +151,12 @@ public class GlobalExceptionHandler { return Result.failed(ResultCode.INTEGRITY_CONSTRAINT_VIOLATION); } - /** - * 处理业务异常 - *

- * 当业务逻辑发生错误时,会抛出 BusinessException 异常。 - */ + @ExceptionHandler(TokenInvalidException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public Result handleTokenInvalidException(TokenInvalidException e) { + return Result.failed(e.getResultCode()); + } + @ExceptionHandler(BusinessException.class) @ResponseStatus(HttpStatus.OK) public Result handleBizException(BusinessException e) { @@ -237,15 +167,9 @@ public class GlobalExceptionHandler { return Result.failed(e.getMessage()); } - /** - * 处理所有未捕获的异常 - *

- * 当发生未捕获的异常时,会抛出 Exception 异常。 - */ @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Result 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 = "\\[\"(.*?)\"]+"; diff --git a/src/main/java/com/youlai/boot/framework/web/aspect/LogAspect.java b/src/main/java/com/youlai/boot/framework/web/aspect/LogAspect.java new file mode 100644 index 00000000..6642a503 --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/web/aspect/LogAspect.java @@ -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; + +/** + * 操作日志切面 + *

@Log 标注的方法执行后异步写入 sys_log 表

+ * + * @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); + } +} diff --git a/src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java b/src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java new file mode 100644 index 00000000..7c1940f1 --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java @@ -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; + +/** + * 接口级限流切面 + *

+ * 对标注了 {@link RateLimit} 的 Controller 方法做 Redis 滑动窗口计数。 + * 超阈值抛出 {@link BusinessException}(A0502),全局异常处理器统一返回 JSON。 + *

+ * + *

升级历史

+ *
    + *
  • 4.3.1 — 初始版本:固定窗口计数器
  • + *
  • 4.4.0 — 升级为滑动窗口 + X-RateLimit-* 渐进式响应头
  • + *
+ * + * @author Ray.Hao + * @since 4.3.1 + */ +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j +public class RateLimitAspect { + + private final RedisTemplate 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)); + } + } + } + +} diff --git a/src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java b/src/main/java/com/youlai/boot/framework/web/aspect/RepeatSubmitAspect.java similarity index 73% rename from src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java rename to src/main/java/com/youlai/boot/framework/web/aspect/RepeatSubmitAspect.java index 97011be1..63e2456c 100644 --- a/src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java +++ b/src/main/java/com/youlai/boot/framework/web/aspect/RepeatSubmitAspect.java @@ -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; } - } - diff --git a/src/main/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfig.java b/src/main/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfig.java new file mode 100644 index 00000000..c50edf6d --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfig.java @@ -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; + } +} diff --git a/src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java b/src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java new file mode 100644 index 00000000..a62102dc --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java @@ -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; + +/** + * 限流配置 + *

+ * 对应 application.yml 中的 {@code rate-limit} 配置节点。 + *

+ * + * @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; + + } + +} diff --git a/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java b/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java new file mode 100644 index 00000000..a05d8f99 --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/web/filter/IpRateLimitFilter.java @@ -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 全局限流过滤器 + *

+ * 在所有请求进入 Controller 之前,按 IP 维度做滑动窗口限流。 + * 与 {@code @RateLimit} 注解级限流叠加:先过 IP 全局,再过接口级。 + *

+ * + *

限流维度

+ *
{@code
+ *   Key: rate_limiter:ip:{clientIp}
+ *   默认: 1000 req / 60s(可通过 rate-limit.ip.* 配置)
+ * }
+ * + *

响应头

+ * + * + * + * + * + *
X-RateLimit-Limit窗口内最大允许请求数
X-RateLimit-Remaining窗口内剩余可用请求数
X-RateLimit-Reset窗口重置时间(Unix 秒)
Retry-After超限时建议重试等待秒数(仅 429)
+ * + * @author Ray.Hao + * @since 4.4.0 + */ +@Component +@Order(Ordered.HIGHEST_PRECEDENCE + 5) +@RequiredArgsConstructor +@Slf4j +public class IpRateLimitFilter extends OncePerRequestFilter { + + private final RedisTemplate 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)); + } + +} diff --git a/src/main/java/com/youlai/boot/framework/web/filter/RateLimiterFilter.java b/src/main/java/com/youlai/boot/framework/web/filter/RateLimiterFilter.java deleted file mode 100644 index 5b860442..00000000 --- a/src/main/java/com/youlai/boot/framework/web/filter/RateLimiterFilter.java +++ /dev/null @@ -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 redisTemplate; - private final ConfigService configService; - - private static final long DEFAULT_IP_LIMIT = 10L; // 默认 IP 限流阈值 - - public RateLimiterFilter(RedisTemplate 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); - } -} diff --git a/src/main/java/com/youlai/boot/framework/web/ratelimit/SlidingWindowScript.java b/src/main/java/com/youlai/boot/framework/web/ratelimit/SlidingWindowScript.java new file mode 100644 index 00000000..a05bc25e --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/web/ratelimit/SlidingWindowScript.java @@ -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 脚本(共享工具) + *

+ * 基于 Redis Sorted Set 实现原子化的滑动窗口计数: + *

    + *
  1. ZREMRANGEBYSCORE 清除窗口外旧请求
  2. + *
  3. ZADD 添加当前请求
  4. + *
  5. ZCARD 统计窗口内请求数
  6. + *
  7. PEXPIRE 设置 Key 过期(窗口 + 1s 冗余)
  8. + *
+ * 一次网络往返完成全部操作,避免多次 Redis 调用之间的竞态条件。 + *

+ * + * @author Ray.Hao + * @since 4.4.0 + */ +public final class SlidingWindowScript { + + private SlidingWindowScript() { + throw new UnsupportedOperationException("工具类不允许实例化"); + } + + private static final DefaultRedisScript 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 redisTemplate, + String key, + long windowMs) { + // 传 Long 而非 String,避免 Jackson 序列化器加双引号导致 Lua tonumber() 返回 nil + return redisTemplate.execute( + SCRIPT, + Collections.singletonList(key), + System.currentTimeMillis(), + windowMs, + UUID.randomUUID().toString() + ); + } + +} diff --git a/src/main/java/com/youlai/boot/framework/web/util/ResponseWriter.java b/src/main/java/com/youlai/boot/framework/web/util/ResponseWriter.java new file mode 100644 index 00000000..02f972ef --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/web/util/ResponseWriter.java @@ -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; + +/** + * 响应写入器 + *

用于过滤器、Security处理器等场景统一写入HTTP响应

+ */ +@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(); + }; + } +} diff --git a/src/main/java/com/youlai/boot/message/event/DictChangeEvent.java b/src/main/java/com/youlai/boot/message/event/DictChangeEvent.java new file mode 100644 index 00000000..e69de29b diff --git a/src/main/java/com/youlai/boot/message/job/OnlineUserCountJob.java b/src/main/java/com/youlai/boot/message/job/OnlineUserCountTask.java similarity index 63% rename from src/main/java/com/youlai/boot/message/job/OnlineUserCountJob.java rename to src/main/java/com/youlai/boot/message/job/OnlineUserCountTask.java index 14ecb42e..63dc9ac5 100644 --- a/src/main/java/com/youlai/boot/message/job/OnlineUserCountJob.java +++ b/src/main/java/com/youlai/boot/message/job/OnlineUserCountTask.java @@ -8,31 +8,22 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; /** - * 在线用户数统计定时任务 - *

- * 定时统计并广播当前在线用户数量到所有 SSE 客户端 + * 在线用户统计 + *

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

*/ @Component @Slf4j @RequiredArgsConstructor -public class OnlineUserCountJob { +public class OnlineUserCountTask { private final SseSessionRegistry sessionRegistry; private final SseService sseService; - /** - * 定时统计在线用户数并广播 - *

- * 每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(); } } diff --git a/src/main/java/com/youlai/boot/system/controller/UserController.java b/src/main/java/com/youlai/boot/system/controller/UserController.java index 528c1e20..ce9a926c 100644 --- a/src/main/java/com/youlai/boot/system/controller/UserController.java +++ b/src/main/java/com/youlai/boot/system/controller/UserController.java @@ -2,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() - .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); diff --git a/src/main/java/com/youlai/boot/system/enums/MenuTypeEnum.java b/src/main/java/com/youlai/boot/system/enums/MenuTypeEnum.java index 8745ff7f..107b7abb 100644 --- a/src/main/java/com/youlai/boot/system/enums/MenuTypeEnum.java +++ b/src/main/java/com/youlai/boot/system/enums/MenuTypeEnum.java @@ -9,6 +9,7 @@ import lombok.Getter; * * C:目录 * M:菜单 + * E:外链 * B:按钮 */ @Getter @@ -16,6 +17,7 @@ public enum MenuTypeEnum implements IBaseEnum { CATALOG("C", "目录"), MENU("M", "菜单"), + EXTERNAL("E", "外链"), BUTTON("B", "按钮"); /** diff --git a/src/main/java/com/youlai/boot/system/model/entity/Config.java b/src/main/java/com/youlai/boot/system/model/entity/Config.java index da4b54cb..c2b26317 100644 --- a/src/main/java/com/youlai/boot/system/model/entity/Config.java +++ b/src/main/java/com/youlai/boot/system/model/entity/Config.java @@ -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 { diff --git a/src/main/java/com/youlai/boot/system/model/entity/Dept.java b/src/main/java/com/youlai/boot/system/model/entity/Dept.java index 3166d09b..9c95c6a3 100644 --- a/src/main/java/com/youlai/boot/system/model/entity/Dept.java +++ b/src/main/java/com/youlai/boot/system/model/entity/Dept.java @@ -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 { /** diff --git a/src/main/java/com/youlai/boot/system/model/entity/Dict.java b/src/main/java/com/youlai/boot/system/model/entity/Dict.java index d669ca51..b011aca1 100644 --- a/src/main/java/com/youlai/boot/system/model/entity/Dict.java +++ b/src/main/java/com/youlai/boot/system/model/entity/Dict.java @@ -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 { diff --git a/src/main/java/com/youlai/boot/system/model/entity/DictItem.java b/src/main/java/com/youlai/boot/system/model/entity/DictItem.java index d7c45ce8..afaafb59 100644 --- a/src/main/java/com/youlai/boot/system/model/entity/DictItem.java +++ b/src/main/java/com/youlai/boot/system/model/entity/DictItem.java @@ -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 { diff --git a/src/main/java/com/youlai/boot/system/model/entity/Menu.java b/src/main/java/com/youlai/boot/system/model/entity/Menu.java index 475e966a..f7c0fab4 100644 --- a/src/main/java/com/youlai/boot/system/model/entity/Menu.java +++ b/src/main/java/com/youlai/boot/system/model/entity/Menu.java @@ -2,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 params; - - @TableField(fill = FieldFill.INSERT) - private LocalDateTime createTime; - - /** - * 更新时间 - */ - @TableField(fill = FieldFill.INSERT_UPDATE) - private LocalDateTime updateTime; - } diff --git a/src/main/java/com/youlai/boot/system/model/entity/Notice.java b/src/main/java/com/youlai/boot/system/model/entity/Notice.java index e16c1d68..9234808d 100644 --- a/src/main/java/com/youlai/boot/system/model/entity/Notice.java +++ b/src/main/java/com/youlai/boot/system/model/entity/Notice.java @@ -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 { diff --git a/src/main/java/com/youlai/boot/system/model/entity/Role.java b/src/main/java/com/youlai/boot/system/model/entity/Role.java index 5e39fdf5..d4f17ee8 100644 --- a/src/main/java/com/youlai/boot/system/model/entity/Role.java +++ b/src/main/java/com/youlai/boot/system/model/entity/Role.java @@ -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 { /** diff --git a/src/main/java/com/youlai/boot/system/model/entity/SysUser.java b/src/main/java/com/youlai/boot/system/model/entity/SysUser.java index 92cc9542..916042d6 100644 --- a/src/main/java/com/youlai/boot/system/model/entity/SysUser.java +++ b/src/main/java/com/youlai/boot/system/model/entity/SysUser.java @@ -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; diff --git a/src/main/java/com/youlai/boot/system/model/entity/UserNotice.java b/src/main/java/com/youlai/boot/system/model/entity/UserNotice.java index 28675f55..e3032595 100644 --- a/src/main/java/com/youlai/boot/system/model/entity/UserNotice.java +++ b/src/main/java/com/youlai/boot/system/model/entity/UserNotice.java @@ -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; } + diff --git a/src/main/java/com/youlai/boot/system/model/form/MenuForm.java b/src/main/java/com/youlai/boot/system/model/form/MenuForm.java index dabc73ce..459ab708 100644 --- a/src/main/java/com/youlai/boot/system/model/form/MenuForm.java +++ b/src/main/java/com/youlai/boot/system/model/form/MenuForm.java @@ -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; diff --git a/src/main/java/com/youlai/boot/system/model/vo/MenuVO.java b/src/main/java/com/youlai/boot/system/model/vo/MenuVO.java index 81010597..001865c6 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/MenuVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/MenuVO.java @@ -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; diff --git a/src/main/java/com/youlai/boot/system/model/vo/RouteVO.java b/src/main/java/com/youlai/boot/system/model/vo/RouteVO.java index a07ed419..121ca5df 100644 --- a/src/main/java/com/youlai/boot/system/model/vo/RouteVO.java +++ b/src/main/java/com/youlai/boot/system/model/vo/RouteVO.java @@ -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 params; } diff --git a/src/main/java/com/youlai/boot/system/service/ConfigService.java b/src/main/java/com/youlai/boot/system/service/ConfigService.java index d663826f..76bd563a 100644 --- a/src/main/java/com/youlai/boot/system/service/ConfigService.java +++ b/src/main/java/com/youlai/boot/system/service/ConfigService.java @@ -58,11 +58,4 @@ public interface ConfigService extends IService { */ boolean refreshCache(); - /** - * 获取系统配置 - * @param key 配置键 - * @return 配置值 - */ - Object getSystemConfig(String key); - } diff --git a/src/main/java/com/youlai/boot/system/service/UserService.java b/src/main/java/com/youlai/boot/system/service/UserService.java index d0abf23f..48c5f58f 100644 --- a/src/main/java/com/youlai/boot/system/service/UserService.java +++ b/src/main/java/com/youlai/boot/system/service/UserService.java @@ -55,6 +55,15 @@ public interface UserService extends IService { */ boolean updateUser(Long userId, UserForm userForm); + /** + * 修改用户状态 + * + * @param userId 用户ID + * @param status 状态值 + * @return {@link Boolean} 是否修改成功 + */ + boolean updateUserStatus(Long userId, Integer status); + /** * 删除用户 diff --git a/src/main/java/com/youlai/boot/system/service/impl/ConfigServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/ConfigServiceImpl.java index 711b90c2..21962565 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/ConfigServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/ConfigServiceImpl.java @@ -142,18 +142,5 @@ public class ConfigServiceImpl extends ServiceImpl 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; - } - } + diff --git a/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java index 088ae04a..28c581dd 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java @@ -227,36 +227,50 @@ public class MenuServiceImpl extends ServiceImpl 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 paramsMap = menu.getParams(); if (paramsMap != null && !paramsMap.isEmpty()) { Map paramMap = paramsMap.entrySet().stream() @@ -275,8 +289,8 @@ public class MenuServiceImpl extends ServiceImpl 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 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 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

() .eq(Menu::getRouteName, entity.getRouteName()) .ne(menuForm.getId() != null, Menu::getId, menuForm.getId()) diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java index 708afd26..8ffef8d4 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java @@ -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 implements ); } + @Override + public boolean updateUserStatus(Long userId, Integer status) { + return this.update(new LambdaUpdateWrapper() + .eq(SysUser::getId, userId) + .set(SysUser::getStatus, status) + ); + } + /** * 获取用户选项列表 * diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 024222ac..b1346bbe 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -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 diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 667e433d..643e2bd6 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -74,6 +74,15 @@ mybatis-plus: # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +# 接口限流 +rate-limit: + default-limit: 10 + default-window: 1 + ip: + enabled: true + limit: 1000 # IP 全局:1000 req / 60s + window-seconds: 60 + # 安全配置 security: session: diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index a424b782..9391b1a8 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -38,7 +38,7 @@ 10MB 30 - + 1GB diff --git a/src/main/resources/mapper/codegen/GenConfigMapper.xml b/src/main/resources/mapper/codegen/GenConfigMapper.xml deleted file mode 100644 index 02709a91..00000000 --- a/src/main/resources/mapper/codegen/GenConfigMapper.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml b/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml deleted file mode 100644 index 8cb86c3d..00000000 --- a/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/src/main/resources/mapper/system/MenuMapper.xml b/src/main/resources/mapper/system/MenuMapper.xml index 3914e720..2f5afaae 100644 --- a/src/main/resources/mapper/system/MenuMapper.xml +++ b/src/main/resources/mapper/system/MenuMapper.xml @@ -18,6 +18,7 @@ t1.route_name, t1.route_path, t1.component, + t1.external_url, t1.icon, t1.sort, t1.visible, diff --git a/src/main/resources/templates/codegen/backend/mapper.xml.vm b/src/main/resources/templates/codegen/backend/mapper.xml.vm index 987b16a1..301bfe4a 100644 --- a/src/main/resources/templates/codegen/backend/mapper.xml.vm +++ b/src/main/resources/templates/codegen/backend/mapper.xml.vm @@ -27,7 +27,7 @@ #else #end - #set ($queryType = ${fieldConfig.queryType}.name()) + #set ($queryType = $fieldConfig.queryType.name()) #if($queryType == "EQ") AND ${fieldConfig.columnName} = #{queryParams.${fieldConfig.fieldName}} #elseif($queryType == "LIKE") diff --git a/src/main/resources/templates/codegen/backend/query.java.vm b/src/main/resources/templates/codegen/backend/query.java.vm index d5c9780e..9d21f3fd 100644 --- a/src/main/resources/templates/codegen/backend/query.java.vm +++ b/src/main/resources/templates/codegen/backend/query.java.vm @@ -4,7 +4,17 @@ import ${packageName}.common.base.BaseQuery; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; +#if($fieldConfigs) + #set ($hasDateBetween = false) + #foreach($fieldConfig in ${fieldConfigs}) + #if(($fieldConfig.formType.name() == "DATE" || $fieldConfig.formType.name() == "DATE_TIME") && $fieldConfig.queryType.name() == "BETWEEN") + #set ($hasDateBetween = true) + #end + #end + #if($hasDateBetween) import java.util.List; + #end +#end #if(${hasLocalDateTime}) import java.time.LocalDateTime; #end diff --git a/src/main/resources/templates/codegen/frontend/js/index.curd.js.vue.vm b/src/main/resources/templates/codegen/frontend/js/index.curd.js.vue.vm index 86205f97..ee1fec35 100644 --- a/src/main/resources/templates/codegen/frontend/js/index.curd.js.vue.vm +++ b/src/main/resources/templates/codegen/frontend/js/index.curd.js.vue.vm @@ -10,7 +10,7 @@ #foreach($fieldConfig in $fieldConfigs) #if($fieldConfig.isShowInQuery == 1 && $fieldConfig.dictType && $fieldConfig.dictType.trim() != "") #end #end @@ -30,7 +30,7 @@ #foreach($fieldConfig in $fieldConfigs) #if($fieldConfig.isShowInList == 1 && $fieldConfig.dictType && $fieldConfig.dictType.trim() != "") #end #end @@ -41,7 +41,7 @@ #foreach($fieldConfig in $fieldConfigs) #if($fieldConfig.isShowInForm == 1 && $fieldConfig.formType.name() != "HIDDEN" && $fieldConfig.dictType && $fieldConfig.dictType.trim() != "") #end #end @@ -52,7 +52,7 @@ #foreach($fieldConfig in $fieldConfigs) #if($fieldConfig.isShowInForm == 1 && $fieldConfig.formType.name() != "HIDDEN" && $fieldConfig.dictType && $fieldConfig.dictType.trim() != "") #end #end @@ -61,7 +61,10 @@