refactor: 统一实体/限流AOP化/精简日志/文件归位

This commit is contained in:
Ray.Hao
2026-06-24 08:11:23 +08:00
parent 0dce433849
commit 6b6c06370f
71 changed files with 751 additions and 914 deletions

View File

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

View File

@@ -1,11 +1,12 @@
package com.youlai.boot.auth.controller; package com.youlai.boot.auth.controller;
import com.youlai.boot.auth.model.LoginReq; import com.youlai.boot.auth.model.form.LoginForm;
import com.youlai.boot.common.enums.ActionTypeEnum; import com.youlai.boot.common.enums.ActionTypeEnum;
import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.result.Result; import com.youlai.boot.common.result.Result;
import com.youlai.boot.auth.service.AuthService; import com.youlai.boot.auth.service.AuthService;
import com.youlai.boot.common.annotation.Log; import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.common.annotation.RateLimit;
import com.youlai.boot.framework.captcha.model.CaptchaInfo; import com.youlai.boot.framework.captcha.model.CaptchaInfo;
import com.youlai.boot.framework.security.model.AuthenticationToken; import com.youlai.boot.framework.security.model.AuthenticationToken;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -41,7 +42,8 @@ public class AuthController {
@Operation(summary = "账号密码登录") @Operation(summary = "账号密码登录")
@PostMapping("/login") @PostMapping("/login")
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) @Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
public Result<AuthenticationToken> login(@RequestBody @Valid LoginReq request) { @RateLimit(limit = 5, window = 60)
public Result<AuthenticationToken> login(@RequestBody @Valid LoginForm request) {
AuthenticationToken authenticationToken = authService.login(request.getUsername(), request.getPassword()); AuthenticationToken authenticationToken = authService.login(request.getUsername(), request.getPassword());
return Result.success(authenticationToken); return Result.success(authenticationToken);
} }
@@ -59,6 +61,7 @@ public class AuthController {
@Operation(summary = "发送登录短信验证码") @Operation(summary = "发送登录短信验证码")
@PostMapping("/sms/code") @PostMapping("/sms/code")
@RateLimit(limit = 1, window = 60)
public Result<Void> sendSmsCode( public Result<Void> sendSmsCode(
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile @Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile
) { ) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.auth.model.WxMaLoginResp; import com.youlai.boot.auth.model.vo.WxMaLoginVO;
import com.youlai.boot.auth.service.WxMaAuthService; import com.youlai.boot.auth.service.WxMaAuthService;
import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.framework.security.exception.NeedBindMobileException; import com.youlai.boot.framework.security.exception.NeedBindMobileException;
@@ -54,14 +54,14 @@ public class WxMaAuthServiceImpl implements WxMaAuthService {
* 静默登录 * 静默登录
*/ */
@Override @Override
public WxMaLoginResp silentLogin(String code) { public WxMaLoginVO silentLogin(String code) {
WxMaAuthenticationToken token = new WxMaAuthenticationToken(code); WxMaAuthenticationToken token = new WxMaAuthenticationToken(code);
try { try {
Authentication authentication = authenticationManager.authenticate(token); Authentication authentication = authenticationManager.authenticate(token);
AuthenticationToken authToken = tokenManager.generateToken(authentication); AuthenticationToken authToken = tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
return WxMaLoginResp.builder() return WxMaLoginVO.builder()
.isNewUser(false) .isNewUser(false)
.needBindMobile(false) .needBindMobile(false)
.accessToken(authToken.getAccessToken()) .accessToken(authToken.getAccessToken())
@@ -70,7 +70,7 @@ public class WxMaAuthServiceImpl implements WxMaAuthService {
.expiresIn(authToken.getExpiresIn()) .expiresIn(authToken.getExpiresIn())
.build(); .build();
} catch (NeedBindMobileException e) { } catch (NeedBindMobileException e) {
return WxMaLoginResp.builder() return WxMaLoginVO.builder()
.isNewUser(true) .isNewUser(true)
.needBindMobile(true) .needBindMobile(true)
.openid(e.getOpenid()) .openid(e.getOpenid())

View File

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

View File

@@ -1,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());
}
}
}

View File

@@ -12,7 +12,7 @@ public interface RedisConstants {
* 限流相关键 * 限流相关键
*/ */
interface RateLimiter { 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
} }
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import cn.hutool.captcha.generator.CodeGenerator;
import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.enums.CaptchaTypeEnum; import com.youlai.boot.framework.captcha.enums.CaptchaTypeEnum;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.framework.captcha.config.CaptchaProperties; import com.youlai.boot.framework.captcha.config.CaptchaProperties;
import com.youlai.boot.framework.captcha.exception.CaptchaException; import com.youlai.boot.framework.captcha.exception.CaptchaException;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@ import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.youlai.boot.framework.mybatis.handler.MyMetaObjectHandler; import com.youlai.boot.framework.mybatis.handler.AutoFillMetaObjectHandler;
import com.youlai.boot.framework.mybatis.interceptor.MyDataPermissionHandler; import com.youlai.boot.framework.mybatis.interceptor.MyDataPermissionHandler;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -43,7 +43,7 @@ public class MybatisConfig {
@Bean @Bean
public GlobalConfig globalConfig() { public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig(); GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setMetaObjectHandler(new MyMetaObjectHandler()); globalConfig.setMetaObjectHandler(new AutoFillMetaObjectHandler());
return globalConfig; return globalConfig;
} }

View File

@@ -9,36 +9,19 @@ import java.time.LocalDateTime;
/** /**
* mybatis-plus 字段自动填充 * mybatis-plus 字段自动填充
* <p>
* 支持自动填充创建时间更新时间
* </p>
*
* @author Ray.Hao
* @since 2022/10/14
*/ */
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class MyMetaObjectHandler implements MetaObjectHandler { public class AutoFillMetaObjectHandler implements MetaObjectHandler {
/**
* 新增填充创建时间更新时间
*
* @param metaObject 元数据
*/
@Override @Override
public void insertFill(MetaObject metaObject) { public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class); this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
} }
/**
* 更新填充更新时间
*
* @param metaObject 元数据
*/
@Override @Override
public void updateFill(MetaObject metaObject) { public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class); this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
} }
} }

