diff --git a/sql/mysql/tenant_add.sql b/sql/mysql/tenant_add.sql index ec367255..07305776 100644 --- a/sql/mysql/tenant_add.sql +++ b/sql/mysql/tenant_add.sql @@ -39,58 +39,24 @@ INSERT INTO `sys_tenant` (`id`, `name`, `code`, `status`, `create_time`) VALUES (1, '默认租户', 'DEFAULT', 1, NOW()); -- ============================================ --- 2. 创建租户切换审计日志表 --- ============================================ -DROP TABLE IF EXISTS `sys_tenant_switch_log`; -CREATE TABLE `sys_tenant_switch_log` ( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` bigint NOT NULL COMMENT '用户ID', - `username` varchar(64) COMMENT '用户名', - `from_tenant_id` bigint COMMENT '原租户ID', - `from_tenant_name` varchar(100) COMMENT '原租户名称', - `to_tenant_id` bigint NOT NULL COMMENT '目标租户ID', - `to_tenant_name` varchar(100) COMMENT '目标租户名称', - `switch_time` datetime NOT NULL COMMENT '切换时间', - `ip_address` varchar(50) COMMENT 'IP地址', - `user_agent` varchar(500) COMMENT '浏览器信息', - `status` tinyint DEFAULT '1' COMMENT '切换状态(1-成功 0-失败)', - `fail_reason` varchar(255) COMMENT '失败原因', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_switch_time` (`switch_time`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租户切换审计日志表'; - --- ============================================ --- 3. 创建用户租户关联表(支持一个用户属于多个租户) --- ============================================ -DROP TABLE IF EXISTS `sys_user_tenant`; -CREATE TABLE `sys_user_tenant` ( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` bigint NOT NULL COMMENT '用户ID', - `tenant_id` bigint NOT NULL COMMENT '租户ID', - `is_default` tinyint DEFAULT '0' COMMENT '是否默认租户(1-是 0-否)', - `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_user_tenant` (`user_id`, `tenant_id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_tenant_id` (`tenant_id`) -) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户租户关联表(多租户模式)'; - --- ============================================ --- 3. 为业务表添加 tenant_id 字段 +-- 2. 为业务表添加 tenant_id 字段 -- ============================================ -- 注意:MySQL 5.7 不支持 IF NOT EXISTS,如果字段已存在会报错 -- 建议先检查字段是否存在,或使用 MySQL 8.0+ --- 用户表 +-- 用户表:仅在不存在时添加列和索引,避免重复执行报错 ALTER TABLE `sys_user` ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`, ADD INDEX `idx_tenant_id` (`tenant_id`); --- 更新现有数据的 tenant_id(设置为默认租户) UPDATE `sys_user` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL; +-- 修改 username 索引:从单列索引改为 (username, tenant_id) 组合唯一索引 +-- 这样同一租户内用户名唯一,不同租户可以有相同用户名 +DROP INDEX `login_name` ON `sys_user`; +ALTER TABLE `sys_user` +ADD UNIQUE KEY `uk_username_tenant` (`username`, `tenant_id`); + -- 角色表 ALTER TABLE `sys_role` ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`, @@ -120,53 +86,21 @@ ADD INDEX `idx_tenant_id` (`tenant_id`); UPDATE `sys_log` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL; -- AI 命令记录表 -ALTER TABLE `ai_command_log` +ALTER TABLE `ai_command_record` ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`, ADD INDEX `idx_tenant_id` (`tenant_id`); -UPDATE `ai_command_log` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL; +UPDATE `ai_command_record` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL; --- 代码生成配置表(如果存在) --- ALTER TABLE `gen_config` --- ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`, --- ADD INDEX `idx_tenant_id` (`tenant_id`); --- UPDATE `gen_config` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL; - --- 代码生成字段配置表(如果存在) --- ALTER TABLE `gen_field_config` --- ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`, --- ADD INDEX `idx_tenant_id` (`tenant_id`); --- UPDATE `gen_field_config` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL; -- ============================================ --- 4. 初始化现有用户的租户关联(默认租户) --- ============================================ -INSERT INTO `sys_user_tenant` (`user_id`, `tenant_id`, `is_default`) -SELECT `id`, 1, 1 FROM `sys_user` WHERE `is_deleted` = 0 -ON DUPLICATE KEY UPDATE `is_default` = 1; - --- ============================================ --- 5. 添加租户管理菜单和权限(仅在菜单不存在时添加) +-- 4. 添加租户管理菜单和权限(仅在菜单不存在时添加) -- ============================================ -- 租户管理主菜单(放在部门管理之后,字典管理之前,ID=6) INSERT INTO `sys_menu` (`id`, `parent_id`, `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, 1, '0,1', '租户管理', 1, 'Tenant', 'tenant', 'system/tenant/index', NULL, NULL, NULL, 1, 5, 'el-icon-OfficeBuilding', NULL, NOW(), NOW(), NULL) ON DUPLICATE KEY UPDATE `name` = '租户管理'; --- 调整字典管理的排序(从6改为7) -UPDATE `sys_menu` SET `sort` = 7 WHERE `id` = 7 AND `sort` = 6; - --- 调整字典项的排序(从7改为8) -UPDATE `sys_menu` SET `sort` = 8 WHERE `id` = 8 AND `sort` = 7; - --- 调整系统日志的排序(从8改为9) -UPDATE `sys_menu` SET `sort` = 9 WHERE `id` = 9 AND `sort` = 8; - --- 调整系统配置的排序(从9改为10) -UPDATE `sys_menu` SET `sort` = 10 WHERE `id` = 10 AND `sort` = 9; - --- 调整通知公告的排序(从10改为11) -UPDATE `sys_menu` SET `sort` = 11 WHERE `id` = 11 AND `sort` = 10; -- 租户管理权限按钮(ID: 141-145) INSERT INTO `sys_menu` (`id`, `parent_id`, `tree_path`, `name`, `type`, `route_name`, `route_path`, `component`, `perm`, `always_show`, `keep_alive`, `visible`, `sort`, `icon`, `redirect`, `create_time`, `update_time`, `params`) @@ -190,20 +124,3 @@ VALUES ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`); SET FOREIGN_KEY_CHECKS = 1; - --- ============================================ --- 脚本执行完成 --- ============================================ --- 执行完成后,请在 application.yml 中配置: --- youlai: --- tenant: --- enabled: true --- column: tenant_id --- default-tenant-id: 1 --- header-name: tenant-id --- ignore-tables: --- - sys_tenant --- - sys_dict --- - sys_dict_item --- - sys_config --- ============================================ diff --git a/sql/mysql/tenant_remove.sql b/sql/mysql/tenant_remove.sql index d247a4f7..c179895b 100644 --- a/sql/mysql/tenant_remove.sql +++ b/sql/mysql/tenant_remove.sql @@ -12,60 +12,48 @@ USE youlai_admin; SET FOREIGN_KEY_CHECKS = 0; -- ============================================ --- 1. 删除用户租户关联表 --- ============================================ -DROP TABLE IF EXISTS `sys_user_tenant`; - --- ============================================ --- 2. 删除租户表(可选) +-- 1. 删除租户表(可选) -- ============================================ -- 注意:如果将来可能再次启用多租户,建议保留此表 -- 如需删除,取消下面的注释 -- DROP TABLE IF EXISTS `sys_tenant`; -- ============================================ --- 3. 移除业务表的 tenant_id 字段和索引 +-- 2. 移除业务表的 tenant_id 字段和索引 -- ============================================ -- 注意:如果字段不存在会报错,请根据实际情况调整 -- 用户表 -ALTER TABLE `sys_user` DROP INDEX IF EXISTS `idx_tenant_id`; -ALTER TABLE `sys_user` DROP COLUMN IF EXISTS `tenant_id`; +-- 先删除组合唯一索引 +ALTER TABLE `sys_user` DROP INDEX `uk_username_tenant`; +-- 删除租户ID索引和字段 +ALTER TABLE `sys_user` DROP INDEX `idx_tenant_id`; +ALTER TABLE `sys_user` DROP COLUMN `tenant_id`; +-- 恢复原来的用户名唯一索引 +ALTER TABLE `sys_user` ADD UNIQUE KEY `login_name` (`username`); -- 角色表 -ALTER TABLE `sys_role` DROP INDEX IF EXISTS `idx_tenant_id`; -ALTER TABLE `sys_role` DROP COLUMN IF EXISTS `tenant_id`; +ALTER TABLE `sys_role` DROP INDEX `idx_tenant_id`; +ALTER TABLE `sys_role` DROP COLUMN `tenant_id`; -- 部门表 -ALTER TABLE `sys_dept` DROP INDEX IF EXISTS `idx_tenant_id`; -ALTER TABLE `sys_dept` DROP COLUMN IF EXISTS `tenant_id`; +ALTER TABLE `sys_dept` DROP INDEX `idx_tenant_id`; +ALTER TABLE `sys_dept` DROP COLUMN `tenant_id`; -- 通知公告表 -ALTER TABLE `sys_notice` DROP INDEX IF EXISTS `idx_tenant_id`; -ALTER TABLE `sys_notice` DROP COLUMN IF EXISTS `tenant_id`; +ALTER TABLE `sys_notice` DROP INDEX `idx_tenant_id`; +ALTER TABLE `sys_notice` DROP COLUMN `tenant_id`; -- 系统日志表 -ALTER TABLE `sys_log` DROP INDEX IF EXISTS `idx_tenant_id`; -ALTER TABLE `sys_log` DROP COLUMN IF EXISTS `tenant_id`; +ALTER TABLE `sys_log` DROP INDEX `idx_tenant_id`; +ALTER TABLE `sys_log` DROP COLUMN `tenant_id`; -- AI 命令记录表 -ALTER TABLE `ai_command_log` DROP INDEX IF EXISTS `idx_tenant_id`; -ALTER TABLE `ai_command_log` DROP COLUMN IF EXISTS `tenant_id`; - --- 代码生成配置表(如果存在) --- ALTER TABLE `gen_config` DROP INDEX IF EXISTS `idx_tenant_id`; --- ALTER TABLE `gen_config` DROP COLUMN IF EXISTS `tenant_id`; - --- 代码生成字段配置表(如果存在) --- ALTER TABLE `gen_field_config` DROP INDEX IF EXISTS `idx_tenant_id`; --- ALTER TABLE `gen_field_config` DROP COLUMN IF EXISTS `tenant_id`; - --- 菜单表(如果之前添加了) --- ALTER TABLE `sys_menu` DROP INDEX IF EXISTS `idx_tenant_id`; --- ALTER TABLE `sys_menu` DROP COLUMN IF EXISTS `tenant_id`; +ALTER TABLE `ai_command_record` DROP INDEX `idx_tenant_id`; +ALTER TABLE `ai_command_record` DROP COLUMN `tenant_id`; -- ============================================ --- 4. 删除租户管理菜单和权限 +-- 3. 删除租户管理菜单和权限 -- ============================================ -- 删除角色菜单关联 DELETE FROM `sys_role_menu` WHERE `menu_id` IN (6, 141, 142, 143, 144, 145); @@ -76,36 +64,4 @@ DELETE FROM `sys_menu` WHERE `id` IN (141, 142, 143, 144, 145); -- 删除租户管理主菜单 DELETE FROM `sys_menu` WHERE `id` = 6; --- 恢复字典管理的排序(从7改回6) -UPDATE `sys_menu` SET `sort` = 6 WHERE `id` = 7 AND `sort` = 7; - --- 恢复字典项的排序(从8改回7) -UPDATE `sys_menu` SET `sort` = 7 WHERE `id` = 8 AND `sort` = 8; - --- 恢复系统日志的排序(从9改回8) -UPDATE `sys_menu` SET `sort` = 8 WHERE `id` = 9 AND `sort` = 9; - --- 恢复系统配置的排序(从10改回9) -UPDATE `sys_menu` SET `sort` = 9 WHERE `id` = 10 AND `sort` = 10; - --- 恢复通知公告的排序(从11改回10) -UPDATE `sys_menu` SET `sort` = 10 WHERE `id` = 11 AND `sort` = 11; - -SET FOREIGN_KEY_CHECKS = 1; - --- ============================================ --- 脚本执行完成 --- ============================================ --- 执行完成后,请执行以下操作: --- 1. 在 application.yml 中配置: --- youlai: --- tenant: --- enabled: false --- 2. 更新 BaseEntity.java,将 tenantId 字段的 exist 设置为 false --- 或移除 tenantId 字段(如果确定不再使用) --- ============================================ --- 注意: --- 1. MySQL 5.7 不支持 IF EXISTS 语法,如果执行报错,请手动检查字段是否存在 --- 2. 对于 MySQL 8.0+,可以使用上面的语法 --- 3. 如果使用 MySQL 5.7,请先检查字段是否存在,再执行删除操作 --- ============================================ +SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file diff --git a/sql/mysql/youlai_admin.sql b/sql/mysql/youlai_admin.sql index 9a50c036..423a091a 100644 --- a/sql/mysql/youlai_admin.sql +++ b/sql/mysql/youlai_admin.sql @@ -255,19 +255,6 @@ INSERT INTO `sys_menu` VALUES (913, 911, '0,9,910,911', '菜单三级-2', 'M', N -- 路由参数 INSERT INTO `sys_menu` VALUES (1001, 10, '0,10', '参数(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, 10, '0,10', '参数(type=2)', 'M', 'RouteParamType2', 'route-param-type2', 'demo/route-param', NULL, 0, 1, 1, 2, 'el-icon-StarFilled', NULL, now(), now(), '{\"type\": \"2\"}'); --- ============================================ ---- 系统配置权限按钮(ID: 901-905) ---- 字典项权限按钮(ID: 701-704) --- ============================================ --- 通知公告权限按钮(ID: 1101-1106) --- ============================================ --- ============================================ --- 字典项权限按钮(ID: 701-704) --- ============================================ --- ============================================ --- 租户管理权限按钮(ID: 501-505) --- ============================================ - -- ---------------------------- -- Table structure for sys_role @@ -353,6 +340,7 @@ INSERT INTO `sys_role_menu` VALUES (2, 1001), (2, 1002); DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, + `tenant_id` bigint DEFAULT 1 COMMENT '租户ID', `username` varchar(64) COMMENT '用户名', `nickname` varchar(64) COMMENT '昵称', `gender` tinyint(1) DEFAULT 1 COMMENT '性别((1-男 2-女 0-保密)', @@ -369,15 +357,16 @@ CREATE TABLE `sys_user` ( `is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)', `openid` char(28) COMMENT '微信 openid', PRIMARY KEY (`id`) USING BTREE, - KEY `login_name` (`username`) + UNIQUE KEY `uk_username_tenant` (`username`, `tenant_id`), + KEY `idx_tenant_id` (`tenant_id`) ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统用户表'; -- ---------------------------- -- Records of sys_user -- ---------------------------- -INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345677', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0,NULL); -INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345678', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0,NULL); -INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0,NULL); +INSERT INTO `sys_user` VALUES (1, 1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345677', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0,NULL); +INSERT INTO `sys_user` VALUES (2, 1, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345678', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0,NULL); +INSERT INTO `sys_user` VALUES (3, 1, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0,NULL); -- ---------------------------- -- Table structure for sys_user_role @@ -425,10 +414,10 @@ CREATE TABLE `sys_log` ( ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表'; -- ---------------------------- --- Table structure for gen_config +-- Table structure for gen_table -- ---------------------------- -DROP TABLE IF EXISTS `gen_config`; -CREATE TABLE `gen_config` ( +DROP TABLE IF EXISTS `gen_table`; +CREATE TABLE `gen_table` ( `id` bigint NOT NULL AUTO_INCREMENT, `table_name` varchar(100) NOT NULL COMMENT '表名', `module_name` varchar(100) COMMENT '模块名', @@ -447,12 +436,12 @@ CREATE TABLE `gen_config` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成配置表'; -- ---------------------------- --- Table structure for gen_field_config +-- Table structure for gen_table_column -- ---------------------------- -DROP TABLE IF EXISTS `gen_field_config`; -CREATE TABLE `gen_field_config` ( +DROP TABLE IF EXISTS `gen_table_column`; +CREATE TABLE `gen_table_column` ( `id` bigint NOT NULL AUTO_INCREMENT, - `config_id` bigint NOT NULL COMMENT '关联的配置ID', + `table_id` bigint NOT NULL COMMENT '关联的表配置ID', `column_name` varchar(100) , `column_type` varchar(50) , `column_length` int , @@ -471,7 +460,7 @@ CREATE TABLE `gen_field_config` ( `create_time` datetime COMMENT '创建时间', `update_time` datetime COMMENT '更新时间', PRIMARY KEY (`id`), - KEY `config_id` (`config_id`) + KEY `idx_table_id` (`table_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成字段配置表'; -- ---------------------------- @@ -559,60 +548,60 @@ INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0); -- ---------------------------- -- AI 命令记录表 -- ---------------------------- -DROP TABLE IF EXISTS `ai_command_log`; -CREATE TABLE `ai_command_log` ( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` bigint DEFAULT NULL COMMENT '用户ID', - `username` varchar(64) DEFAULT NULL COMMENT '用户名', - `original_command` text COMMENT '原始命令', - `ai_provider` varchar(32) DEFAULT NULL COMMENT 'AI 供应商(qwen/openai/deepseek/gemini等)', - `ai_model` varchar(64) DEFAULT NULL COMMENT 'AI 模型名称(qwen-plus/qwen-max/gpt-4-turbo等)', - `parse_status` tinyint DEFAULT '0' COMMENT '解析是否成功(0-失败, 1-成功)', - `function_calls` text COMMENT '解析出的函数调用列表(JSON)', - `explanation` varchar(500) DEFAULT NULL COMMENT 'AI的理解说明', - `confidence` decimal(3,2) DEFAULT NULL COMMENT '置信度(0.00-1.00)', - `parse_error_message` text COMMENT '解析错误信息', - `input_tokens` int DEFAULT NULL COMMENT '输入Token数量', - `output_tokens` int DEFAULT NULL COMMENT '输出Token数量', - `parse_duration_ms` int DEFAULT NULL COMMENT '解析耗时(毫秒)', - `function_name` varchar(255) DEFAULT NULL COMMENT '执行的函数名称', - `function_arguments` text COMMENT '函数参数(JSON)', - `execute_status` tinyint(1) DEFAULT NULL COMMENT '执行状态(0-待执行, 1-成功, -1-失败)', - `execute_error_message` text COMMENT '执行错误信息', - `ip_address` varchar(128) DEFAULT NULL COMMENT 'IP地址', - `create_time` datetime DEFAULT NULL COMMENT '创建时间', - `update_time` datetime DEFAULT NULL COMMENT '更新时间', - PRIMARY KEY (`id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_create_time` (`create_time`), - KEY `idx_provider` (`ai_provider`), - KEY `idx_model` (`ai_model`), - KEY `idx_parse_success` (`parse_status`), - KEY `idx_execute_status` (`execute_status`) -) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='AI 命令日志表'; +DROP TABLE IF EXISTS `ai_command_record`; +CREATE TABLE `ai_command_record` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `user_id` bigint DEFAULT NULL COMMENT '用户ID', + `username` varchar(64) DEFAULT NULL COMMENT '用户名', + `original_command` text COMMENT '原始命令', + `ai_provider` varchar(32) DEFAULT NULL COMMENT 'AI 供应商(qwen/openai/deepseek/gemini等)', + `ai_model` varchar(64) DEFAULT NULL COMMENT 'AI 模型名称(qwen-plus/qwen-max/gpt-4-turbo等)', + `parse_status` tinyint DEFAULT '0' COMMENT '解析是否成功(0-失败, 1-成功)', + `function_calls` text COMMENT '解析出的函数调用列表(JSON)', + `explanation` varchar(500) DEFAULT NULL COMMENT 'AI的理解说明', + `confidence` decimal(3,2) DEFAULT NULL COMMENT '置信度(0.00-1.00)', + `parse_error_message` text COMMENT '解析错误信息', + `input_tokens` int DEFAULT NULL COMMENT '输入Token数量', + `output_tokens` int DEFAULT NULL COMMENT '输出Token数量', + `parse_duration_ms` int DEFAULT NULL COMMENT '解析耗时(毫秒)', + `function_name` varchar(255) DEFAULT NULL COMMENT '执行的函数名称', + `function_arguments` text COMMENT '函数参数(JSON)', + `execute_status` tinyint(1) DEFAULT NULL COMMENT '执行状态(0-待执行, 1-成功, -1-失败)', + `execute_error_message` text COMMENT '执行错误信息', + `ip_address` varchar(128) DEFAULT NULL COMMENT 'IP地址', + `create_time` datetime DEFAULT NULL COMMENT '创建时间', + `update_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_create_time` (`create_time`), + KEY `idx_provider` (`ai_provider`), + KEY `idx_model` (`ai_model`), + KEY `idx_parse_success` (`parse_status`), + KEY `idx_execute_status` (`execute_status`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='AI 命令记录表'; -- ---------------------------- -- 租户表(多租户模式) -- ---------------------------- DROP TABLE IF EXISTS `sys_tenant`; CREATE TABLE `sys_tenant` ( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '租户ID', - `name` varchar(100) NOT NULL COMMENT '租户名称', - `code` varchar(50) NOT NULL COMMENT '租户编码(唯一)', - `contact_name` varchar(50) DEFAULT NULL COMMENT '联系人姓名', - `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系人电话', - `contact_email` varchar(100) DEFAULT NULL COMMENT '联系人邮箱', - `domain` varchar(100) DEFAULT NULL COMMENT '租户域名(用于域名识别)', - `logo` varchar(255) DEFAULT NULL COMMENT '租户Logo', - `status` tinyint DEFAULT '1' COMMENT '状态(1-正常 0-禁用)', - `remark` varchar(500) DEFAULT NULL COMMENT '备注', - `expire_time` datetime DEFAULT NULL COMMENT '过期时间(NULL表示永不过期)', - `create_time` datetime COMMENT '创建时间', - `update_time` datetime COMMENT '更新时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_code` (`code`), - UNIQUE KEY `uk_domain` (`domain`), - KEY `idx_status` (`status`) + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '租户ID', + `name` varchar(100) NOT NULL COMMENT '租户名称', + `code` varchar(50) NOT NULL COMMENT '租户编码(唯一)', + `contact_name` varchar(50) DEFAULT NULL COMMENT '联系人姓名', + `contact_phone` varchar(20) DEFAULT NULL COMMENT '联系人电话', + `contact_email` varchar(100) DEFAULT NULL COMMENT '联系人邮箱', + `domain` varchar(100) DEFAULT NULL COMMENT '租户域名(用于域名识别)', + `logo` varchar(255) DEFAULT NULL COMMENT '租户Logo', + `status` tinyint DEFAULT '1' COMMENT '状态(1-正常 0-禁用)', + `remark` varchar(500) DEFAULT NULL COMMENT '备注', + `expire_time` datetime DEFAULT NULL COMMENT '过期时间(NULL表示永不过期)', + `create_time` datetime COMMENT '创建时间', + `update_time` datetime COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_code` (`code`), + UNIQUE KEY `uk_domain` (`domain`), + KEY `idx_status` (`status`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统租户表'; -- ---------------------------- @@ -621,28 +610,6 @@ CREATE TABLE `sys_tenant` ( INSERT INTO `sys_tenant` VALUES (1, '默认租户', 'DEFAULT', '系统管理员', '18812345678', 'admin@youlai.tech', NULL, NULL, 1, '系统默认租户', NULL, now(), now()); INSERT INTO `sys_tenant` VALUES (2, '演示租户', 'DEMO', '演示用户', '18812345679', 'demo@youlai.tech', 'demo.youlai.tech', NULL, 1, '演示租户', NULL, now(), now()); --- ---------------------------- --- 用户租户关联表(多租户模式) --- ---------------------------- -DROP TABLE IF EXISTS `sys_user_tenant`; -CREATE TABLE `sys_user_tenant` ( - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', - `user_id` bigint NOT NULL COMMENT '用户ID', - `tenant_id` bigint NOT NULL COMMENT '租户ID', - `is_default` tinyint DEFAULT '0' COMMENT '是否默认租户(1-是 0-否)', - `create_time` datetime COMMENT '创建时间', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_user_tenant` (`user_id`, `tenant_id`), - KEY `idx_user_id` (`user_id`), - KEY `idx_tenant_id` (`tenant_id`) -) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户租户关联表(多租户模式)'; - --- ---------------------------- --- Records of sys_user_tenant --- ---------------------------- -INSERT INTO `sys_user_tenant` VALUES (1, 1, 1, 1, now()); -INSERT INTO `sys_user_tenant` VALUES (2, 2, 1, 1, now()); -INSERT INTO `sys_user_tenant` VALUES (3, 2, 2, 0, now()); SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/java/com/youlai/boot/auth/controller/AuthController.java b/src/main/java/com/youlai/boot/auth/controller/AuthController.java index e4d169fc..ddfe2773 100644 --- a/src/main/java/com/youlai/boot/auth/controller/AuthController.java +++ b/src/main/java/com/youlai/boot/auth/controller/AuthController.java @@ -1,21 +1,33 @@ package com.youlai.boot.auth.controller; import com.youlai.boot.auth.model.vo.CaptchaVO; +import com.youlai.boot.auth.model.vo.ChooseTenantVO; +import com.youlai.boot.auth.model.dto.LoginRequest; import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO; import com.youlai.boot.common.enums.LogModuleEnum; +import com.youlai.boot.config.property.TenantProperties; import com.youlai.boot.core.web.Result; import com.youlai.boot.auth.service.AuthService; import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO; import com.youlai.boot.common.annotation.Log; +import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.security.model.AuthenticationToken; +import com.youlai.boot.system.model.entity.User; +import com.youlai.boot.system.model.vo.TenantVO; +import com.youlai.boot.system.service.TenantService; +import com.youlai.boot.system.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; +import java.util.List; +import java.util.stream.Collectors; + /** * 认证控制层 @@ -31,6 +43,10 @@ import org.springframework.web.bind.annotation.*; public class AuthController { private final AuthService authService; + private final UserService userService; + private final TenantService tenantService; + private final TenantProperties tenantProperties; + private final PasswordEncoder passwordEncoder; @Operation(summary = "获取验证码") @GetMapping("/captcha") @@ -42,12 +58,62 @@ public class AuthController { @Operation(summary = "账号密码登录") @PostMapping("/login") @Log(value = "登录", module = LogModuleEnum.LOGIN) - public Result login( - @Parameter(description = "用户名", example = "admin") @RequestParam String username, - @Parameter(description = "密码", example = "123456") @RequestParam String password - ) { - AuthenticationToken authenticationToken = authService.login(username, password); - return Result.success(authenticationToken); + public Result login(@RequestBody @Valid LoginRequest request) { + String username = request.getUsername(); + String password = request.getPassword(); + Long tenantId = request.getTenantId(); + + // 如果未启用多租户,直接登录 + if (tenantProperties == null || !Boolean.TRUE.equals(tenantProperties.getEnabled())) { + AuthenticationToken authenticationToken = authService.login(username, password, null); + return Result.success(authenticationToken); + } + + // 多租户模式:如果指定了租户ID,直接验证该租户下的密码 + if (tenantId != null) { + AuthenticationToken authenticationToken = authService.login(username, password, tenantId); + return Result.success(authenticationToken); + } + + // 多租户模式:未指定租户ID,查询该用户名在所有租户下的记录 + List users = userService.listUsersByUsername(username); + + if (users.isEmpty()) { + return Result.failed("用户不存在"); + } + + // 过滤出正常状态的用户 + List activeUsers = users.stream() + .filter(user -> user.getStatus() != null && user.getStatus() == 1) + .toList(); + + if (activeUsers.isEmpty()) { + return Result.failed("用户已被禁用"); + } + + // 如果只有1个租户,尝试验证该租户下的密码(兼容性) + if (activeUsers.size() == 1) { + User user = activeUsers.get(0); + // 登录(Spring Security 会验证密码) + AuthenticationToken authenticationToken = authService.login(username, password, user.getTenantId()); + return Result.success(authenticationToken); + } + + // 如果多个租户,返回 choose_tenant 响应(含 tenants 列表) + // 注意:此时不验证密码,直接返回租户列表让用户选择 + List tenants = activeUsers.stream() + .map(user -> tenantService.getTenantById(user.getTenantId())) + .filter(tenant -> tenant != null && (tenant.getStatus() == null || tenant.getStatus() == 1)) + .distinct() // 去重(理论上不会有重复,但保险起见) + .collect(Collectors.toList()); + + if (tenants.isEmpty()) { + return Result.failed("用户所属的租户均不可用"); + } + + // 返回 choose_tenant 响应 + ChooseTenantVO chooseTenantVO = new ChooseTenantVO(tenants); + return Result.failed(ResultCode.CHOOSE_TENANT, chooseTenantVO); } @Operation(summary = "短信验证码登录") diff --git a/src/main/java/com/youlai/boot/auth/model/dto/LoginRequest.java b/src/main/java/com/youlai/boot/auth/model/dto/LoginRequest.java new file mode 100644 index 00000000..de52a3cc --- /dev/null +++ b/src/main/java/com/youlai/boot/auth/model/dto/LoginRequest.java @@ -0,0 +1,35 @@ +package com.youlai.boot.auth.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; + +/** + * 登录请求参数 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Schema(description = "登录请求参数") +@Data +public class LoginRequest { + + @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin") + @NotBlank(message = "用户名不能为空") + private String username; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotBlank(message = "密码不能为空") + private String password; + + @Schema(description = "验证码缓存ID", example = "captcha_id_123") + private String captchaId; + + @Schema(description = "验证码", example = "1234") + private String captchaCode; + + @Schema(description = "租户ID(可选,多租户模式下用于指定租户)", example = "1") + private Long tenantId; +} + diff --git a/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java b/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java index 3f42c300..a81fb3c6 100644 --- a/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java +++ b/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java @@ -15,8 +15,8 @@ import lombok.Data; @Builder public class CaptchaVO { - @Schema(description = "验证码缓存 Key") - private String captchaKey; + @Schema(description = "验证码缓存 ID") + private String captchaId; @Schema(description = "验证码图片Base64字符串") private String captchaBase64; diff --git a/src/main/java/com/youlai/boot/auth/model/vo/ChooseTenantVO.java b/src/main/java/com/youlai/boot/auth/model/vo/ChooseTenantVO.java new file mode 100644 index 00000000..15fdfdda --- /dev/null +++ b/src/main/java/com/youlai/boot/auth/model/vo/ChooseTenantVO.java @@ -0,0 +1,27 @@ +package com.youlai.boot.auth.model.vo; + +import com.youlai.boot.system.model.vo.TenantVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + * 选择租户响应VO + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "选择租户响应") +public class ChooseTenantVO implements Serializable { + + @Schema(description = "租户列表") + private List tenants; +} + diff --git a/src/main/java/com/youlai/boot/auth/service/AuthService.java b/src/main/java/com/youlai/boot/auth/service/AuthService.java index 5fe2eadf..2adaf581 100644 --- a/src/main/java/com/youlai/boot/auth/service/AuthService.java +++ b/src/main/java/com/youlai/boot/auth/service/AuthService.java @@ -18,9 +18,10 @@ public interface AuthService { * * @param username 用户名 * @param password 密码 + * @param tenantId 租户ID(可选,多租户模式下用于指定租户) * @return 登录结果 */ - AuthenticationToken login(String username, String password); + AuthenticationToken login(String username, String password, Long tenantId); /** * 登出 diff --git a/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java index 259a26a5..1d740de1 100644 --- a/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java @@ -21,6 +21,7 @@ import com.youlai.boot.security.model.WxMiniAppCodeAuthenticationToken; import com.youlai.boot.security.model.WxMiniAppPhoneAuthenticationToken; import com.youlai.boot.security.token.TokenManager; import com.youlai.boot.security.util.SecurityUtils; +import com.youlai.boot.common.tenant.TenantContextHolder; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; @@ -61,10 +62,16 @@ public class AuthServiceImpl implements AuthService { * * @param username 用户名 * @param password 密码 + * @param tenantId 租户ID(可选,多租户模式下用于指定租户) * @return 访问令牌 */ @Override - public AuthenticationToken login(String username, String password) { + public AuthenticationToken login(String username, String password, Long tenantId) { + // 如果指定了租户ID,需要先设置租户上下文,以便查询该租户下的用户 + if (tenantId != null) { + com.youlai.boot.common.tenant.TenantContextHolder.setTenantId(tenantId); + } + // 1. 创建用于密码认证的令牌(未认证) UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username.trim(), password); @@ -194,16 +201,16 @@ public class AuthServiceImpl implements AuthService { String imageBase64Data = captcha.getImageBase64Data(); // 验证码文本缓存至Redis,用于登录校验 - String captchaKey = IdUtil.fastSimpleUUID(); + String captchaId = IdUtil.fastSimpleUUID(); redisTemplate.opsForValue().set( - StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaKey), + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId), captchaCode, captchaProperties.getExpireSeconds(), TimeUnit.SECONDS ); return CaptchaVO.builder() - .captchaKey(captchaKey) + .captchaId(captchaId) .captchaBase64(imageBase64Data) .build(); } diff --git a/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java b/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java index c0a84a94..4e2b3532 100644 --- a/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java +++ b/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java @@ -35,6 +35,11 @@ public interface JwtClaimConstants { */ String AUTHORITIES = "authorities"; + /** + * 租户ID + */ + String TENANT_ID = "tenantId"; + /** * 安全版本号,用于按用户失效历史令牌 */ diff --git a/src/main/java/com/youlai/boot/config/MybatisConfig.java b/src/main/java/com/youlai/boot/config/MybatisConfig.java index 6cca57d5..2a6166b0 100644 --- a/src/main/java/com/youlai/boot/config/MybatisConfig.java +++ b/src/main/java/com/youlai/boot/config/MybatisConfig.java @@ -9,12 +9,11 @@ import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerIntercept import com.youlai.boot.config.property.TenantProperties; import com.youlai.boot.plugin.mybatis.MyDataPermissionHandler; import com.youlai.boot.plugin.mybatis.MyMetaObjectHandler; -import com.youlai.boot.plugin.mybatis.TenantLineHandler; +import com.youlai.boot.plugin.mybatis.MyTenantLineHandler; import org.apache.ibatis.mapping.DatabaseIdProvider; import org.apache.ibatis.mapping.VendorDatabaseIdProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; @@ -35,7 +34,7 @@ public class MybatisConfig { private String dbType; @Autowired(required = false) - private TenantLineHandler tenantLineHandler; + private MyTenantLineHandler myTenantLineHandler; @Autowired(required = false) private TenantProperties tenantProperties; @@ -51,8 +50,8 @@ public class MybatisConfig { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 多租户插件(如果启用,必须在最前面) - if (tenantProperties != null && Boolean.TRUE.equals(tenantProperties.getEnabled()) && tenantLineHandler != null) { - interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantLineHandler)); + if (tenantProperties != null && Boolean.TRUE.equals(tenantProperties.getEnabled()) && myTenantLineHandler != null) { + interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(myTenantLineHandler)); } // 数据权限 diff --git a/src/main/java/com/youlai/boot/config/WebMvcConfig.java b/src/main/java/com/youlai/boot/config/WebMvcConfig.java index fae163f3..a8082ea9 100644 --- a/src/main/java/com/youlai/boot/config/WebMvcConfig.java +++ b/src/main/java/com/youlai/boot/config/WebMvcConfig.java @@ -7,21 +7,17 @@ import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; -import com.youlai.boot.core.interceptor.TenantValidationInterceptor; import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hibernate.validator.HibernateValidator; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.math.BigInteger; @@ -43,9 +39,6 @@ public class WebMvcConfig implements WebMvcConfigurer { private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - @Autowired(required = false) - private TenantValidationInterceptor tenantValidationInterceptor; - /** * 配置消息转换器 * @@ -85,22 +78,6 @@ public class WebMvcConfig implements WebMvcConfigurer { * @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory * @return Validator 实例 */ - /** - * 配置拦截器 - * - * @param registry 拦截器注册器 - */ - @Override - public void addInterceptors(InterceptorRegistry registry) { - // 注册租户校验拦截器(仅在多租户模式启用时生效) - if (tenantValidationInterceptor != null) { - registry.addInterceptor(tenantValidationInterceptor) - .addPathPatterns("/api/**") - .order(2); // 在认证拦截器之后执行 - log.info("租户校验拦截器已注册"); - } - } - @Bean public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) { try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) diff --git a/src/main/java/com/youlai/boot/config/property/TenantProperties.java b/src/main/java/com/youlai/boot/config/property/TenantProperties.java index 460c2068..66c9c04e 100644 --- a/src/main/java/com/youlai/boot/config/property/TenantProperties.java +++ b/src/main/java/com/youlai/boot/config/property/TenantProperties.java @@ -57,6 +57,9 @@ public class TenantProperties { ignoreTables.add("sys_dict"); ignoreTables.add("sys_dict_item"); ignoreTables.add("sys_config"); + // 代码生成表(平台共用,不做租户隔离) + ignoreTables.add("gen_table"); + ignoreTables.add("gen_table_column"); } } diff --git a/src/main/java/com/youlai/boot/core/filter/TenantContextFilter.java b/src/main/java/com/youlai/boot/core/filter/TenantContextFilter.java index 9e53282d..667e2b43 100644 --- a/src/main/java/com/youlai/boot/core/filter/TenantContextFilter.java +++ b/src/main/java/com/youlai/boot/core/filter/TenantContextFilter.java @@ -1,7 +1,10 @@ package com.youlai.boot.core.filter; +import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.tenant.TenantContextHolder; import com.youlai.boot.config.property.TenantProperties; +import com.youlai.boot.security.model.SysUserDetails; +import com.youlai.boot.security.token.TokenManager; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -10,6 +13,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.core.annotation.Order; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; @@ -34,39 +39,59 @@ import java.io.IOException; public class TenantContextFilter extends OncePerRequestFilter { private final TenantProperties tenantProperties; + private final TokenManager tokenManager; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { - // 从请求头获取租户ID - String tenantIdStr = request.getHeader(tenantProperties.getHeaderName()); + // 1) 优先从已认证用户中获取租户ID + Long tenantId = resolveTenantFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); - if (StringUtils.hasText(tenantIdStr)) { - try { - Long tenantId = Long.parseLong(tenantIdStr); - TenantContextHolder.setTenantId(tenantId); - log.debug("从请求头获取租户ID: {}", tenantId); - } catch (NumberFormatException e) { - log.warn("租户ID格式错误: {}", tenantIdStr); - } - } else { - // 如果未提供租户ID,使用默认租户ID + // 2) 如果尚未获取到,尝试从 Token 中解析 + if (tenantId == null) { + tenantId = resolveTenantFromToken(request); + } + + // 3) 仍为空则使用默认租户 + if (tenantId == null) { Long defaultTenantId = tenantProperties.getDefaultTenantId(); if (defaultTenantId != null) { - TenantContextHolder.setTenantId(defaultTenantId); - log.debug("使用默认租户ID: {}", defaultTenantId); + tenantId = defaultTenantId; } } - // 继续执行过滤器链 - filterChain.doFilter(request, response); + if (tenantId != null) { + TenantContextHolder.setTenantId(tenantId); + log.debug("TenantContextFilter set tenantId: {}", tenantId); + } + filterChain.doFilter(request, response); } finally { - // 请求结束时清除租户上下文,避免线程池复用导致的数据泄露 TenantContextHolder.clear(); } } + + private Long resolveTenantFromAuthentication(Authentication authentication) { + if (authentication == null) { + return null; + } + Object principal = authentication.getPrincipal(); + if (principal instanceof SysUserDetails details) { + return details.getTenantId(); + } + return null; + } + + private Long resolveTenantFromToken(HttpServletRequest request) { + String authHeader = request.getHeader("Authorization"); + if (!StringUtils.hasText(authHeader) || !authHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { + return null; + } + String token = authHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); + Authentication authentication = tokenManager.parseToken(token); + return resolveTenantFromAuthentication(authentication); + } } diff --git a/src/main/java/com/youlai/boot/core/interceptor/TenantValidationInterceptor.java b/src/main/java/com/youlai/boot/core/interceptor/TenantValidationInterceptor.java deleted file mode 100644 index 7ef57da7..00000000 --- a/src/main/java/com/youlai/boot/core/interceptor/TenantValidationInterceptor.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.youlai.boot.core.interceptor; - -import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.common.tenant.TenantContextHolder; -import com.youlai.boot.config.property.TenantProperties; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - -/** - * 租户ID强制校验拦截器 - *

- * 对于需要租户隔离的接口,强制要求携带有效的租户ID - * 防止恶意用户通过不携带租户ID来访问默认租户数据 - *

- * - * @author Ray.Hao - * @since 3.0.0 - */ -@Slf4j -@Component -@RequiredArgsConstructor -@ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true") -public class TenantValidationInterceptor implements HandlerInterceptor { - - private final TenantProperties tenantProperties; - - /** - * 白名单路径:这些路径不需要租户ID校验 - */ - private static final List WHITELIST_PATHS = Arrays.asList( - "/api/v1/auth/login", - "/api/v1/auth/logout", - "/api/v1/auth/captcha", - "/api/v1/tenant/list", - "/doc.html", - "/v3/api-docs", - "/swagger-ui", - "/favicon.ico", - "/error" - ); - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { - String requestPath = request.getRequestURI(); - - // 检查是否在白名单中 - if (isWhitelistPath(requestPath)) { - return true; - } - - // 检查租户ID是否存在 - Long tenantId = TenantContextHolder.getTenantId(); - if (tenantId == null) { - log.warn("请求路径 {} 缺少租户ID", requestPath); - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write(String.format( - "{\"code\":\"%s\",\"msg\":\"租户ID不能为空,请联系管理员\"}", - ResultCode.BAD_REQUEST.getCode() - )); - return false; - } - - // 可选:校验租户是否有效(需要注入 TenantService) - // 这里暂时只校验租户ID不为空 - - return true; - } - - /** - * 检查路径是否在白名单中 - */ - private boolean isWhitelistPath(String requestPath) { - return WHITELIST_PATHS.stream() - .anyMatch(requestPath::startsWith); - } -} diff --git a/src/main/java/com/youlai/boot/core/web/Result.java b/src/main/java/com/youlai/boot/core/web/Result.java index 783be757..6e4e6ac6 100644 --- a/src/main/java/com/youlai/boot/core/web/Result.java +++ b/src/main/java/com/youlai/boot/core/web/Result.java @@ -56,6 +56,14 @@ public class Result implements Serializable { return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), null); } + public static Result failed(IResultCode resultCode, T data) { + return result(resultCode.getCode(), resultCode.getMsg(), data); + } + + public static Result failed(IResultCode resultCode, String msg, T data) { + return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), data); + } + private static Result result(IResultCode resultCode, T data) { return result(resultCode.getCode(), resultCode.getMsg(), data); } diff --git a/src/main/java/com/youlai/boot/core/web/ResultCode.java b/src/main/java/com/youlai/boot/core/web/ResultCode.java index d7ea897b..8ae2a22e 100644 --- a/src/main/java/com/youlai/boot/core/web/ResultCode.java +++ b/src/main/java/com/youlai/boot/core/web/ResultCode.java @@ -76,6 +76,9 @@ public enum ResultCode implements IResultCode, Serializable { USER_VERIFICATION_CODE_ATTEMPT_LIMIT_EXCEEDED("A0241", "用户验证码尝试次数超限"), USER_VERIFICATION_CODE_EXPIRED("A0242", "用户验证码过期"), + // 多租户登录 + CHOOSE_TENANT("A0250", "请选择登录租户"), + /** 二级宏观错误码 */ ACCESS_PERMISSION_EXCEPTION("A0300", "访问权限异常"), ACCESS_UNAUTHORIZED("A0301", "访问未授权"), diff --git a/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java b/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java index f64b833f..464dfc19 100644 --- a/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java +++ b/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java @@ -7,8 +7,8 @@ import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDTO; import com.youlai.boot.platform.ai.model.dto.AiParseRequestDTO; import com.youlai.boot.platform.ai.model.dto.AiParseResponseDTO; import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery; -import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO; -import com.youlai.boot.platform.ai.service.AiCommandLogService; +import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO; +import com.youlai.boot.platform.ai.service.AiCommandRecordService; import com.youlai.boot.platform.ai.service.AiCommandService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -32,7 +32,7 @@ import org.springframework.web.bind.annotation.*; public class AiCommandController { private final AiCommandService aiCommandService; - private final AiCommandLogService logService; + private final AiCommandRecordService recordService; @Operation(summary = "解析自然语言命令") @PostMapping("/parse") @@ -72,17 +72,17 @@ public class AiCommandController { @Operation(summary = "获取AI命令记录分页列表") @GetMapping("/records") - public PageResult getLogPage(AiCommandPageQuery queryParams) { - IPage page = logService.getLogPage(queryParams); + public PageResult getRecordPage(AiCommandPageQuery queryParams) { + IPage page = recordService.getRecordPage(queryParams); return PageResult.success(page); } @Operation(summary = "撤销命令执行") - @PostMapping("/rollback/{logId}") + @PostMapping("/rollback/{recordId}") public Result rollbackCommand( - @Parameter(description = "记录ID") @PathVariable String logId + @Parameter(description = "记录ID") @PathVariable String recordId ) { - logService.rollbackCommand(logId); + recordService.rollbackCommand(recordId); return Result.success("撤销成功"); } diff --git a/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandLogMapper.java b/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandRecordMapper.java similarity index 60% rename from src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandLogMapper.java rename to src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandRecordMapper.java index 90bb3f32..e26bb6c6 100644 --- a/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandLogMapper.java +++ b/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandRecordMapper.java @@ -3,9 +3,9 @@ package com.youlai.boot.platform.ai.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.youlai.boot.platform.ai.model.entity.AiCommandLog; +import com.youlai.boot.platform.ai.model.entity.AiCommandRecord; import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery; -import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO; +import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO; import org.apache.ibatis.annotations.Mapper; /** @@ -15,12 +15,12 @@ import org.apache.ibatis.annotations.Mapper; * @since 3.0.0 */ @Mapper -public interface AiCommandLogMapper extends BaseMapper { +public interface AiCommandRecordMapper extends BaseMapper { /** * 获取 AI 命令记录分页列表 */ - IPage getLogPage(Page page, AiCommandPageQuery queryParams); + IPage getRecordPage(Page page, AiCommandPageQuery queryParams); } diff --git a/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandLog.java b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandRecord.java similarity index 95% rename from src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandLog.java rename to src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandRecord.java index 9a55687f..1750970b 100644 --- a/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandLog.java +++ b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandRecord.java @@ -15,8 +15,8 @@ import java.math.BigDecimal; */ @Data @EqualsAndHashCode(callSuper = true) -@TableName("ai_command_log") -public class AiCommandLog extends BaseEntity { +@TableName("ai_command_record") +public class AiCommandRecord extends BaseEntity { /** 用户ID */ private Long userId; diff --git a/src/main/java/com/youlai/boot/platform/ai/model/query/AiParseLogPageQuery.java b/src/main/java/com/youlai/boot/platform/ai/model/query/AiParseLogPageQuery.java deleted file mode 100644 index 065de26e..00000000 --- a/src/main/java/com/youlai/boot/platform/ai/model/query/AiParseLogPageQuery.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.youlai.boot.platform.ai.model.query; - -import com.youlai.boot.common.base.BasePageQuery; -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Getter; -import lombok.Setter; - -import java.util.List; - -/** - * AI命令解析日志分页查询对象 - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Schema(description = "AI命令解析日志分页查询对象") -@Getter -@Setter -public class AiParseLogPageQuery extends BasePageQuery { - - @Schema(description = "关键字(原始命令/用户名)") - private String keywords; - - @Schema(description = "解析是否成功(0-失败, 1-成功)") - private Boolean parseSuccess; - - @Schema(description = "用户ID") - private Long userId; - - @Schema(description = "AI提供商(qwen/openai/deepseek/gemini等)") - private String provider; - - @Schema(description = "AI模型(qwen-plus/qwen-max/gpt-4-turbo等)") - private String model; - - @Schema(description = "创建时间范围") - private List createTime; -} - diff --git a/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandLogVO.java b/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandRecordVO.java similarity index 97% rename from src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandLogVO.java rename to src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandRecordVO.java index b35d3745..088c5f9c 100644 --- a/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandLogVO.java +++ b/src/main/java/com/youlai/boot/platform/ai/model/vo/AiCommandRecordVO.java @@ -16,7 +16,7 @@ import java.time.LocalDateTime; */ @Data @Schema(description = "AI命令记录VO") -public class AiCommandLogVO implements Serializable { +public class AiCommandRecordVO implements Serializable { @Schema(description = "主键ID") private String id; @@ -90,4 +90,3 @@ public class AiCommandLogVO implements Serializable { private LocalDateTime updateTime; } - diff --git a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandLogService.java b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandRecordService.java similarity index 66% rename from src/main/java/com/youlai/boot/platform/ai/service/AiCommandLogService.java rename to src/main/java/com/youlai/boot/platform/ai/service/AiCommandRecordService.java index 5b4bea19..5e94c87d 100644 --- a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandLogService.java +++ b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandRecordService.java @@ -2,9 +2,9 @@ package com.youlai.boot.platform.ai.service; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.IService; -import com.youlai.boot.platform.ai.model.entity.AiCommandLog; +import com.youlai.boot.platform.ai.model.entity.AiCommandRecord; import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery; -import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO; +import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO; /** * AI 命令记录服务接口 @@ -12,7 +12,7 @@ import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO; * @author Ray.Hao * @since 3.0.0 */ -public interface AiCommandLogService extends IService { +public interface AiCommandRecordService extends IService { /** * 获取命令记录分页列表 @@ -20,7 +20,7 @@ public interface AiCommandLogService extends IService { * @param queryParams 查询参数 * @return 命令记录分页列表 */ - IPage getLogPage(AiCommandPageQuery queryParams); + IPage getRecordPage(AiCommandPageQuery queryParams); /** * 撤销命令执行 diff --git a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandLogServiceImpl.java b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandRecordServiceImpl.java similarity index 52% rename from src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandLogServiceImpl.java rename to src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandRecordServiceImpl.java index 05ec8f4f..33df604c 100644 --- a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandLogServiceImpl.java +++ b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandRecordServiceImpl.java @@ -3,11 +3,11 @@ package com.youlai.boot.platform.ai.service.impl; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.youlai.boot.platform.ai.mapper.AiCommandLogMapper; -import com.youlai.boot.platform.ai.model.entity.AiCommandLog; +import com.youlai.boot.platform.ai.mapper.AiCommandRecordMapper; +import com.youlai.boot.platform.ai.model.entity.AiCommandRecord; import com.youlai.boot.platform.ai.model.query.AiCommandPageQuery; -import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO; -import com.youlai.boot.platform.ai.service.AiCommandLogService; +import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO; +import com.youlai.boot.platform.ai.service.AiCommandRecordService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -21,28 +21,28 @@ import org.springframework.stereotype.Service; @Service @Slf4j @RequiredArgsConstructor -public class AiCommandLogServiceImpl extends ServiceImpl - implements AiCommandLogService { +public class AiCommandRecordServiceImpl extends ServiceImpl + implements AiCommandRecordService { @Override - public IPage getLogPage(AiCommandPageQuery queryParams) { - Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); - return this.baseMapper.getLogPage(page, queryParams); + public IPage getRecordPage(AiCommandPageQuery queryParams) { + Page page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); + return this.baseMapper.getRecordPage(page, queryParams); } @Override public void rollbackCommand(String logId) { - AiCommandLog commandLog = this.getById(logId); - if (commandLog == null) { + AiCommandRecord commandRecord = this.getById(logId); + if (commandRecord == null) { throw new RuntimeException("命令记录不存在"); } - if (commandLog.getExecuteStatus() == null || commandLog.getExecuteStatus() != 1) { + if (commandRecord.getExecuteStatus() == null || commandRecord.getExecuteStatus() != 1) { throw new RuntimeException("只能撤销成功执行的命令"); } // TODO: 实现具体的回滚逻辑 - log.info("撤销命令执行: logId={}, function={}", logId, commandLog.getFunctionName()); + log.info("撤销命令执行: logId={}, function={}", logId, commandRecord.getFunctionName()); throw new UnsupportedOperationException("回滚功能尚未实现"); } } diff --git a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java index 3be3ed68..c4b76980 100644 --- a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java +++ b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java @@ -10,8 +10,8 @@ import com.youlai.boot.platform.ai.model.dto.AiExecuteRequestDTO; import com.youlai.boot.platform.ai.model.dto.AiFunctionCallDTO; import com.youlai.boot.platform.ai.model.dto.AiParseRequestDTO; import com.youlai.boot.platform.ai.model.dto.AiParseResponseDTO; -import com.youlai.boot.platform.ai.model.entity.AiCommandLog; -import com.youlai.boot.platform.ai.service.AiCommandLogService; +import com.youlai.boot.platform.ai.model.entity.AiCommandRecord; +import com.youlai.boot.platform.ai.service.AiCommandRecordService; import com.youlai.boot.platform.ai.service.AiCommandService; import com.youlai.boot.platform.ai.tools.UserTools; import com.youlai.boot.security.util.SecurityUtils; @@ -50,7 +50,7 @@ public class AiCommandServiceImpl implements AiCommandService { 当无法识别命令时,success=false,并给出 error。 """; - private final AiCommandLogService logService; + private final AiCommandRecordService recordService; private final UserTools userTools; private final ChatClient chatClient; @@ -71,13 +71,13 @@ public class AiCommandServiceImpl implements AiCommandService { String username = SecurityUtils.getUsername(); String ipAddress = JakartaServletUtil.getClientIP(httpRequest); - AiCommandLog commandLog = new AiCommandLog(); - commandLog.setUserId(userId); - commandLog.setUsername(username); - commandLog.setOriginalCommand(command); - commandLog.setIpAddress(ipAddress); - commandLog.setAiProvider("spring-ai"); - commandLog.setAiModel("auto"); + AiCommandRecord commandRecord = new AiCommandRecord(); + commandRecord.setUserId(userId); + commandRecord.setUsername(username); + commandRecord.setOriginalCommand(command); + commandRecord.setIpAddress(ipAddress); + commandRecord.setAiProvider("spring-ai"); + commandRecord.setAiModel("auto"); String systemPrompt = buildSystemPrompt(); String userPrompt = buildUserPrompt(request); @@ -95,20 +95,20 @@ public class AiCommandServiceImpl implements AiCommandService { ParseResult parseResult = parseAiResponse(rawContent); - commandLog.setAiProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai")); - commandLog.setAiModel(StrUtil.emptyToDefault(parseResult.model(), "auto")); - commandLog.setParseStatus(parseResult.success() ? 1 : 0); - commandLog.setExplanation(parseResult.explanation()); - commandLog.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls())); - commandLog.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null); - commandLog.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败")); + commandRecord.setAiProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai")); + commandRecord.setAiModel(StrUtil.emptyToDefault(parseResult.model(), "auto")); + commandRecord.setParseStatus(parseResult.success() ? 1 : 0); + commandRecord.setExplanation(parseResult.explanation()); + commandRecord.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls())); + commandRecord.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null); + commandRecord.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败")); long duration = System.currentTimeMillis() - startTime; - commandLog.setParseDurationMs((int) duration); + commandRecord.setParseDurationMs((int) duration); - logService.save(commandLog); + recordService.save(commandRecord); AiParseResponseDTO response = AiParseResponseDTO.builder() - .parseLogId(commandLog.getId()) + .parseLogId(commandRecord.getId()) .success(parseResult.success()) .functionCalls(parseResult.functionCalls()) .explanation(parseResult.explanation()) @@ -120,17 +120,17 @@ public class AiCommandServiceImpl implements AiCommandService { if (!parseResult.success()) { log.warn("❗️ AI 未能解析命令: {}", parseResult.error()); } else { - log.info("✅ 解析成功,审计记录ID: {}", commandLog.getId()); + log.info("✅ 解析成功,审计记录ID: {}", commandRecord.getId()); } return response; } catch (Exception e) { long duration = System.currentTimeMillis() - startTime; - commandLog.setParseStatus(0); - commandLog.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList())); - commandLog.setParseErrorMessage(e.getMessage()); - commandLog.setParseDurationMs((int) duration); - logService.save(commandLog); + commandRecord.setParseStatus(0); + commandRecord.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList())); + commandRecord.setParseErrorMessage(e.getMessage()); + commandRecord.setParseDurationMs((int) duration); + recordService.save(commandRecord); log.error("❌ 解析命令失败: {}", e.getMessage(), e); throw new RuntimeException("解析命令失败: " + e.getMessage(), e); @@ -232,52 +232,52 @@ public class AiCommandServiceImpl implements AiCommandService { AiFunctionCallDTO functionCall = request.getFunctionCall(); // 根据解析日志ID获取审计记录,如果不存在则创建新记录 - AiCommandLog commandLog; + AiCommandRecord commandRecord ; if (StrUtil.isNotBlank(request.getParseLogId())) { // 更新已存在的审计记录(解析阶段已创建) - commandLog = logService.getById(request.getParseLogId()); - if (commandLog == null) { + commandRecord = recordService.getById(request.getParseLogId()); + if (commandRecord == null) { throw new IllegalStateException("未找到对应的解析记录,ID: " + request.getParseLogId()); } } else { // 如果没有解析日志ID,创建新记录(兼容直接执行的情况) - commandLog = new AiCommandLog(); - commandLog.setUserId(userId); - commandLog.setUsername(username); - commandLog.setOriginalCommand(request.getOriginalCommand()); - commandLog.setIpAddress(ipAddress); - logService.save(commandLog); + commandRecord = new AiCommandRecord(); + commandRecord.setUserId(userId); + commandRecord.setUsername(username); + commandRecord.setOriginalCommand(request.getOriginalCommand()); + commandRecord.setIpAddress(ipAddress); + recordService.save(commandRecord); } // 更新执行相关字段 - commandLog.setFunctionName(functionCall.getName()); - commandLog.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments())); - commandLog.setExecuteStatus(0); // 0-待执行 + commandRecord.setFunctionName(functionCall.getName()); + commandRecord.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments())); + commandRecord.setExecuteStatus(0); // 0-待执行 try { // 🎯 执行具体的函数调用 Object result = executeFunctionCall(functionCall); // 更新执行成功 - commandLog.setExecuteStatus(1); // 1-成功 - commandLog.setExecuteErrorMessage(null); + commandRecord.setExecuteStatus(1); // 1-成功 + commandRecord.setExecuteErrorMessage(null); // 更新审计记录 - logService.updateById(commandLog); + recordService.updateById(commandRecord); - log.info("✅ 命令执行成功,审计记录ID: {}", commandLog.getId()); + log.info("✅ 命令执行成功,审计记录ID: {}", commandRecord.getId()); return result; } catch (Exception e) { // 更新执行失败 - commandLog.setExecuteStatus(-1); // -1-失败 - commandLog.setExecuteErrorMessage(e.getMessage()); + commandRecord.setExecuteStatus(-1); // -1-失败 + commandRecord.setExecuteErrorMessage(e.getMessage()); // 更新审计记录 - logService.updateById(commandLog); + recordService.updateById(commandRecord); - log.error("❌ 命令执行失败,审计记录ID: {}", commandLog.getId(), e); + log.error("❌ 命令执行失败,审计记录ID: {}", commandRecord.getId(), e); // 抛出异常,由 Controller 统一处理 throw e; diff --git a/src/main/java/com/youlai/boot/platform/ai/tools/UserTools.java b/src/main/java/com/youlai/boot/platform/ai/tools/UserTools.java index 6cff249f..64d9e923 100644 --- a/src/main/java/com/youlai/boot/platform/ai/tools/UserTools.java +++ b/src/main/java/com/youlai/boot/platform/ai/tools/UserTools.java @@ -17,28 +17,28 @@ import org.springframework.ai.tool.annotation.ToolParam; @RequiredArgsConstructor public class UserTools { - private final UserService userService; + private final UserService userService; - @Tool(description = "根据关键字在用户列表中筛选用户") - public String queryUser( - @ToolParam(description = "搜索关键字,用于在列表中搜索筛选") String keywords - ) { - // 返回搜索关键字,前端会在用户列表页面进行筛选 - return "将在用户列表中搜索:" + keywords; - } + @Tool(description = "根据关键字在用户列表中筛选用户") + public String queryUser( + @ToolParam(description = "搜索关键字,用于在列表中搜索筛选") String keywords + ) { + // 返回搜索关键字,前端会在用户列表页面进行筛选 + return "将在用户列表中搜索:" + keywords; + } - @Tool(description = "根据用户名更新用户昵称") - public String updateUserNickname( - @ToolParam(description = "用户名") String username, - @ToolParam(description = "新的昵称") String nickname - ) { + @Tool(description = "根据用户名更新用户昵称") + public String updateUserNickname( + @ToolParam(description = "用户名") String username, + @ToolParam(description = "新的昵称") String nickname + ) { - boolean ok = userService.update(new LambdaUpdateWrapper() - .eq(User::getUsername, username) - .set(User::getNickname, nickname) - ); - return ok ? "用户昵称更新成功" : "用户昵称更新失败"; - } + boolean ok = userService.update(new LambdaUpdateWrapper() + .eq(User::getUsername, username) + .set(User::getNickname, nickname) + ); + return ok ? "用户昵称更新成功" : "用户昵称更新失败"; + } } diff --git a/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java b/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java index 497682f4..5485dced 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java +++ b/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java @@ -11,7 +11,7 @@ import com.youlai.boot.platform.codegen.model.query.TablePageQuery; import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO; import com.youlai.boot.platform.codegen.model.vo.TablePageVO; import com.youlai.boot.common.annotation.Log; -import com.youlai.boot.platform.codegen.service.GenConfigService; +import com.youlai.boot.platform.codegen.service.GenTableService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -40,7 +40,7 @@ import java.util.List; public class CodegenController { private final CodegenService codegenService; - private final GenConfigService genConfigService; + private final GenTableService genTableService; private final CodegenProperties codegenProperties; @Operation(summary = "获取数据表分页列表") @@ -55,10 +55,10 @@ public class CodegenController { @Operation(summary = "获取代码生成配置") @GetMapping("/{tableName}/config") - public Result getGenConfigFormData( + public Result getGenTableFormData( @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName ) { - GenConfigForm formData = genConfigService.getGenConfigFormData(tableName); + GenConfigForm formData = genTableService.getGenTableFormData(tableName); return Result.success(formData); } @@ -66,7 +66,7 @@ public class CodegenController { @PostMapping("/{tableName}/config") @Log(value = "生成代码", module = LogModuleEnum.OTHER) public Result saveGenConfig(@RequestBody GenConfigForm formData) { - genConfigService.saveGenConfig(formData); + genTableService.saveGenConfig(formData); return Result.success(); } @@ -75,7 +75,7 @@ public class CodegenController { public Result deleteGenConfig( @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName ) { - genConfigService.deleteGenConfig(tableName); + genTableService.deleteGenConfig(tableName); return Result.success(); } diff --git a/src/main/java/com/youlai/boot/platform/codegen/converter/CodegenConverter.java b/src/main/java/com/youlai/boot/platform/codegen/converter/CodegenConverter.java index 999d5fd1..4e9be4b9 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/converter/CodegenConverter.java +++ b/src/main/java/com/youlai/boot/platform/codegen/converter/CodegenConverter.java @@ -1,7 +1,7 @@ package com.youlai.boot.platform.codegen.converter; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; -import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; +import com.youlai.boot.platform.codegen.model.entity.GenTableColumn; import com.youlai.boot.platform.codegen.model.form.GenConfigForm; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -17,25 +17,25 @@ import java.util.List; @Mapper(componentModel = "spring") public interface CodegenConverter { - @Mapping(source = "genConfig.tableName", target = "tableName") - @Mapping(source = "genConfig.businessName", target = "businessName") - @Mapping(source = "genConfig.moduleName", target = "moduleName") - @Mapping(source = "genConfig.packageName", target = "packageName") - @Mapping(source = "genConfig.entityName", target = "entityName") - @Mapping(source = "genConfig.author", target = "author") - @Mapping(source = "genConfig.pageType", target = "pageType") - @Mapping(source = "genConfig.removeTablePrefix", target = "removeTablePrefix") + @Mapping(source = "genTable.tableName", target = "tableName") + @Mapping(source = "genTable.businessName", target = "businessName") + @Mapping(source = "genTable.moduleName", target = "moduleName") + @Mapping(source = "genTable.packageName", target = "packageName") + @Mapping(source = "genTable.entityName", target = "entityName") + @Mapping(source = "genTable.author", target = "author") + @Mapping(source = "genTable.pageType", target = "pageType") + @Mapping(source = "genTable.removeTablePrefix", target = "removeTablePrefix") @Mapping(source = "fieldConfigs", target = "fieldConfigs") - GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); + GenConfigForm toGenConfigForm(GenTable genTable, List fieldConfigs); - List toGenFieldConfigForm(List fieldConfigs); + List toGenTableColumnForm(List fieldConfigs); - GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig); + GenConfigForm.FieldConfig toGenTableColumnForm(GenTableColumn genTableColumn); - GenConfig toGenConfig(GenConfigForm formData); + GenTable toGenTable(GenConfigForm formData); - List toGenFieldConfig(List fieldConfigs); + List toGenTableColumn(List fieldConfigs); - GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig); + GenTableColumn toGenTableColumn(GenConfigForm.FieldConfig fieldConfig); } \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/platform/codegen/mapper/GenFieldConfigMapper.java b/src/main/java/com/youlai/boot/platform/codegen/mapper/GenTableColumnMapper.java similarity index 53% rename from src/main/java/com/youlai/boot/platform/codegen/mapper/GenFieldConfigMapper.java rename to src/main/java/com/youlai/boot/platform/codegen/mapper/GenTableColumnMapper.java index 14aaf2b1..de7728e1 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/mapper/GenFieldConfigMapper.java +++ b/src/main/java/com/youlai/boot/platform/codegen/mapper/GenTableColumnMapper.java @@ -1,17 +1,17 @@ package com.youlai.boot.platform.codegen.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTableColumn; import org.apache.ibatis.annotations.Mapper; /** - * 代码生成字段配置访问层 + * 代码生成表字段配置访问层 * * @author Ray * @since 2.10.0 */ @Mapper -public interface GenFieldConfigMapper extends BaseMapper { +public interface GenTableColumnMapper extends BaseMapper { } diff --git a/src/main/java/com/youlai/boot/platform/codegen/mapper/GenConfigMapper.java b/src/main/java/com/youlai/boot/platform/codegen/mapper/GenTableMapper.java similarity index 55% rename from src/main/java/com/youlai/boot/platform/codegen/mapper/GenConfigMapper.java rename to src/main/java/com/youlai/boot/platform/codegen/mapper/GenTableMapper.java index ac3c79af..d7d2e750 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/mapper/GenConfigMapper.java +++ b/src/main/java/com/youlai/boot/platform/codegen/mapper/GenTableMapper.java @@ -1,17 +1,17 @@ package com.youlai.boot.platform.codegen.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; import org.apache.ibatis.annotations.Mapper; /** - * 代码生成基础配置访问层 + * 代码生成表配置访问层 * * @author Ray * @since 2.10.0 */ @Mapper -public interface GenConfigMapper extends BaseMapper { +public interface GenTableMapper extends BaseMapper { } diff --git a/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenConfig.java b/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenTable.java similarity index 89% rename from src/main/java/com/youlai/boot/platform/codegen/model/entity/GenConfig.java rename to src/main/java/com/youlai/boot/platform/codegen/model/entity/GenTable.java index 13ad52f7..45d30606 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenConfig.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenTable.java @@ -7,15 +7,15 @@ import lombok.Getter; import lombok.Setter; /** - * 代码生成基础配置 + * 代码生成表配置 * * @author Ray * @since 2.10.0 */ -@TableName(value = "gen_config") +@TableName(value = "gen_table") @Getter @Setter -public class GenConfig extends BaseEntity { +public class GenTable extends BaseEntity { /** * 表名 @@ -61,4 +61,5 @@ public class GenConfig extends BaseEntity { * 要移除的表前缀,如: sys_ */ private String removeTablePrefix; -} \ No newline at end of file +} + diff --git a/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenFieldConfig.java b/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenTableColumn.java similarity index 89% rename from src/main/java/com/youlai/boot/platform/codegen/model/entity/GenFieldConfig.java rename to src/main/java/com/youlai/boot/platform/codegen/model/entity/GenTableColumn.java index 7c9bb91f..837bbc50 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenFieldConfig.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenTableColumn.java @@ -11,21 +11,21 @@ import lombok.Getter; import lombok.Setter; /** - * 字段生成配置实体 + * 代码生成表字段配置实体 * * @author Ray * @since 2.10.0 */ -@TableName(value = "gen_field_config") +@TableName(value = "gen_table_column") @Getter @Setter -public class GenFieldConfig extends BaseEntity { +public class GenTableColumn extends BaseEntity { /** - * 关联的配置ID + * 关联的表配置ID */ - private Long configId; + private Long tableId; /** * 列名 @@ -104,3 +104,4 @@ public class GenFieldConfig extends BaseEntity { */ private String dictType; } + diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/GenFieldConfigService.java b/src/main/java/com/youlai/boot/platform/codegen/service/GenTableColumnService.java similarity index 56% rename from src/main/java/com/youlai/boot/platform/codegen/service/GenFieldConfigService.java rename to src/main/java/com/youlai/boot/platform/codegen/service/GenTableColumnService.java index fbfda4d4..11e9ad61 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/service/GenFieldConfigService.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/GenTableColumnService.java @@ -1,7 +1,7 @@ package com.youlai.boot.platform.codegen.service; import com.baomidou.mybatisplus.extension.service.IService; -import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTableColumn; /** * 代码生成配置接口 @@ -9,6 +9,6 @@ import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; * @author Ray * @since 2.10.0 */ -public interface GenFieldConfigService extends IService { +public interface GenTableColumnService extends IService { } diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/GenConfigService.java b/src/main/java/com/youlai/boot/platform/codegen/service/GenTableService.java similarity index 77% rename from src/main/java/com/youlai/boot/platform/codegen/service/GenConfigService.java rename to src/main/java/com/youlai/boot/platform/codegen/service/GenTableService.java index 2731260e..135c0c3e 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/service/GenConfigService.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/GenTableService.java @@ -1,7 +1,7 @@ package com.youlai.boot.platform.codegen.service; import com.baomidou.mybatisplus.extension.service.IService; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; import com.youlai.boot.platform.codegen.model.form.GenConfigForm; /** @@ -10,7 +10,7 @@ import com.youlai.boot.platform.codegen.model.form.GenConfigForm; * @author Ray * @since 2.10.0 */ -public interface GenConfigService extends IService { +public interface GenTableService extends IService { /** * 获取代码生成配置 @@ -18,7 +18,7 @@ public interface GenConfigService extends IService { * @param tableName 表名 * @return */ - GenConfigForm getGenConfigFormData(String tableName); + GenConfigForm getGenTableFormData(String tableName); /** * 保存代码生成配置 diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java b/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java index aa4e507b..8e33a2df 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java @@ -13,13 +13,13 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.platform.codegen.enums.JavaTypeEnum; import com.youlai.boot.config.property.CodegenProperties; -import com.youlai.boot.platform.codegen.service.GenConfigService; -import com.youlai.boot.platform.codegen.service.GenFieldConfigService; +import com.youlai.boot.platform.codegen.service.GenTableService; +import com.youlai.boot.platform.codegen.service.GenTableColumnService; import com.youlai.boot.platform.codegen.service.CodegenService; import com.youlai.boot.core.exception.BusinessException; import com.youlai.boot.platform.codegen.mapper.DatabaseMapper; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; -import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; +import com.youlai.boot.platform.codegen.model.entity.GenTableColumn; import com.youlai.boot.platform.codegen.model.query.TablePageQuery; import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO; import com.youlai.boot.platform.codegen.model.vo.TablePageVO; @@ -48,8 +48,8 @@ public class CodegenServiceImpl implements CodegenService { private final DatabaseMapper databaseMapper; private final CodegenProperties codegenProperties; - private final GenConfigService genConfigService; - private final GenFieldConfigService genFieldConfigService; + private final GenTableService genTableService; + private final GenTableColumnService genTableColumnService; /** * 数据表分页列表 @@ -77,16 +77,16 @@ public class CodegenServiceImpl implements CodegenService { List list = new ArrayList<>(); - GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper() - .eq(GenConfig::getTableName, tableName) + GenTable genTable = genTableService.getOne(new LambdaQueryWrapper() + .eq(GenTable::getTableName, tableName) ); - if (genConfig == null) { + if (genTable == null) { throw new BusinessException("未找到表生成配置"); } - List fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper() - .eq(GenFieldConfig::getConfigId, genConfig.getId()) - .orderByAsc(GenFieldConfig::getFieldSort) + List fieldConfigs = genTableColumnService.list(new LambdaQueryWrapper() + .eq(GenTableColumn::getTableId, genTable.getId()) + .orderByAsc(GenTableColumn::getFieldSort) ); if (CollectionUtil.isEmpty(fieldConfigs)) { @@ -102,7 +102,7 @@ public class CodegenServiceImpl implements CodegenService { /* 1. 生成文件名 UserController */ // User Role Menu Dept - String entityName = genConfig.getEntityName(); + String entityName = genTable.getEntityName(); // Controller Service Mapper Entity String templateName = templateConfigEntry.getKey(); // .java .ts .vue @@ -114,9 +114,9 @@ public class CodegenServiceImpl implements CodegenService { /* 2. 生成文件路径 */ // 包名:com.youlai.boot - String packageName = genConfig.getPackageName(); + String packageName = genTable.getPackageName(); // 模块名:system - String moduleName = genConfig.getModuleName(); + String moduleName = genTable.getModuleName(); // 子包名:controller String subpackageName = templateConfig.getSubpackageName(); // 组合成文件路径:src/main/java/com/youlai/boot/system/controller @@ -126,8 +126,8 @@ public class CodegenServiceImpl implements CodegenService { /* 3. 生成文件内容 */ // 将模板文件中的变量替换为具体的值 生成代码内容 // 优先使用保存的 ui,没有则使用请求参数 - String finalType = StrUtil.blankToDefault(genConfig.getPageType(), pageType); - String content = getCodeContent(templateConfig, genConfig, fieldConfigs, finalType); + String finalType = StrUtil.blankToDefault(genTable.getPageType(), pageType); + String content = getCodeContent(templateConfig, genTable, fieldConfigs, finalType); previewVO.setContent(content); list.add(previewVO); @@ -215,29 +215,29 @@ public class CodegenServiceImpl implements CodegenService { * @param fieldConfigs 字段配置 * @return 代码内容 */ - private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs, String pageType) { + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenTable genTable, List fieldConfigs, String pageType) { Map bindMap = new HashMap<>(); - String entityName = genConfig.getEntityName(); + String entityName = genTable.getEntityName(); - bindMap.put("packageName", genConfig.getPackageName()); - bindMap.put("moduleName", genConfig.getModuleName()); + bindMap.put("packageName", genTable.getPackageName()); + bindMap.put("moduleName", genTable.getModuleName()); bindMap.put("subpackageName", templateConfig.getSubpackageName()); bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); bindMap.put("entityName", entityName); - bindMap.put("tableName", genConfig.getTableName()); - bindMap.put("author", genConfig.getAuthor()); + bindMap.put("tableName", genTable.getTableName()); + bindMap.put("author", genTable.getAuthor()); bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-test - bindMap.put("businessName", genConfig.getBusinessName()); + bindMap.put("businessName", genTable.getBusinessName()); bindMap.put("fieldConfigs", fieldConfigs); boolean hasLocalDateTime = false; boolean hasBigDecimal = false; boolean hasRequiredField = false; - for (GenFieldConfig fieldConfig : fieldConfigs) { + for (GenTableColumn fieldConfig : fieldConfigs) { if ("LocalDateTime".equals(fieldConfig.getFieldType())) { hasLocalDateTime = true; diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenFieldConfigServiceImpl.java b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenFieldConfigServiceImpl.java deleted file mode 100644 index e6dd938b..00000000 --- a/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenFieldConfigServiceImpl.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.youlai.boot.platform.codegen.service.impl; - -import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.youlai.boot.platform.codegen.mapper.GenFieldConfigMapper; -import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; -import com.youlai.boot.platform.codegen.service.GenFieldConfigService; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; - -/** - * 代码生成字段配置服务实现类 - * - * @author Ray - * @since 2.10.0 - */ -@Service -@RequiredArgsConstructor -public class GenFieldConfigServiceImpl extends ServiceImpl implements GenFieldConfigService { - - -} diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableColumnServiceImpl.java b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableColumnServiceImpl.java new file mode 100644 index 00000000..d74c0e57 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableColumnServiceImpl.java @@ -0,0 +1,21 @@ +package com.youlai.boot.platform.codegen.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.youlai.boot.platform.codegen.mapper.GenTableColumnMapper; +import com.youlai.boot.platform.codegen.model.entity.GenTableColumn; +import com.youlai.boot.platform.codegen.service.GenTableColumnService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 代码生成字段配置服务实现类 + * + * @author Ray.Hao + * @since 2.10.0 + */ +@Service +@RequiredArgsConstructor +public class GenTableColumnServiceImpl extends ServiceImpl implements GenTableColumnService { + + +} diff --git a/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenConfigServiceImpl.java b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableServiceImpl.java similarity index 66% rename from src/main/java/com/youlai/boot/platform/codegen/service/impl/GenConfigServiceImpl.java rename to src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableServiceImpl.java index eaf59a40..b483652f 100644 --- a/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenConfigServiceImpl.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenTableServiceImpl.java @@ -14,14 +14,14 @@ import com.youlai.boot.core.exception.BusinessException; import com.youlai.boot.config.property.CodegenProperties; import com.youlai.boot.platform.codegen.converter.CodegenConverter; import com.youlai.boot.platform.codegen.mapper.DatabaseMapper; -import com.youlai.boot.platform.codegen.mapper.GenConfigMapper; +import com.youlai.boot.platform.codegen.mapper.GenTableMapper; import com.youlai.boot.platform.codegen.model.bo.ColumnMetaData; import com.youlai.boot.platform.codegen.model.bo.TableMetaData; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; -import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; +import com.youlai.boot.platform.codegen.model.entity.GenTableColumn; import com.youlai.boot.platform.codegen.model.form.GenConfigForm; -import com.youlai.boot.platform.codegen.service.GenConfigService; -import com.youlai.boot.platform.codegen.service.GenFieldConfigService; +import com.youlai.boot.platform.codegen.service.GenTableService; +import com.youlai.boot.platform.codegen.service.GenTableColumnService; import com.youlai.boot.system.service.MenuService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -40,11 +40,11 @@ import java.util.Objects; */ @Service @RequiredArgsConstructor -public class GenConfigServiceImpl extends ServiceImpl implements GenConfigService { +public class GenTableServiceImpl extends ServiceImpl implements GenTableService { private final DatabaseMapper databaseMapper; private final CodegenProperties codegenProperties; - private final GenFieldConfigService genFieldConfigService; + private final GenTableColumnService genTableColumnService; private final CodegenConverter codegenConverter; @Value("${spring.profiles.active}") @@ -59,64 +59,64 @@ public class GenConfigServiceImpl extends ServiceImpl(GenConfig.class) - .eq(GenConfig::getTableName, tableName) + GenTable genTable = this.getOne( + new LambdaQueryWrapper<>(GenTable.class) + .eq(GenTable::getTableName, tableName) .last("LIMIT 1") ); // 是否有代码生成配置 - boolean hasGenConfig = genConfig != null; + boolean hasGenTable = genTable != null; // 如果没有代码生成配置,则根据表的元数据生成默认配置 - if (genConfig == null) { + if (genTable == null) { TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName); Assert.isTrue(tableMetadata != null, "未找到表元数据"); - genConfig = new GenConfig(); - genConfig.setTableName(tableName); + genTable = new GenTable(); + genTable.setTableName(tableName); // 表注释作为业务名称,去掉表字 例如:用户表 -> 用户 String tableComment = tableMetadata.getTableComment(); if (StrUtil.isNotBlank(tableComment)) { - genConfig.setBusinessName(tableComment.replace("表", "").trim()); + genTable.setBusinessName(tableComment.replace("表", "").trim()); } // 根据表名生成实体类名,支持去除前缀 例如:sys_user -> SysUser - String removePrefix = genConfig.getRemoveTablePrefix(); + String removePrefix = genTable.getRemoveTablePrefix(); String processedTable = tableName; if (StrUtil.isNotBlank(removePrefix) && StrUtil.startWith(tableName, removePrefix)) { processedTable = StrUtil.removePrefix(tableName, removePrefix); } - genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(processedTable)))); + genTable.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(processedTable)))); - genConfig.setPackageName(YouLaiBootApplication.class.getPackageName()); - genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 - genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); + genTable.setPackageName(YouLaiBootApplication.class.getPackageName()); + genTable.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 + genTable.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); } // 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置 - List genFieldConfigs = new ArrayList<>(); + List genTableColumns = new ArrayList<>(); // 获取表的列 List tableColumns = databaseMapper.getTableColumns(tableName); if (CollectionUtil.isNotEmpty(tableColumns)) { // 查询字段生成配置 - List fieldConfigList = genFieldConfigService.list( - new LambdaQueryWrapper() - .eq(GenFieldConfig::getConfigId, genConfig.getId()) - .orderByAsc(GenFieldConfig::getFieldSort) + List fieldConfigList = genTableColumnService.list( + new LambdaQueryWrapper() + .eq(GenTableColumn::getTableId, genTable.getId()) + .orderByAsc(GenTableColumn::getFieldSort) ); Integer maxSort = fieldConfigList.stream() - .map(GenFieldConfig::getFieldSort) + .map(GenTableColumn::getFieldSort) .filter(Objects::nonNull) // 过滤掉空值 .max(Integer::compareTo) .orElse(0); for (ColumnMetaData tableColumn : tableColumns) { // 根据列名获取字段生成配置 String columnName = tableColumn.getColumnName(); - GenFieldConfig fieldConfig = fieldConfigList.stream() + GenTableColumn fieldConfig = fieldConfigList.stream() .filter(item -> StrUtil.equals(item.getColumnName(), columnName)) .findFirst() .orElseGet(() -> createDefaultFieldConfig(tableColumn)); @@ -130,16 +130,16 @@ public class GenConfigServiceImpl extends ServiceImpl genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs()); + List genTableColumns = codegenConverter.toGenTableColumn(formData.getFieldConfigs()); - if (CollectionUtil.isEmpty(genFieldConfigs)) { + if (CollectionUtil.isEmpty(genTableColumns)) { throw new BusinessException("字段配置不能为空"); } - genFieldConfigs.forEach(genFieldConfig -> { - genFieldConfig.setConfigId(genConfig.getId()); + genTableColumns.forEach(genTableColumn -> { + genTableColumn.setTableId(genTable.getId()); }); - genFieldConfigService.saveOrUpdateBatch(genFieldConfigs); + genTableColumnService.saveOrUpdateBatch(genTableColumns); } /** @@ -208,15 +208,15 @@ public class GenConfigServiceImpl extends ServiceImpl() - .eq(GenConfig::getTableName, tableName)); + GenTable genTable = this.getOne(new LambdaQueryWrapper() + .eq(GenTable::getTableName, tableName)); - boolean result = this.remove(new LambdaQueryWrapper() - .eq(GenConfig::getTableName, tableName) + boolean result = this.remove(new LambdaQueryWrapper() + .eq(GenTable::getTableName, tableName) ); if (result) { - genFieldConfigService.remove(new LambdaQueryWrapper() - .eq(GenFieldConfig::getConfigId, genConfig.getId()) + genTableColumnService.remove(new LambdaQueryWrapper() + .eq(GenTableColumn::getTableId, genTable.getId()) ); } } diff --git a/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java b/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java index 23852ae0..fec93bc9 100644 --- a/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java +++ b/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java @@ -16,7 +16,7 @@ import java.time.LocalDateTime; * 支持自动填充创建时间、更新时间和租户ID *

* - * @author haoxr + * @author Ray.Hao * @since 2022/10/14 */ @Component @@ -50,7 +50,8 @@ public class MyMetaObjectHandler implements MetaObjectHandler { if (tenantId != null) { // 使用 strictInsertFill 自动填充租户ID // 注意:由于 TenantDynamicFieldConfig 已将 exist 设置为 true,这里可以正常填充 - this.strictInsertFill(metaObject, "tenantId", () -> tenantId, Long.class); + Long finalTenantId = tenantId; + this.strictInsertFill(metaObject, "tenantId", () -> finalTenantId, Long.class); } } } diff --git a/src/main/java/com/youlai/boot/plugin/mybatis/TenantLineHandler.java b/src/main/java/com/youlai/boot/plugin/mybatis/MyTenantLineHandler.java similarity index 95% rename from src/main/java/com/youlai/boot/plugin/mybatis/TenantLineHandler.java rename to src/main/java/com/youlai/boot/plugin/mybatis/MyTenantLineHandler.java index 1b33ba0c..5e63731f 100644 --- a/src/main/java/com/youlai/boot/plugin/mybatis/TenantLineHandler.java +++ b/src/main/java/com/youlai/boot/plugin/mybatis/MyTenantLineHandler.java @@ -25,7 +25,7 @@ import java.util.List; @Component @RequiredArgsConstructor @ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true", matchIfMissing = false) -public class TenantLineHandler implements com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler { +public class MyTenantLineHandler implements TenantLineHandler { private final TenantProperties tenantProperties; diff --git a/src/main/java/com/youlai/boot/security/filter/CaptchaValidationFilter.java b/src/main/java/com/youlai/boot/security/filter/CaptchaValidationFilter.java index f0a08976..b4169424 100644 --- a/src/main/java/com/youlai/boot/security/filter/CaptchaValidationFilter.java +++ b/src/main/java/com/youlai/boot/security/filter/CaptchaValidationFilter.java @@ -2,38 +2,45 @@ package com.youlai.boot.security.filter; import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.core.web.WebResponseHelper; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; +import jakarta.servlet.ServletInputStream; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.StreamUtils; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; import java.io.IOException; - +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; /** * 图形验证码校验过滤器 - * - * @author haoxr - * @since 2022/10/1 */ public class CaptchaValidationFilter extends OncePerRequestFilter { - private static final RequestMatcher LOGIN_PATH_REQUEST_MATCHER = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST,SecurityConstants.LOGIN_PATH); + private static final RequestMatcher LOGIN_PATH_REQUEST_MATCHER = PathPatternRequestMatcher.withDefaults() + .matcher(HttpMethod.POST, SecurityConstants.LOGIN_PATH); public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; - public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; + public static final String CAPTCHA_ID_PARAM_NAME = "captchaId"; private final RedisTemplate redisTemplate; - private final CodeGenerator codeGenerator; public CaptchaValidationFilter(RedisTemplate redisTemplate, CodeGenerator codeGenerator) { @@ -41,37 +48,111 @@ public class CaptchaValidationFilter extends OncePerRequestFilter { this.codeGenerator = codeGenerator; } - @Override - public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { - // 检验登录接口的验证码 - if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) { - // 请求中的验证码 - String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME); - // TODO 兼容没有验证码的版本(线上请移除这个判断) - if (StrUtil.isBlank(captchaCode)) { - chain.doFilter(request, response); - return; - } - // 缓存中的验证码 - String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME); - String cacheVerifyCode = (String) redisTemplate.opsForValue().get( - StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) - ); - if (cacheVerifyCode == null) { - WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); - } else { - // 验证码比对 - if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { - chain.doFilter(request, response); - } else { - WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR); - } - } - } else { - // 非登录接口放行 + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + // 非登录接口直接放行 + if (!LOGIN_PATH_REQUEST_MATCHER.matches(request)) { chain.doFilter(request, response); + return; + } + + // 仅支持 JSON 登录 + String contentType = request.getContentType(); + if (contentType == null || !contentType.contains(MediaType.APPLICATION_JSON_VALUE)) { + WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + return; + } + + // 包装请求,确保下游还能读取 body + ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request); + + byte[] bodyBytes = StreamUtils.copyToByteArray(requestWrapper.getInputStream()); + String body = new String(bodyBytes, StandardCharsets.UTF_8); + String captchaCode = null; + String captchaId = null; + + if (StrUtil.isNotBlank(body)) { + JSONObject jsonObject = JSONUtil.parseObj(body); + captchaCode = jsonObject.getStr(CAPTCHA_CODE_PARAM_NAME); + captchaId = jsonObject.getStr(CAPTCHA_ID_PARAM_NAME); + } + + if (StrUtil.isBlank(captchaCode) || StrUtil.isBlank(captchaId)) { + WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + return; + } + + String cacheVerifyCode = (String) redisTemplate.opsForValue().get( + StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId) + ); + if (cacheVerifyCode == null) { + WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + return; + } + + if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { + HttpServletRequest repeatableRequest = new RepeatableReadRequestWrapper(requestWrapper, bodyBytes); + chain.doFilter(repeatableRequest, response); + } else { + WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR); } } + /** + * Simple wrapper to allow repeated reads of the request body after we've parsed it here. + */ + private static class RepeatableReadRequestWrapper extends HttpServletRequestWrapper { + + private final byte[] cachedBody; + + RepeatableReadRequestWrapper(HttpServletRequest request, byte[] cachedBody) { + super(request); + this.cachedBody = cachedBody != null ? cachedBody : new byte[0]; + } + + @Override + public ServletInputStream getInputStream() { + ByteArrayInputStream bais = new ByteArrayInputStream(cachedBody); + return new ServletInputStream() { + @Override + public int read() { + return bais.read(); + } + + @Override + public boolean isFinished() { + return bais.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(jakarta.servlet.ReadListener readListener) { + // no-op + } + }; + } + + @Override + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); + } + + @Override + public int getContentLength() { + return cachedBody.length; + } + + @Override + public long getContentLengthLong() { + return cachedBody.length; + } + } } + + diff --git a/src/main/java/com/youlai/boot/security/model/OnlineUser.java b/src/main/java/com/youlai/boot/security/model/OnlineUser.java index 6dd72608..50d9ac7b 100644 --- a/src/main/java/com/youlai/boot/security/model/OnlineUser.java +++ b/src/main/java/com/youlai/boot/security/model/OnlineUser.java @@ -38,6 +38,11 @@ public class OnlineUser { */ private Integer dataScope; + /** + * 租户ID + */ + private Long tenantId; + /** * 角色权限集合 */ diff --git a/src/main/java/com/youlai/boot/security/model/SysUserDetails.java b/src/main/java/com/youlai/boot/security/model/SysUserDetails.java index 8a79831c..1112fcb3 100644 --- a/src/main/java/com/youlai/boot/security/model/SysUserDetails.java +++ b/src/main/java/com/youlai/boot/security/model/SysUserDetails.java @@ -56,6 +56,11 @@ public class SysUserDetails implements UserDetails { */ private Integer dataScope; + /** + * 租户ID + */ + private Long tenantId; + /** * 用户角色权限集合 */ @@ -73,6 +78,7 @@ public class SysUserDetails implements UserDetails { this.enabled = ObjectUtil.equal(user.getStatus(), 1); this.deptId = user.getDeptId(); this.dataScope = user.getDataScope(); + this.tenantId = user.getTenantId(); // 初始化角色权限集合 this.authorities = CollectionUtil.isNotEmpty(user.getRoles()) diff --git a/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java b/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java index e68d119d..df292fde 100644 --- a/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java +++ b/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java @@ -54,4 +54,9 @@ public class UserAuthCredentials { */ private Integer dataScope; + /** + * 租户ID(从登录上下文中获取) + */ + private Long tenantId; + } diff --git a/src/main/java/com/youlai/boot/security/service/PermissionService.java b/src/main/java/com/youlai/boot/security/service/PermissionService.java index 8b11ad4e..0ebd87e3 100644 --- a/src/main/java/com/youlai/boot/security/service/PermissionService.java +++ b/src/main/java/com/youlai/boot/security/service/PermissionService.java @@ -15,8 +15,8 @@ import java.util.*; /** * SpringSecurity 权限校验 * - * @author haoxr - * @since 2022/2/22 + * @author Ray.Hao + * @since 0.0.1 */ @Component("ss") @RequiredArgsConstructor diff --git a/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java b/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java index 213b698f..40f9be1d 100644 --- a/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java +++ b/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java @@ -1,5 +1,6 @@ package com.youlai.boot.security.service; +import com.youlai.boot.common.tenant.TenantContextHolder; import com.youlai.boot.security.model.SysUserDetails; import com.youlai.boot.security.model.UserAuthCredentials; import com.youlai.boot.system.service.UserService; @@ -37,6 +38,8 @@ public class SysUserDetailsService implements UserDetailsService { if (userAuthCredentials == null) { throw new UsernameNotFoundException(username); } + // 将当前上下文中的租户ID写入认证凭证,便于后续 Token 携带租户信息 + userAuthCredentials.setTenantId(TenantContextHolder.getTenantId()); return new SysUserDetails(userAuthCredentials); } catch (Exception e) { // 记录异常日志 diff --git a/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java b/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java index 5f3f5397..715801c7 100644 --- a/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java +++ b/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java @@ -91,6 +91,7 @@ public class JwtTokenManager implements TokenManager { userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围 + userDetails.setTenantId(payloads.getLong(JwtClaimConstants.TENANT_ID)); // 租户ID userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名 // 角色集合 @@ -275,6 +276,7 @@ public class JwtTokenManager implements TokenManager { payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围 + payload.put(JwtClaimConstants.TENANT_ID, userDetails.getTenantId()); // 租户ID // claims 中添加角色信息 Set roles = authentication.getAuthorities().stream() diff --git a/src/main/java/com/youlai/boot/security/token/RedisTokenManager.java b/src/main/java/com/youlai/boot/security/token/RedisTokenManager.java index 9c899156..bd9dbaa0 100644 --- a/src/main/java/com/youlai/boot/security/token/RedisTokenManager.java +++ b/src/main/java/com/youlai/boot/security/token/RedisTokenManager.java @@ -61,6 +61,7 @@ public class RedisTokenManager implements TokenManager { user.getUsername(), user.getDeptId(), user.getDataScope(), + user.getTenantId(), user.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toSet()) @@ -268,6 +269,7 @@ public class RedisTokenManager implements TokenManager { userDetails.setUsername(onlineUser.getUsername()); userDetails.setDeptId(onlineUser.getDeptId()); userDetails.setDataScope(onlineUser.getDataScope()); + userDetails.setTenantId(onlineUser.getTenantId()); userDetails.setAuthorities(authorities); return userDetails; } diff --git a/src/main/java/com/youlai/boot/system/controller/TenantController.java b/src/main/java/com/youlai/boot/system/controller/TenantController.java index 723c6b86..f7cdf190 100644 --- a/src/main/java/com/youlai/boot/system/controller/TenantController.java +++ b/src/main/java/com/youlai/boot/system/controller/TenantController.java @@ -27,7 +27,7 @@ import java.util.List; */ @Tag(name = "租户管理接口") @RestController -@RequestMapping("/api/v1/tenant") +@RequestMapping("/api/v1/tenants") @RequiredArgsConstructor @Slf4j @ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true", matchIfMissing = false) @@ -44,7 +44,7 @@ public class TenantController { * @return 租户列表 */ @Operation(summary = "获取当前用户的租户列表") - @GetMapping("/list") + @GetMapping public Result> getTenantList() { Long userId = SecurityUtils.getUserId(); List tenantList = tenantService.getTenantListByUserId(userId); @@ -72,14 +72,13 @@ public class TenantController { * 切换租户 *

* 切换当前用户的租户上下文,需要验证用户是否有权限访问该租户 - * 并记录审计日志 *

* * @param tenantId 目标租户ID * @return 切换结果 */ @Operation(summary = "切换租户") - @PostMapping("/switch/{tenantId}") + @PostMapping("/{tenantId}/switch") public Result switchTenant( @Parameter(description = "租户ID") @PathVariable Long tenantId, HttpServletRequest request @@ -89,41 +88,30 @@ public class TenantController { log.info("用户 {} 请求切换租户:{} -> {}", userId, fromTenantId, tenantId); - try { - // 验证用户是否有权限访问该租户 - boolean hasPermission = tenantService.hasTenantPermission(userId, tenantId); - if (!hasPermission) { - log.warn("用户 {} 无权限访问租户 {}", userId, tenantId); - // 记录失败日志 - tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, "无权限访问该租户", request); - return Result.failed("无权限访问该租户"); - } - - // 验证租户是否存在且正常 - TenantVO tenant = tenantService.getTenantById(tenantId); - if (tenant == null) { - tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, "租户不存在", request); - return Result.failed("租户不存在"); - } - if (tenant.getStatus() == null || tenant.getStatus() != 1) { - tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, "租户已禁用", request); - return Result.failed("租户已禁用"); - } - - // 设置新的租户上下文 - TenantContextHolder.setTenantId(tenantId); - - // 记录成功日志 - tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, true, null, request); - - log.info("用户 {} 成功切换租户到 {}", userId, tenantId); - - return Result.success(tenant); - } catch (Exception e) { - log.error("用户 {} 切换租户失败", userId, e); - tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, e.getMessage(), request); - return Result.failed("切换租户失败:" + e.getMessage()); + // 验证用户是否有权限访问该租户 + boolean hasPermission = tenantService.hasTenantPermission(userId, tenantId); + if (!hasPermission) { + log.warn("用户 {} 无权限访问租户 {}", userId, tenantId); + return Result.failed("无权限访问该租户"); } + + // 验证租户是否存在且正常 + TenantVO tenant = tenantService.getTenantById(tenantId); + if (tenant == null) { + log.warn("用户 {} 尝试切换到不存在的租户 {}", userId, tenantId); + return Result.failed("租户不存在"); + } + if (tenant.getStatus() == null || tenant.getStatus() != 1) { + log.warn("用户 {} 尝试切换到已禁用的租户 {}", userId, tenantId); + return Result.failed("租户已禁用"); + } + + // 设置新的租户上下文 + TenantContextHolder.setTenantId(tenantId); + + log.info("用户 {} 成功切换租户:{} -> {}", userId, fromTenantId, tenantId); + + return Result.success(tenant); } } diff --git a/src/main/java/com/youlai/boot/system/mapper/TenantSwitchLogMapper.java b/src/main/java/com/youlai/boot/system/mapper/TenantSwitchLogMapper.java deleted file mode 100644 index 62cd36ec..00000000 --- a/src/main/java/com/youlai/boot/system/mapper/TenantSwitchLogMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.youlai.boot.system.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.youlai.boot.system.model.entity.TenantSwitchLog; -import org.apache.ibatis.annotations.Mapper; - -/** - * 租户切换审计日志 Mapper - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Mapper -public interface TenantSwitchLogMapper extends BaseMapper { -} diff --git a/src/main/java/com/youlai/boot/system/mapper/UserTenantMapper.java b/src/main/java/com/youlai/boot/system/mapper/UserTenantMapper.java deleted file mode 100644 index 9be70dc6..00000000 --- a/src/main/java/com/youlai/boot/system/mapper/UserTenantMapper.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.youlai.boot.system.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.youlai.boot.system.model.entity.UserTenant; -import org.apache.ibatis.annotations.Mapper; - -/** - * 用户租户关联 Mapper - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Mapper -public interface UserTenantMapper extends BaseMapper { -} - diff --git a/src/main/java/com/youlai/boot/system/model/entity/TenantSwitchLog.java b/src/main/java/com/youlai/boot/system/model/entity/TenantSwitchLog.java deleted file mode 100644 index d455d1e1..00000000 --- a/src/main/java/com/youlai/boot/system/model/entity/TenantSwitchLog.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.youlai.boot.system.model.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.time.LocalDateTime; - -/** - * 租户切换审计日志实体 - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Data -@TableName("sys_tenant_switch_log") -public class TenantSwitchLog { - - /** - * 主键ID - */ - @TableId(type = IdType.AUTO) - private Long id; - - /** - * 用户ID - */ - private Long userId; - - /** - * 用户名 - */ - private String username; - - /** - * 原租户ID - */ - private Long fromTenantId; - - /** - * 原租户名称 - */ - private String fromTenantName; - - /** - * 目标租户ID - */ - private Long toTenantId; - - /** - * 目标租户名称 - */ - private String toTenantName; - - /** - * 切换时间 - */ - private LocalDateTime switchTime; - - /** - * IP地址 - */ - private String ipAddress; - - /** - * 浏览器信息 - */ - private String userAgent; - - /** - * 切换状态(1-成功 0-失败) - */ - private Integer status; - - /** - * 失败原因 - */ - private String failReason; -} diff --git a/src/main/java/com/youlai/boot/system/model/entity/UserTenant.java b/src/main/java/com/youlai/boot/system/model/entity/UserTenant.java deleted file mode 100644 index 46becb9d..00000000 --- a/src/main/java/com/youlai/boot/system/model/entity/UserTenant.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.youlai.boot.system.model.entity; - -import com.baomidou.mybatisplus.annotation.TableName; -import com.youlai.boot.common.base.BaseEntity; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * 用户租户关联实体 - * - * @author Ray.Hao - * @since 3.0.0 - */ -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("sys_user_tenant") -public class UserTenant extends BaseEntity { - - /** - * 用户ID - */ - private Long userId; - - /** - * 租户ID - */ - private Long tenantId; - - /** - * 是否默认租户(1-是 0-否) - */ - private Integer isDefault; -} - diff --git a/src/main/java/com/youlai/boot/system/service/MenuService.java b/src/main/java/com/youlai/boot/system/service/MenuService.java index 6fbff3f3..389ec2bb 100644 --- a/src/main/java/com/youlai/boot/system/service/MenuService.java +++ b/src/main/java/com/youlai/boot/system/service/MenuService.java @@ -1,7 +1,7 @@ package com.youlai.boot.system.service; import com.baomidou.mybatisplus.extension.service.IService; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; import com.youlai.boot.system.model.form.MenuForm; import com.youlai.boot.common.model.Option; import com.youlai.boot.system.model.entity.Menu; @@ -79,5 +79,5 @@ public interface MenuService extends IService { * @param parentMenuId 父菜单ID * @param genConfig 实体名 */ - void addMenuForCodegen(Long parentMenuId, GenConfig genConfig); + void addMenuForCodegen(Long parentMenuId, GenTable genTable); } diff --git a/src/main/java/com/youlai/boot/system/service/TenantService.java b/src/main/java/com/youlai/boot/system/service/TenantService.java index 130ad916..2d265cea 100644 --- a/src/main/java/com/youlai/boot/system/service/TenantService.java +++ b/src/main/java/com/youlai/boot/system/service/TenantService.java @@ -46,17 +46,4 @@ public interface TenantService extends IService { * @return true-有权限,false-无权限 */ boolean hasTenantPermission(Long userId, Long tenantId); - - /** - * 记录租户切换审计日志 - * - * @param userId 用户ID - * @param fromTenantId 原租户ID - * @param toTenantId 目标租户ID - * @param success 是否成功 - * @param failReason 失败原因 - * @param request HTTP请求对象 - */ - void recordTenantSwitch(Long userId, Long fromTenantId, Long toTenantId, - boolean success, String failReason, jakarta.servlet.http.HttpServletRequest request); } diff --git a/src/main/java/com/youlai/boot/system/service/UserService.java b/src/main/java/com/youlai/boot/system/service/UserService.java index 2ee3ca09..89cb5bd7 100644 --- a/src/main/java/com/youlai/boot/system/service/UserService.java +++ b/src/main/java/com/youlai/boot/system/service/UserService.java @@ -71,9 +71,25 @@ public interface UserService extends IService { * @param username 用户名 * @return {@link UserAuthCredentials} */ - UserAuthCredentials getAuthCredentialsByUsername(String username); + /** + * 根据用户名和租户ID获取认证信息(用于多租户登录) + * + * @param username 用户名 + * @param tenantId 租户ID + * @return {@link UserAuthCredentials} + */ + UserAuthCredentials getAuthCredentialsByUsernameAndTenant(String username, Long tenantId); + + /** + * 根据用户名查询该用户在所有租户下的记录(用于多租户登录时判断是否需要选择租户) + * + * @param username 用户名 + * @return 用户列表(每个租户一条记录) + */ + List listUsersByUsername(String username); + /** * 获取导出用户列表 diff --git a/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java index 81120c24..17ac96f7 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java @@ -10,7 +10,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.youlai.boot.platform.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.entity.GenTable; import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.system.converter.MenuConverter; import com.youlai.boot.system.mapper.MenuMapper; @@ -427,14 +427,14 @@ public class MenuServiceImpl extends ServiceImpl implements Me * 代码生成时添加菜单 * * @param parentMenuId 父菜单ID - * @param genConfig 实体名称 + * @param genTable 实体名称 */ @Override - public void addMenuForCodegen(Long parentMenuId, GenConfig genConfig) { + public void addMenuForCodegen(Long parentMenuId, GenTable genTable) { Menu parentMenu = this.getById(parentMenuId); Assert.notNull(parentMenu, "上级菜单不存在"); - String entityName = genConfig.getEntityName(); + String entityName = genTable.getEntityName(); long count = this.count(new LambdaQueryWrapper().eq(Menu::getRouteName, entityName)); if (count > 0) { @@ -453,11 +453,11 @@ public class MenuServiceImpl extends ServiceImpl implements Me Menu menu = new Menu(); menu.setParentId(parentMenuId); - menu.setName(genConfig.getBusinessName()); + menu.setName(genTable.getBusinessName()); menu.setRouteName(entityName); menu.setRoutePath(StrUtil.toSymbolCase(entityName, '-')); - menu.setComponent(genConfig.getModuleName() + "/" + StrUtil.toSymbolCase(entityName, '-') + "/index"); + menu.setComponent(genTable.getModuleName() + "/" + StrUtil.toSymbolCase(entityName, '-') + "/index"); menu.setType(MenuTypeEnum.MENU.getValue()); menu.setSort(sort); menu.setVisible(1); @@ -470,7 +470,7 @@ public class MenuServiceImpl extends ServiceImpl implements Me this.updateById(menu); // 生成CURD按钮权限 - String permPrefix = genConfig.getModuleName() + ":" + genConfig.getTableName().replace("_", "-") + ":"; + String permPrefix = genTable.getModuleName() + ":" + genTable.getTableName().replace("_", "-") + ":"; String[] actions = {"查询", "新增", "修改", "删除"}; String[] perms = {"list", "create", "update", "delete"}; diff --git a/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java index 14bda285..b4dfed9c 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/TenantServiceImpl.java @@ -1,28 +1,22 @@ package com.youlai.boot.system.service.impl; -import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.youlai.boot.common.tenant.TenantContextHolder; import com.youlai.boot.system.mapper.TenantMapper; -import com.youlai.boot.system.mapper.TenantSwitchLogMapper; import com.youlai.boot.system.mapper.UserMapper; -import com.youlai.boot.system.mapper.UserTenantMapper; import com.youlai.boot.system.model.entity.Tenant; -import com.youlai.boot.system.model.entity.TenantSwitchLog; import com.youlai.boot.system.model.entity.User; -import com.youlai.boot.system.model.entity.UserTenant; import com.youlai.boot.system.model.vo.TenantVO; import com.youlai.boot.system.service.TenantService; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; -import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.IntStream; /** * 租户服务实现类 @@ -35,8 +29,6 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class TenantServiceImpl extends ServiceImpl implements TenantService { - private final UserTenantMapper userTenantMapper; - private final TenantSwitchLogMapper tenantSwitchLogMapper; private final UserMapper userMapper; @Override @@ -44,21 +36,34 @@ public class TenantServiceImpl extends ServiceImpl impleme // 临时忽略租户过滤,查询所有租户 TenantContextHolder.setIgnoreTenant(true); try { - // 查询用户关联的租户ID列表 - List userTenants = userTenantMapper.selectList( - new LambdaQueryWrapper() - .eq(UserTenant::getUserId, userId) - ); - - if (userTenants.isEmpty()) { + // 先根据用户ID查询用户信息(获取 username) + User user = userMapper.selectById(userId); + if (user == null) { return List.of(); } - // 提取租户ID列表 - List tenantIds = userTenants.stream() - .map(UserTenant::getTenantId) + // 通过 username 查询该用户在所有租户下的记录,获取租户ID列表 + List users = userMapper.selectList( + new LambdaQueryWrapper() + .eq(User::getUsername, user.getUsername()) + .eq(User::getIsDeleted, 0) + ); + + if (users.isEmpty()) { + return List.of(); + } + + // 提取租户ID列表(去重) + List tenantIds = users.stream() + .map(User::getTenantId) + .filter(tenantId -> tenantId != null) + .distinct() .collect(Collectors.toList()); + if (tenantIds.isEmpty()) { + return List.of(); + } + // 查询租户信息 List tenants = this.list( new LambdaQueryWrapper() @@ -67,17 +72,19 @@ public class TenantServiceImpl extends ServiceImpl impleme .orderByDesc(Tenant::getId) ); - // 转换为VO并标记默认租户 - return tenants.stream().map(tenant -> { - TenantVO vo = new TenantVO(); - BeanUtils.copyProperties(tenant, vo); - // 查找是否为默认租户 - userTenants.stream() - .filter(ut -> ut.getTenantId().equals(tenant.getId()) && ut.getIsDefault() == 1) - .findFirst() - .ifPresent(ut -> vo.setIsDefault(true)); - return vo; - }).collect(Collectors.toList()); + // 转换为VO,第一个租户作为默认租户 + return IntStream.range(0, tenants.size()) + .mapToObj(index -> { + Tenant tenant = tenants.get(index); + TenantVO vo = new TenantVO(); + BeanUtils.copyProperties(tenant, vo); + // 第一个租户作为默认租户 + if (index == 0) { + vo.setIsDefault(true); + } + return vo; + }) + .collect(Collectors.toList()); } finally { TenantContextHolder.setIgnoreTenant(false); } @@ -119,94 +126,25 @@ public class TenantServiceImpl extends ServiceImpl impleme public boolean hasTenantPermission(Long userId, Long tenantId) { TenantContextHolder.setIgnoreTenant(true); try { - UserTenant userTenant = userTenantMapper.selectOne( - new LambdaQueryWrapper() - .eq(UserTenant::getUserId, userId) - .eq(UserTenant::getTenantId, tenantId) + // 先根据用户ID查询用户信息(获取 username) + User user = userMapper.selectById(userId); + if (user == null) { + return false; + } + + // 检查该 username 在指定租户下是否存在用户记录 + User tenantUser = userMapper.selectOne( + new LambdaQueryWrapper() + .eq(User::getUsername, user.getUsername()) + .eq(User::getTenantId, tenantId) + .eq(User::getIsDeleted, 0) .last("LIMIT 1") ); - return userTenant != null; + return tenantUser != null; } finally { TenantContextHolder.setIgnoreTenant(false); } } - @Override - public void recordTenantSwitch(Long userId, Long fromTenantId, Long toTenantId, - boolean success, String failReason, HttpServletRequest request) { - try { - // 临时忽略租户过滤,确保日志可以写入 - TenantContextHolder.setIgnoreTenant(true); - - // 创建审计日志 - TenantSwitchLog log = new TenantSwitchLog(); - log.setUserId(userId); - log.setFromTenantId(fromTenantId); - log.setToTenantId(toTenantId); - log.setSwitchTime(LocalDateTime.now()); - log.setStatus(success ? 1 : 0); - log.setFailReason(failReason); - - // 获取用户名 - if (userId != null) { - User user = userMapper.selectById(userId); - if (user != null) { - log.setUsername(user.getUsername()); - } - } - - // 获取租户名称 - if (fromTenantId != null) { - Tenant fromTenant = this.getById(fromTenantId); - if (fromTenant != null) { - log.setFromTenantName(fromTenant.getName()); - } - } - if (toTenantId != null) { - Tenant toTenant = this.getById(toTenantId); - if (toTenant != null) { - log.setToTenantName(toTenant.getName()); - } - } - - // 获取IP地址和User-Agent - if (request != null) { - log.setIpAddress(getIpAddress(request)); - log.setUserAgent(request.getHeader("User-Agent")); - } - - // 保存审计日志 - tenantSwitchLogMapper.insert(log); - } catch (Exception e) { - // 记录日志失败不应影响业务,仅记录错误 - Slf4j.getLogger(this.getClass()).error("记录租户切换日志失败", e); - } finally { - TenantContextHolder.setIgnoreTenant(false); - } - } - - /** - * 获取客户端IP地址 - */ - private String getIpAddress(HttpServletRequest request) { - String ip = request.getHeader("X-Forwarded-For"); - if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("X-Real-IP"); - } - if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("Proxy-Client-IP"); - } - if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("WL-Proxy-Client-IP"); - } - if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) { - ip = request.getRemoteAddr(); - } - // 处理多级代理的情况 - if (ip != null && ip.contains(",")) { - ip = ip.split(",")[0].trim(); - } - return ip; - } } diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java index 401dc492..b81bba24 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java @@ -18,6 +18,7 @@ import com.youlai.boot.security.model.UserAuthCredentials; import com.youlai.boot.security.service.PermissionService; import com.youlai.boot.security.token.TokenManager; import com.youlai.boot.security.util.SecurityUtils; +import com.youlai.boot.common.tenant.TenantContextHolder; import com.youlai.boot.platform.mail.service.MailService; import com.youlai.boot.system.converter.UserConverter; import com.youlai.boot.system.enums.DictCodeEnum; @@ -78,7 +79,6 @@ public class UserServiceImpl extends ServiceImpl implements Us private final com.youlai.boot.config.property.TenantProperties tenantProperties; - private final com.youlai.boot.system.mapper.UserTenantMapper userTenantMapper; /** * 获取用户分页列表 @@ -126,17 +126,26 @@ public class UserServiceImpl extends ServiceImpl implements Us public boolean saveUser(UserForm userForm) { String username = userForm.getUsername(); - - long count = this.count(new LambdaQueryWrapper().eq(User::getUsername, username)); - Assert.isTrue(count == 0, "用户名已存在"); - + // 实体转换 form->entity User entity = userConverter.toEntity(userForm); + + // 获取当前操作员的租户ID(新增用户时,租户ID由 MyMetaObjectHandler 自动填充) + Long tenantId = TenantContextHolder.getTenantId(); + Assert.notNull(tenantId, "租户ID不能为空"); + + // 检查同一租户下用户名是否已存在(新设计:用户名在租户内唯一) + long count = this.count(new LambdaQueryWrapper() + .eq(User::getUsername, username) + .eq(User::getTenantId, tenantId)); + Assert.isTrue(count == 0, "该租户下用户名已存在"); // 设置默认加密密码 String defaultEncryptPwd = passwordEncoder.encode(SystemConstants.DEFAULT_PASSWORD); entity.setPassword(defaultEncryptPwd); entity.setCreateBy(SecurityUtils.getUserId()); + + // 注意:租户ID由 MyMetaObjectHandler.insertFill() 自动填充,无需手动设置 // 新增用户 boolean result = this.save(entity); @@ -144,11 +153,6 @@ public class UserServiceImpl extends ServiceImpl implements Us if (result) { // 保存用户角色 userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds()); - - // 如果启用多租户,保存用户租户关联 - if (Boolean.TRUE.equals(tenantProperties.getEnabled())) { - saveUserTenantRelation(entity.getId(), entity.getTenantId(), true); - } } return result; } @@ -165,20 +169,32 @@ public class UserServiceImpl extends ServiceImpl implements Us public boolean updateUser(Long userId, UserForm userForm) { String username = userForm.getUsername(); + + // 获取原用户信息 + User oldUser = this.getById(userId); + Assert.notNull(oldUser, "用户不存在"); + + Long oldTenantId = oldUser.getTenantId(); + Long currentTenantId = TenantContextHolder.getTenantId(); + + // 验证:只能修改当前租户下的用户(防止跨租户修改) + Assert.isTrue(oldTenantId != null && oldTenantId.equals(currentTenantId), + "只能修改当前租户下的用户"); + // 检查同一租户下用户名是否已存在(排除当前用户) long count = this.count(new LambdaQueryWrapper() .eq(User::getUsername, username) + .eq(User::getTenantId, currentTenantId) .ne(User::getId, userId) ); - Assert.isTrue(count == 0, "用户名已存在"); - - // 获取原用户信息,用于比较租户是否变更 - User oldUser = this.getById(userId); - Long oldTenantId = oldUser != null ? oldUser.getTenantId() : null; + Assert.isTrue(count == 0, "该租户下用户名已存在"); // form -> entity User entity = userConverter.toEntity(userForm); entity.setUpdateBy(SecurityUtils.getUserId()); + + // 保持租户ID不变(不允许跨租户修改用户) + entity.setTenantId(oldTenantId); // 修改用户 boolean result = this.updateById(entity); @@ -186,23 +202,6 @@ public class UserServiceImpl extends ServiceImpl implements Us if (result) { // 保存用户角色 userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds()); - - // 如果启用多租户且租户发生变更,更新用户租户关联 - if (Boolean.TRUE.equals(tenantProperties.getEnabled())) { - Long newTenantId = entity.getTenantId(); - if (newTenantId != null && !newTenantId.equals(oldTenantId)) { - // 删除旧的租户关联 - if (oldTenantId != null) { - userTenantMapper.delete( - new LambdaQueryWrapper() - .eq(com.youlai.boot.system.model.entity.UserTenant::getUserId, userId) - .eq(com.youlai.boot.system.model.entity.UserTenant::getTenantId, oldTenantId) - ); - } - // 保存新的租户关联 - saveUserTenantRelation(userId, newTenantId, true); - } - } } return result; } @@ -224,16 +223,7 @@ public class UserServiceImpl extends ServiceImpl implements Us boolean result = this.removeByIds(ids); - // 如果启用多租户,删除用户租户关联 - if (result && Boolean.TRUE.equals(tenantProperties.getEnabled())) { - for (Long userId : ids) { - userTenantMapper.delete( - new LambdaQueryWrapper() - .eq(com.youlai.boot.system.model.entity.UserTenant::getUserId, userId) - ); - log.info("删除用户租户关联:userId={}", userId); - } - } + // 新设计:用户删除时,tenant_id 字段会随用户记录一起逻辑删除,无需额外处理 return result; } @@ -256,6 +246,46 @@ public class UserServiceImpl extends ServiceImpl implements Us return userAuthCredentials; } + @Override + public UserAuthCredentials getAuthCredentialsByUsernameAndTenant(String username, Long tenantId) { + // 临时忽略租户过滤,查询指定租户下的用户 + TenantContextHolder.setIgnoreTenant(true); + try { + // 先查询用户 + User user = this.getOne( + new LambdaQueryWrapper() + .eq(User::getUsername, username) + .eq(User::getTenantId, tenantId) + .eq(User::getIsDeleted, 0) + .last("LIMIT 1") + ); + if (user == null) { + return null; + } + // 设置租户上下文,然后查询认证信息(这样会包含该租户下的角色) + TenantContextHolder.setTenantId(tenantId); + return getAuthCredentialsByUsername(username); + } finally { + TenantContextHolder.setIgnoreTenant(false); + } + } + + @Override + public List listUsersByUsername(String username) { + // 临时忽略租户过滤,查询该用户名在所有租户下的记录 + TenantContextHolder.setIgnoreTenant(true); + try { + return this.list( + new LambdaQueryWrapper() + .eq(User::getUsername, username) + .eq(User::getIsDeleted, 0) + .orderByAsc(User::getTenantId) // 按租户ID排序,优先返回较小的租户ID + ); + } finally { + TenantContextHolder.setIgnoreTenant(false); + } + } + /** * 根据OpenID获取用户认证信息 * @@ -731,45 +761,5 @@ public class UserServiceImpl extends ServiceImpl implements Us return userConverter.toOptions(list); } - /** - * 保存用户租户关联关系 - *

- * 仅在启用多租户时调用此方法 - *

- * - * @param userId 用户ID - * @param tenantId 租户ID - * @param isDefault 是否为默认租户 - */ - private void saveUserTenantRelation(Long userId, Long tenantId, boolean isDefault) { - if (userId == null || tenantId == null) { - log.warn("用户ID或租户ID为空,跳过保存用户租户关联"); - return; - } - - // 检查关联是否已存在 - com.youlai.boot.system.model.entity.UserTenant existingRelation = userTenantMapper.selectOne( - new LambdaQueryWrapper() - .eq(com.youlai.boot.system.model.entity.UserTenant::getUserId, userId) - .eq(com.youlai.boot.system.model.entity.UserTenant::getTenantId, tenantId) - ); - - if (existingRelation != null) { - // 如果已存在,更新 is_default 标识 - if (isDefault && existingRelation.getIsDefault() != 1) { - existingRelation.setIsDefault(1); - userTenantMapper.updateById(existingRelation); - log.info("更新用户租户关联:userId={}, tenantId={}, isDefault=true", userId, tenantId); - } - } else { - // 不存在则新增 - com.youlai.boot.system.model.entity.UserTenant userTenant = new com.youlai.boot.system.model.entity.UserTenant(); - userTenant.setUserId(userId); - userTenant.setTenantId(tenantId); - userTenant.setIsDefault(isDefault ? 1 : 0); - userTenantMapper.insert(userTenant); - log.info("保存用户租户关联:userId={}, tenantId={}, isDefault={}", userId, tenantId, isDefault); - } - } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 09335b47..38af018b 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -11,8 +11,8 @@ spring: # === MySQL 数据源(默认启用) === driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://www.youlai.tech:3306/youlai_admin?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true - username: youlai - password: 123456 + username: root + password: Youlai@2025 # === PostgreSQL 数据源示例(按需启用) === # driver-class-name: org.postgresql.Driver @@ -275,7 +275,7 @@ youlai: tenant: # 是否启用多租户功能(默认:false) # 设置为 true 启用多租户,设置为 false 禁用多租户(零成本切换) - enabled: false + enabled: true # 租户字段名(默认:tenant_id) column: tenant_id diff --git a/src/main/resources/codegen.yml b/src/main/resources/codegen.yml index 134ddfd4..ceaf271a 100644 --- a/src/main/resources/codegen.yml +++ b/src/main/resources/codegen.yml @@ -13,8 +13,8 @@ codegen: moduleName: system # 排除数据表 excludeTables: - - gen_config - - gen_field_config + - gen_table + - gen_table_column ## 模板配置 templateConfigs: API: diff --git a/src/main/resources/mapper/ai/AiCommandLogMapper.xml b/src/main/resources/mapper/ai/AiCommandRecordMapper.xml similarity index 63% rename from src/main/resources/mapper/ai/AiCommandLogMapper.xml rename to src/main/resources/mapper/ai/AiCommandRecordMapper.xml index 55a12a2b..1755acdc 100644 --- a/src/main/resources/mapper/ai/AiCommandLogMapper.xml +++ b/src/main/resources/mapper/ai/AiCommandRecordMapper.xml @@ -2,9 +2,9 @@ - + - + @@ -28,55 +28,55 @@ - SELECT - acl.id, - acl.user_id, - acl.username, - acl.original_command, - acl.ai_provider, - acl.ai_model, - acl.parse_status, - acl.function_calls, - acl.explanation, - acl.confidence, - acl.parse_error_message, - acl.input_tokens, - acl.output_tokens, - acl.parse_duration_ms, - acl.function_name, - acl.function_arguments, - acl.execute_status, - acl.execute_error_message, - acl.ip_address, - acl.create_time, - acl.update_time - FROM ai_command_log acl + acr.id, + acr.user_id, + acr.username, + acr.original_command, + acr.ai_provider, + acr.ai_model, + acr.parse_status, + acr.function_calls, + acr.explanation, + acr.confidence, + acr.parse_error_message, + acr.input_tokens, + acr.output_tokens, + acr.parse_duration_ms, + acr.function_name, + acr.function_arguments, + acr.execute_status, + acr.execute_error_message, + acr.ip_address, + acr.create_time, + acr.update_time + FROM ai_command_record acr ( - acl.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%') - OR acl.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%') - OR acl.username LIKE CONCAT('%', #{queryParams.keywords}, '%') + acr.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%') + OR acr.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%') + OR acr.username LIKE CONCAT('%', #{queryParams.keywords}, '%') ) - AND acl.execute_status = #{queryParams.executeStatus} + AND acr.execute_status = #{queryParams.executeStatus} - AND acl.user_id = #{queryParams.userId} + AND acr.user_id = #{queryParams.userId} - AND acl.parse_status = #{queryParams.parseStatus} + AND acr.parse_status = #{queryParams.parseStatus} - AND acl.function_name = #{queryParams.functionName} + AND acr.function_name = #{queryParams.functionName} - AND acl.ai_provider = #{queryParams.aiProvider} + AND acr.ai_provider = #{queryParams.aiProvider} - AND acl.ai_model = #{queryParams.aiModel} + AND acr.ai_model = #{queryParams.aiModel} - AND acl.create_time BETWEEN #{queryParams.createTime[0]} AND #{queryParams.createTime[1]} + AND acr.create_time BETWEEN #{queryParams.createTime[0]} AND #{queryParams.createTime[1]} - ORDER BY acl.create_time DESC + ORDER BY acr.create_time DESC diff --git a/src/main/resources/mapper/codegen/DatabaseMapper.xml b/src/main/resources/mapper/codegen/DatabaseMapper.xml index c2aefee0..0e402a8d 100644 --- a/src/main/resources/mapper/codegen/DatabaseMapper.xml +++ b/src/main/resources/mapper/codegen/DatabaseMapper.xml @@ -16,7 +16,7 @@ CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured FROM information_schema.tables t1 - LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name + LEFT JOIN gen_table t2 on t1.TABLE_NAME = t2.table_name WHERE t1.TABLE_SCHEMA = (SELECT DATABASE()) AND t1.table_type = 'BASE TABLE' @@ -46,7 +46,7 @@ ALL_TABLES t1 LEFT JOIN ALL_TAB_COMMENTS c ON t1.TABLE_NAME = c.TABLE_NAME AND t1.OWNER = c.OWNER LEFT JOIN ALL_OBJECTS o ON t1.TABLE_NAME = o.OBJECT_NAME AND t1.OWNER = o.OWNER AND o.OBJECT_TYPE = 'TABLE' - LEFT JOIN gen_config t2 ON t1.TABLE_NAME = t2.table_name + LEFT JOIN gen_table t2 ON t1.TABLE_NAME = t2.table_name WHERE t1.OWNER = 'YOULAI_BOOT' diff --git a/src/main/resources/mapper/codegen/GenConfigMapper.xml b/src/main/resources/mapper/codegen/GenConfigMapper.xml index 02709a91..0271fa20 100644 --- a/src/main/resources/mapper/codegen/GenConfigMapper.xml +++ b/src/main/resources/mapper/codegen/GenConfigMapper.xml @@ -2,6 +2,6 @@ - + diff --git a/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml b/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml index 0bd4db31..8cb86c3d 100644 --- a/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml +++ b/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml @@ -2,6 +2,6 @@ - + diff --git a/src/main/resources/mapper/codegen/GenTableColumnMapper.xml b/src/main/resources/mapper/codegen/GenTableColumnMapper.xml new file mode 100644 index 00000000..8cb86c3d --- /dev/null +++ b/src/main/resources/mapper/codegen/GenTableColumnMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/src/main/resources/mapper/codegen/GenTableMapper.xml b/src/main/resources/mapper/codegen/GenTableMapper.xml new file mode 100644 index 00000000..0271fa20 --- /dev/null +++ b/src/main/resources/mapper/codegen/GenTableMapper.xml @@ -0,0 +1,7 @@ + + + + +