diff --git a/sql/youlai-admin.sql b/sql/youlai-admin.sql index b5955f4a..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 @@ -607,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/auth/controller/AuthController.java b/src/main/java/com/youlai/boot/auth/controller/AuthController.java index 7a7975cf..c159f577 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; @@ -41,7 +42,8 @@ public class AuthController { @Operation(summary = "账号密码登录") @PostMapping("/login") @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) - public Result login(@RequestBody @Valid LoginReq request) { + @RateLimit(limit = 5, window = 60) + public Result login(@RequestBody @Valid LoginForm request) { AuthenticationToken authenticationToken = authService.login(request.getUsername(), request.getPassword()); return Result.success(authenticationToken); } @@ -59,6 +61,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..33653466 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(limit = 10, window = 60) + 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(limit = 5, window = 60) + 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(limit = 3, window = 60) + 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/RateLimit.java b/src/main/java/com/youlai/boot/common/annotation/RateLimit.java new file mode 100644 index 00000000..d8e9dfc7 --- /dev/null +++ b/src/main/java/com/youlai/boot/common/annotation/RateLimit.java @@ -0,0 +1,22 @@ +package com.youlai.boot.common.annotation; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * 接口限流 + *

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

+ */ +@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 145341c9..00000000 --- a/src/main/java/com/youlai/boot/common/aspect/LogAspect.java +++ /dev/null @@ -1,144 +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); - - // 解析 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); - logEntity.setOperatorName(username); - logEntity.setRequestUri(request.getRequestURI()); - logEntity.setRequestMethod(request.getMethod()); - logEntity.setIp(ip); - logEntity.setProvince(province); - logEntity.setCity(city); - logEntity.setDevice(userAgent.getOs().getName()); - logEntity.setOs(userAgent.getOs().getName()); - logEntity.setBrowser(userAgent.getBrowser().getName()); - logEntity.setStatus(exception == null ? 1 : 0); - logEntity.setErrorMsg(exception != null ? exception.getMessage() : null); - logEntity.setExecutionTime((int) executionTime); - logEntity.setCreateTime(LocalDateTime.now()); - - logService.save(logEntity); - } catch (Exception e) { - log.error("保存操作日志异常: {}", e.getMessage()); - } - } -} 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..410ff1ed 100644 --- a/src/main/java/com/youlai/boot/common/constant/RedisConstants.java +++ b/src/main/java/com/youlai/boot/common/constant/RedisConstants.java @@ -12,7 +12,7 @@ public interface RedisConstants { * 限流相关键 */ interface RateLimiter { - String IP = "rate_limiter:ip:{}"; // IP限流(示例:rate_limiter:ip:192.168.1.1) + String API = "{}rate_limit:{}:{}"; // 接口限流(示例:login:rate_limit:token:/api/v1/auth/login) } /** 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/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/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/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..2da50bd3 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,7 +5,7 @@ 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; @@ -43,7 +43,7 @@ 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 68% 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..8cb13732 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 @@ -9,36 +9,19 @@ import java.time.LocalDateTime; /** * mybatis-plus 字段自动填充 - *

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