View File

@@ -3,7 +3,6 @@ package com.youlai.boot.framework.security.config;
import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.api.WxMaService;
import com.youlai.boot.framework.captcha.service.CaptchaService; import com.youlai.boot.framework.captcha.service.CaptchaService;
import cn.hutool.core.util.ArrayUtil; 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.CaptchaValidationFilter;
import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter; import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter;
import com.youlai.boot.framework.security.handler.MyAccessDeniedHandler; import com.youlai.boot.framework.security.handler.MyAccessDeniedHandler;
@@ -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.provider.WxMaAuthenticationProvider;
import com.youlai.boot.framework.security.token.TokenManager; import com.youlai.boot.framework.security.token.TokenManager;
import com.youlai.boot.framework.security.service.SysUserDetailsService; import com.youlai.boot.framework.security.service.SysUserDetailsService;
import com.youlai.boot.system.service.ConfigService;
import com.youlai.boot.system.service.UserService; import com.youlai.boot.system.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
@@ -52,7 +51,6 @@ public class SecurityConfig {
private final SysUserDetailsService userDetailsService; private final SysUserDetailsService userDetailsService;
private final CaptchaService captchaService; private final CaptchaService captchaService;
private final ConfigService configService;
private final SecurityProperties securityProperties; private final SecurityProperties securityProperties;
/** /**
@@ -86,8 +84,6 @@ public class SecurityConfig {
.httpBasic(AbstractHttpConfigurer::disable) // 禁用 HTTP Basic 认证,避免弹窗式登录 .httpBasic(AbstractHttpConfigurer::disable) // 禁用 HTTP Basic 认证,避免弹窗式登录
// 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中 // 禁用 X-Frame-Options 响应头,允许页面被嵌套到 iframe 中
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
// 限流过滤器
.addFilterBefore(new RateLimiterFilter(redisTemplate, configService), UsernamePasswordAuthenticationFilter.class)
// 验证码校验过滤器 // 验证码校验过滤器
.addFilterBefore(new CaptchaValidationFilter(captchaService), UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new CaptchaValidationFilter(captchaService), UsernamePasswordAuthenticationFilter.class)
// 验证和解析过滤器 // 验证和解析过滤器

View File

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

View File

@@ -3,7 +3,7 @@ package com.youlai.boot.framework.security.filter;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.result.ResponseWriter; import com.youlai.boot.framework.web.util.ResponseWriter;
import com.youlai.boot.framework.security.token.TokenManager; import com.youlai.boot.framework.security.token.TokenManager;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.framework.security.handler; package com.youlai.boot.framework.security.handler;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.result.ResponseWriter; import com.youlai.boot.framework.web.util.ResponseWriter;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.access.AccessDeniedHandler;

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.framework.security.handler; package com.youlai.boot.framework.security.handler;
import com.youlai.boot.common.result.ResultCode; import com.youlai.boot.common.result.ResultCode;
import com.youlai.boot.common.result.ResponseWriter; import com.youlai.boot.framework.web.util.ResponseWriter;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;

View File

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

View File

@@ -0,0 +1,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;
/**
* 接口限流切面
* <p>对 @RateLimit 标注的方法做 Redis 计数,超阈值则拒绝</p>
*/
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class RateLimitAspect {
private final RedisTemplate<String, Object> redisTemplate;
private final RateLimitProperties rateLimitProperties;
@Around("@annotation(rateLimit)")
public Object handle(ProceedingJoinPoint jp, RateLimit rateLimit) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String key = buildKey(request, rateLimit);
int limit = rateLimit.limit() > 0 ? rateLimit.limit() : rateLimitProperties.getDefaultLimit();
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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,10 +2,10 @@ package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.*; import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Getter; import com.youlai.boot.common.base.BaseEntity;
import lombok.Setter; import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
import java.util.Map; import java.util.Map;
/** /**
@@ -15,15 +15,9 @@ import java.util.Map;
* @since 2023/3/6 * @since 2023/3/6
*/ */
@TableName(value = "sys_menu", autoResultMap = true) @TableName(value = "sys_menu", autoResultMap = true)
@Getter @Data
@Setter @EqualsAndHashCode(callSuper = true)
public class Menu { public class Menu extends BaseEntity {
/**
* 菜单ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/** /**
* 父菜单ID * 父菜单ID
*/ */
@@ -100,14 +94,4 @@ public class Menu {
@TableField(updateStrategy = FieldStrategy.ALWAYS, typeHandler = JacksonTypeHandler.class) @TableField(updateStrategy = FieldStrategy.ALWAYS, typeHandler = JacksonTypeHandler.class)
private Map<String, Object> params; private Map<String, Object> params;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
} }

View File

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

View File

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

View File

@@ -2,15 +2,15 @@ package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.youlai.boot.common.base.BaseEntity; import com.youlai.boot.common.base.BaseEntity;
import lombok.Getter; import lombok.Data;
import lombok.Setter; import lombok.EqualsAndHashCode;
/** /**
* 用户实体 * 用户实体
*/ */
@TableName("sys_user") @TableName("sys_user")
@Getter @Data
@Setter @EqualsAndHashCode(callSuper = true)
public class SysUser extends BaseEntity { public class SysUser extends BaseEntity {
/** /**

View File

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

View File

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

View File

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

View File

@@ -68,6 +68,11 @@ mybatis-plus:
# 这个配置会将执行的sql打印出来在开发或测试的时候可以用 # 这个配置会将执行的sql打印出来在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 接口限流
rate-limit:
default-limit: 5
default-window: 1
# 安全配置 # 安全配置
security: security:
session: session:

View File

@@ -67,6 +67,11 @@ mybatis-plus:
# 这个配置会将执行的sql打印出来在开发或测试的时候可以用 # 这个配置会将执行的sql打印出来在开发或测试的时候可以用
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 接口限流
rate-limit:
default-limit: 10
default-window: 1
# 安全配置 # 安全配置
security: security:
session: session:

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youlai.boot.platform.codegen.mapper.GenConfigMapper">
</mapper>

View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.youlai.boot.platform.codegen.mapper.GenTableColumnMapper">
</mapper>

View File

@@ -18,6 +18,7 @@
t1.route_name, t1.route_name,
t1.route_path, t1.route_path,
t1.component, t1.component,
t1.external_url,
t1.icon, t1.icon,
t1.sort, t1.sort,
t1.visible, t1.visible,

View File

@@ -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<String, Object> 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();
}
}

View File

@@ -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("文件路径不属于当前本地存储配置");
}
}

View File

@@ -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<Void> result = handler.handleException(new IllegalStateException("database password leaked"));
assertThat(result.getCode()).isEqualTo(ResultCode.SYSTEM_ERROR.getCode());
assertThat(result.getMsg()).isEqualTo("系统异常,请稍后重试");
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}