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