- * - * @author Ray.Hao - * @since 2022/10/14 */ @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/security/config/SecurityConfig.java b/src/main/java/com/youlai/boot/framework/security/config/SecurityConfig.java index 190175e6..c1b7c828 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 @@ -3,7 +3,6 @@ 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.web.filter.RateLimiterFilter; import com.youlai.boot.framework.security.filter.CaptchaValidationFilter; import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter; import com.youlai.boot.framework.security.handler.MyAccessDeniedHandler; @@ -12,12 +11,12 @@ 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.data.redis.core.RedisTemplate; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.core.RedisTemplate; + import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; @@ -52,7 +51,6 @@ public class SecurityConfig { private final SysUserDetailsService userDetailsService; private final CaptchaService captchaService; - private final ConfigService configService; private final SecurityProperties securityProperties; /** @@ -86,8 +84,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) // 验证和解析过滤器 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/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/web/aspect/LogAspect.java b/src/main/java/com/youlai/boot/framework/web/aspect/LogAspect.java new file mode 100644 index 00000000..6ecf00ee --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/web/aspect/LogAspect.java @@ -0,0 +1,135 @@ +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 表

+ */ +@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..4276a604 --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/web/aspect/RateLimitAspect.java @@ -0,0 +1,69 @@ +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 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.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; + +import java.util.concurrent.TimeUnit; + +/** + * 接口限流切面 + *

对 @RateLimit 标注的方法做 Redis 计数,超阈值则拒绝

+ */ +@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(); + Long count = redisTemplate.opsForValue().increment(key); + if (count != null && count == 1) { + int window = rateLimit.window() > 0 ? rateLimit.window() : rateLimitProperties.getDefaultWindow(); + redisTemplate.expire(key, window, rateLimit.timeUnit()); + } + if (count != null && count > limit) { + log.warn("限流触发 key={} count={} limit={}", key, count, 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); + } +} diff --git a/src/main/java/com/youlai/boot/framework/web/aspect/RepeatSubmitAspect.java b/src/main/java/com/youlai/boot/framework/web/aspect/RepeatSubmitAspect.java new file mode 100644 index 00000000..e69de29b 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..c4b5c646 --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfig.java @@ -0,0 +1,29 @@ +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; + +/** + * 操作日志线程池 + */ +@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..6ea46c6d --- /dev/null +++ b/src/main/java/com/youlai/boot/framework/web/config/RateLimitProperties.java @@ -0,0 +1,18 @@ +package com.youlai.boot.framework.web.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 限流配置 + */ +@Data +@Component +@ConfigurationProperties(prefix = "rate-limit") +public class RateLimitProperties { + + private int defaultLimit = 5; + + private int defaultWindow = 1; +} 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/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/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..180435f0 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 */ @@ -100,14 +94,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..42cae8f3 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 { /** 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/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/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/resources/application-dev.yml b/src/main/resources/application-dev.yml index a95e360d..12d1d96c 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -68,6 +68,11 @@ mybatis-plus: # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +# 接口限流 +rate-limit: + default-limit: 5 + default-window: 1 + # 安全配置 security: session: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 86085e47..f445a740 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -67,6 +67,11 @@ mybatis-plus: # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +# 接口限流 +rate-limit: + default-limit: 10 + default-window: 1 + # 安全配置 security: session: 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/test/java/com/youlai/boot/auth/service/impl/AuthServiceImplTest.java b/src/test/java/com/youlai/boot/auth/service/impl/AuthServiceImplTest.java new file mode 100644 index 00000000..ab8d40a2 --- /dev/null +++ b/src/test/java/com/youlai/boot/auth/service/impl/AuthServiceImplTest.java @@ -0,0 +1,61 @@ +package com.youlai.boot.auth.service.impl; + +import com.youlai.boot.common.exception.BusinessException; +import com.youlai.boot.common.result.ResultCode; +import com.youlai.boot.framework.captcha.service.CaptchaService; +import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum; +import com.youlai.boot.framework.integration.sms.service.SmsService; +import com.youlai.boot.framework.security.token.TokenManager; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.AuthenticationManager; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AuthServiceImplTest { + + @Mock + private AuthenticationManager authenticationManager; + + @Mock + private TokenManager tokenManager; + + @Mock + private SmsService smsService; + + @Mock + private RedisTemplate redisTemplate; + + @Mock + private CaptchaService captchaService; + + @Test + void should_not_cache_sms_code_when_send_fails() { + when(smsService.sendSms(eq("13800138000"), eq(SmsTypeEnum.LOGIN), any(Map.class))).thenReturn(false); + + AuthServiceImpl authService = new AuthServiceImpl( + authenticationManager, + tokenManager, + smsService, + redisTemplate, + captchaService + ); + + assertThatThrownBy(() -> authService.sendSmsCode("13800138000")) + .isInstanceOf(BusinessException.class) + .hasMessage("短信验证码发送失败"); + + verify(redisTemplate, never()).opsForValue(); + } +} diff --git a/src/test/java/com/youlai/boot/file/service/impl/LocalFileServiceTest.java b/src/test/java/com/youlai/boot/file/service/impl/LocalFileServiceTest.java new file mode 100644 index 00000000..78c651f3 --- /dev/null +++ b/src/test/java/com/youlai/boot/file/service/impl/LocalFileServiceTest.java @@ -0,0 +1,27 @@ +package com.youlai.boot.file.service.impl; + +import com.youlai.boot.common.exception.BusinessException; +import com.youlai.boot.file.config.LocalFileProperties; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LocalFileServiceTest { + + @TempDir + Path storagePath; + + @Test + void should_reject_path_outside_storage_root() { + LocalFileProperties properties = new LocalFileProperties(); + properties.setStoragePath(storagePath.toString()); + LocalFileService fileService = new LocalFileService(properties); + + assertThatThrownBy(() -> fileService.deleteFile("../secret.txt")) + .isInstanceOf(BusinessException.class) + .hasMessage("文件路径不属于当前本地存储配置"); + } +} diff --git a/src/test/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandlerTest.java b/src/test/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandlerTest.java new file mode 100644 index 00000000..0c88711f --- /dev/null +++ b/src/test/java/com/youlai/boot/framework/web/advice/GlobalExceptionHandlerTest.java @@ -0,0 +1,20 @@ +package com.youlai.boot.framework.web.advice; + +import com.youlai.boot.common.result.Result; +import com.youlai.boot.common.result.ResultCode; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class GlobalExceptionHandlerTest { + + @Test + void should_not_expose_unknown_exception_message() throws Exception { + GlobalExceptionHandler handler = new GlobalExceptionHandler(); + + Result result = handler.handleException(new IllegalStateException("database password leaked")); + + assertThat(result.getCode()).isEqualTo(ResultCode.SYSTEM_ERROR.getCode()); + assertThat(result.getMsg()).isEqualTo("系统异常,请稍后重试"); + } +} diff --git a/src/test/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfigTest.java b/src/test/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfigTest.java new file mode 100644 index 00000000..8c0b9494 --- /dev/null +++ b/src/test/java/com/youlai/boot/framework/web/config/OperationLogExecutorConfigTest.java @@ -0,0 +1,28 @@ +package com.youlai.boot.framework.web.config; + +import org.junit.jupiter.api.Test; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +import static org.assertj.core.api.Assertions.assertThat; + +class OperationLogExecutorConfigTest { + + @Test + void should_create_operation_log_executor() { + Executor executor = new OperationLogExecutorConfig().operationLogExecutor(); + + assertThat(executor).isInstanceOf(ThreadPoolTaskExecutor.class); + ThreadPoolTaskExecutor taskExecutor = (ThreadPoolTaskExecutor) executor; + assertThat(taskExecutor.getThreadNamePrefix()).isEqualTo("operation-log-"); + assertThat(taskExecutor.getCorePoolSize()).isEqualTo(1); + assertThat(taskExecutor.getMaxPoolSize()).isEqualTo(2); + assertThat(taskExecutor.getThreadPoolExecutor().getQueue().remainingCapacity()).isEqualTo(1000); + assertThat(taskExecutor.getThreadPoolExecutor().getRejectedExecutionHandler()) + .isInstanceOf(ThreadPoolExecutor.CallerRunsPolicy.class); + + taskExecutor.shutdown(); + } +} diff --git a/src/test/java/com/youlai/boot/security/filter/TokenAuthenticationFilterTest.java b/src/test/java/com/youlai/boot/security/filter/TokenAuthenticationFilterTest.java new file mode 100644 index 00000000..e8393b06 --- /dev/null +++ b/src/test/java/com/youlai/boot/security/filter/TokenAuthenticationFilterTest.java @@ -0,0 +1,48 @@ +package com.youlai.boot.security.filter; + +import com.youlai.boot.common.constant.SecurityConstants; +import com.youlai.boot.common.result.ResultCode; +import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter; +import com.youlai.boot.framework.security.token.TokenManager; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TokenAuthenticationFilterTest { + + @Mock + private TokenManager tokenManager; + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void should_clear_context_and_return_unauthorized_when_token_validation_fails() throws Exception { + String token = "invalid-token"; + when(tokenManager.validateToken(token)).thenThrow(new IllegalStateException("token store unavailable")); + SecurityContextHolder.getContext().setAuthentication(new org.springframework.security.authentication.TestingAuthenticationToken("old", null)); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/users"); + request.addHeader(HttpHeaders.AUTHORIZATION, SecurityConstants.BEARER_TOKEN_PREFIX + token); + MockHttpServletResponse response = new MockHttpServletResponse(); + + new TokenAuthenticationFilter(tokenManager).doFilter(request, response, new MockFilterChain()); + + assertThat(response.getStatus()).isEqualTo(401); + assertThat(response.getContentAsString()).contains(ResultCode.ACCESS_TOKEN_INVALID.getCode()); + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + } +}