refactor: 多租户开发和代码规范调整
This commit is contained in:
@@ -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
|
||||
-- ============================================
|
||||
|
||||
@@ -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,请先检查字段是否存在,再执行删除操作
|
||||
-- ============================================
|
||||
|
||||
@@ -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,8 +548,8 @@ 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` (
|
||||
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 '用户名',
|
||||
@@ -589,7 +578,7 @@ CREATE TABLE `ai_command_log` (
|
||||
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 命令日志表';
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='AI 命令记录表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 租户表(多租户模式)
|
||||
@@ -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;
|
||||
|
||||
@@ -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,14 +58,64 @@ public class AuthController {
|
||||
@Operation(summary = "账号密码登录")
|
||||
@PostMapping("/login")
|
||||
@Log(value = "登录", module = LogModuleEnum.LOGIN)
|
||||
public Result<AuthenticationToken> login(
|
||||
@Parameter(description = "用户名", example = "admin") @RequestParam String username,
|
||||
@Parameter(description = "密码", example = "123456") @RequestParam String password
|
||||
) {
|
||||
AuthenticationToken authenticationToken = authService.login(username, password);
|
||||
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<User> users = userService.listUsersByUsername(username);
|
||||
|
||||
if (users.isEmpty()) {
|
||||
return Result.failed("用户不存在");
|
||||
}
|
||||
|
||||
// 过滤出正常状态的用户
|
||||
List<User> 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<TenantVO> 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 = "短信验证码登录")
|
||||
@PostMapping("/login/sms")
|
||||
@Log(value = "短信验证码登录", module = LogModuleEnum.LOGIN)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<TenantVO> tenants;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 登出
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@ public interface JwtClaimConstants {
|
||||
*/
|
||||
String AUTHORITIES = "authorities";
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
String TENANT_ID = "tenantId";
|
||||
|
||||
/**
|
||||
* 安全版本号,用于按用户失效历史令牌
|
||||
*/
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
// 数据权限
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
// 2) 如果尚未获取到,尝试从 Token 中解析
|
||||
if (tenantId == null) {
|
||||
tenantId = resolveTenantFromToken(request);
|
||||
}
|
||||
} else {
|
||||
// 如果未提供租户ID,使用默认租户ID
|
||||
|
||||
// 3) 仍为空则使用默认租户
|
||||
if (tenantId == null) {
|
||||
Long defaultTenantId = tenantProperties.getDefaultTenantId();
|
||||
if (defaultTenantId != null) {
|
||||
TenantContextHolder.setTenantId(defaultTenantId);
|
||||
log.debug("使用默认租户ID: {}", defaultTenantId);
|
||||
tenantId = defaultTenantId;
|
||||
}
|
||||
}
|
||||
|
||||
// 继续执行过滤器链
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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强制校验拦截器
|
||||
* <p>
|
||||
* 对于需要租户隔离的接口,强制要求携带有效的租户ID
|
||||
* 防止恶意用户通过不携带租户ID来访问默认租户数据
|
||||
* </p>
|
||||
*
|
||||
* @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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,14 @@ public class Result<T> implements Serializable {
|
||||
return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> failed(IResultCode resultCode, T data) {
|
||||
return result(resultCode.getCode(), resultCode.getMsg(), data);
|
||||
}
|
||||
|
||||
public static <T> Result<T> failed(IResultCode resultCode, String msg, T data) {
|
||||
return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), data);
|
||||
}
|
||||
|
||||
private static <T> Result<T> result(IResultCode resultCode, T data) {
|
||||
return result(resultCode.getCode(), resultCode.getMsg(), data);
|
||||
}
|
||||
|
||||
@@ -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", "访问未授权"),
|
||||
|
||||
@@ -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<AiCommandLogVO> getLogPage(AiCommandPageQuery queryParams) {
|
||||
IPage<AiCommandLogVO> page = logService.getLogPage(queryParams);
|
||||
public PageResult<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams) {
|
||||
IPage<AiCommandRecordVO> 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("撤销成功");
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AiCommandLog> {
|
||||
public interface AiCommandRecordMapper extends BaseMapper<AiCommandRecord> {
|
||||
|
||||
/**
|
||||
* 获取 AI 命令记录分页列表
|
||||
*/
|
||||
IPage<AiCommandLogVO> getLogPage(Page<AiCommandLogVO> page, AiCommandPageQuery queryParams);
|
||||
IPage<AiCommandRecordVO> getRecordPage(Page<AiCommandRecordVO> page, AiCommandPageQuery queryParams);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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<String> createTime;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<AiCommandLog> {
|
||||
public interface AiCommandRecordService extends IService<AiCommandRecord> {
|
||||
|
||||
/**
|
||||
* 获取命令记录分页列表
|
||||
@@ -20,7 +20,7 @@ public interface AiCommandLogService extends IService<AiCommandLog> {
|
||||
* @param queryParams 查询参数
|
||||
* @return 命令记录分页列表
|
||||
*/
|
||||
IPage<AiCommandLogVO> getLogPage(AiCommandPageQuery queryParams);
|
||||
IPage<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams);
|
||||
|
||||
/**
|
||||
* 撤销命令执行
|
||||
@@ -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<AiCommandLogMapper, AiCommandLog>
|
||||
implements AiCommandLogService {
|
||||
public class AiCommandRecordServiceImpl extends ServiceImpl<AiCommandRecordMapper, AiCommandRecord>
|
||||
implements AiCommandRecordService {
|
||||
|
||||
@Override
|
||||
public IPage<AiCommandLogVO> getLogPage(AiCommandPageQuery queryParams) {
|
||||
Page<AiCommandLogVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
|
||||
return this.baseMapper.getLogPage(page, queryParams);
|
||||
public IPage<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams) {
|
||||
Page<AiCommandRecordVO> 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("回滚功能尚未实现");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<GenConfigForm> getGenConfigFormData(
|
||||
public Result<GenConfigForm> 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<GenFieldConfig> fieldConfigs);
|
||||
GenConfigForm toGenConfigForm(GenTable genTable, List<GenTableColumn> fieldConfigs);
|
||||
|
||||
List<GenConfigForm.FieldConfig> toGenFieldConfigForm(List<GenFieldConfig> fieldConfigs);
|
||||
List<GenConfigForm.FieldConfig> toGenTableColumnForm(List<GenTableColumn> fieldConfigs);
|
||||
|
||||
GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig);
|
||||
GenConfigForm.FieldConfig toGenTableColumnForm(GenTableColumn genTableColumn);
|
||||
|
||||
GenConfig toGenConfig(GenConfigForm formData);
|
||||
GenTable toGenTable(GenConfigForm formData);
|
||||
|
||||
List<GenFieldConfig> toGenFieldConfig(List<GenConfigForm.FieldConfig> fieldConfigs);
|
||||
List<GenTableColumn> toGenTableColumn(List<GenConfigForm.FieldConfig> fieldConfigs);
|
||||
|
||||
GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig);
|
||||
GenTableColumn toGenTableColumn(GenConfigForm.FieldConfig fieldConfig);
|
||||
|
||||
}
|
||||
@@ -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<GenFieldConfig> {
|
||||
public interface GenTableColumnMapper extends BaseMapper<GenTableColumn> {
|
||||
|
||||
}
|
||||
|
||||
@@ -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<GenConfig> {
|
||||
public interface GenTableMapper extends BaseMapper<GenTable> {
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
* 表名
|
||||
@@ -62,3 +62,4 @@ public class GenConfig extends BaseEntity {
|
||||
*/
|
||||
private String removeTablePrefix;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<GenFieldConfig> {
|
||||
public interface GenTableColumnService extends IService<GenTableColumn> {
|
||||
|
||||
}
|
||||
@@ -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<GenConfig> {
|
||||
public interface GenTableService extends IService<GenTable> {
|
||||
|
||||
/**
|
||||
* 获取代码生成配置
|
||||
@@ -18,7 +18,7 @@ public interface GenConfigService extends IService<GenConfig> {
|
||||
* @param tableName 表名
|
||||
* @return
|
||||
*/
|
||||
GenConfigForm getGenConfigFormData(String tableName);
|
||||
GenConfigForm getGenTableFormData(String tableName);
|
||||
|
||||
/**
|
||||
* 保存代码生成配置
|
||||
@@ -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<CodegenPreviewVO> list = new ArrayList<>();
|
||||
|
||||
GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper<GenConfig>()
|
||||
.eq(GenConfig::getTableName, tableName)
|
||||
GenTable genTable = genTableService.getOne(new LambdaQueryWrapper<GenTable>()
|
||||
.eq(GenTable::getTableName, tableName)
|
||||
);
|
||||
if (genConfig == null) {
|
||||
if (genTable == null) {
|
||||
throw new BusinessException("未找到表生成配置");
|
||||
}
|
||||
|
||||
List<GenFieldConfig> fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper<GenFieldConfig>()
|
||||
.eq(GenFieldConfig::getConfigId, genConfig.getId())
|
||||
.orderByAsc(GenFieldConfig::getFieldSort)
|
||||
List<GenTableColumn> fieldConfigs = genTableColumnService.list(new LambdaQueryWrapper<GenTableColumn>()
|
||||
.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<GenFieldConfig> fieldConfigs, String pageType) {
|
||||
private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenTable genTable, List<GenTableColumn> fieldConfigs, String pageType) {
|
||||
|
||||
Map<String, Object> 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;
|
||||
|
||||
@@ -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<GenFieldConfigMapper, GenFieldConfig> implements GenFieldConfigService {
|
||||
|
||||
|
||||
}
|
||||
@@ -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<GenTableColumnMapper, GenTableColumn> implements GenTableColumnService {
|
||||
|
||||
|
||||
}
|
||||
@@ -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<GenConfigMapper, GenConfig> implements GenConfigService {
|
||||
public class GenTableServiceImpl extends ServiceImpl<GenTableMapper, GenTable> 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<GenConfigMapper, GenConfig
|
||||
* @return 代码生成配置
|
||||
*/
|
||||
@Override
|
||||
public GenConfigForm getGenConfigFormData(String tableName) {
|
||||
public GenConfigForm getGenTableFormData(String tableName) {
|
||||
// 查询表生成配置
|
||||
GenConfig genConfig = this.getOne(
|
||||
new LambdaQueryWrapper<>(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<GenFieldConfig> genFieldConfigs = new ArrayList<>();
|
||||
List<GenTableColumn> genTableColumns = new ArrayList<>();
|
||||
|
||||
// 获取表的列
|
||||
List<ColumnMetaData> tableColumns = databaseMapper.getTableColumns(tableName);
|
||||
if (CollectionUtil.isNotEmpty(tableColumns)) {
|
||||
// 查询字段生成配置
|
||||
List<GenFieldConfig> fieldConfigList = genFieldConfigService.list(
|
||||
new LambdaQueryWrapper<GenFieldConfig>()
|
||||
.eq(GenFieldConfig::getConfigId, genConfig.getId())
|
||||
.orderByAsc(GenFieldConfig::getFieldSort)
|
||||
List<GenTableColumn> fieldConfigList = genTableColumnService.list(
|
||||
new LambdaQueryWrapper<GenTableColumn>()
|
||||
.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<GenConfigMapper, GenConfig
|
||||
fieldConfig.setFieldType(javaType);
|
||||
}
|
||||
// 如果没有代码生成配置,则默认展示在列表和表单
|
||||
if (!hasGenConfig) {
|
||||
if (!hasGenTable) {
|
||||
fieldConfig.setIsShowInList(1);
|
||||
fieldConfig.setIsShowInForm(1);
|
||||
}
|
||||
genFieldConfigs.add(fieldConfig);
|
||||
genTableColumns.add(fieldConfig);
|
||||
}
|
||||
}
|
||||
// 对 genFieldConfigs 按照 fieldSort 排序
|
||||
genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList();
|
||||
GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs);
|
||||
// 对 genTableColumns 按照 fieldSort 排序
|
||||
genTableColumns = genTableColumns.stream().sorted(Comparator.comparing(GenTableColumn::getFieldSort)).toList();
|
||||
GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genTable, genTableColumns);
|
||||
|
||||
genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName());
|
||||
genConfigForm.setBackendAppName(codegenProperties.getBackendAppName());
|
||||
@@ -153,8 +153,8 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
||||
* @param columnMetaData 表字段元数据
|
||||
* @return
|
||||
*/
|
||||
private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) {
|
||||
GenFieldConfig fieldConfig = new GenFieldConfig();
|
||||
private GenTableColumn createDefaultFieldConfig(ColumnMetaData columnMetaData) {
|
||||
GenTableColumn fieldConfig = new GenTableColumn();
|
||||
fieldConfig.setColumnName(columnMetaData.getColumnName());
|
||||
fieldConfig.setColumnType(columnMetaData.getDataType());
|
||||
fieldConfig.setFieldComment(columnMetaData.getColumnComment());
|
||||
@@ -181,24 +181,24 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
||||
*/
|
||||
@Override
|
||||
public void saveGenConfig(GenConfigForm formData) {
|
||||
GenConfig genConfig = codegenConverter.toGenConfig(formData);
|
||||
this.saveOrUpdate(genConfig);
|
||||
GenTable genTable = codegenConverter.toGenTable(formData);
|
||||
this.saveOrUpdate(genTable);
|
||||
|
||||
// 如果选择上级菜单且当前环境不是生产环境,则保存菜单
|
||||
Long parentMenuId = formData.getParentMenuId();
|
||||
if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) {
|
||||
menuService.addMenuForCodegen(parentMenuId, genConfig);
|
||||
menuService.addMenuForCodegen(parentMenuId, genTable);
|
||||
}
|
||||
|
||||
List<GenFieldConfig> genFieldConfigs = codegenConverter.toGenFieldConfig(formData.getFieldConfigs());
|
||||
List<GenTableColumn> 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<GenConfigMapper, GenConfig
|
||||
*/
|
||||
@Override
|
||||
public void deleteGenConfig(String tableName) {
|
||||
GenConfig genConfig = this.getOne(new LambdaQueryWrapper<GenConfig>()
|
||||
.eq(GenConfig::getTableName, tableName));
|
||||
GenTable genTable = this.getOne(new LambdaQueryWrapper<GenTable>()
|
||||
.eq(GenTable::getTableName, tableName));
|
||||
|
||||
boolean result = this.remove(new LambdaQueryWrapper<GenConfig>()
|
||||
.eq(GenConfig::getTableName, tableName)
|
||||
boolean result = this.remove(new LambdaQueryWrapper<GenTable>()
|
||||
.eq(GenTable::getTableName, tableName)
|
||||
);
|
||||
if (result) {
|
||||
genFieldConfigService.remove(new LambdaQueryWrapper<GenFieldConfig>()
|
||||
.eq(GenFieldConfig::getConfigId, genConfig.getId())
|
||||
genTableColumnService.remove(new LambdaQueryWrapper<GenTableColumn>()
|
||||
.eq(GenTableColumn::getTableId, genTable.getId())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
|
||||
* 支持自动填充创建时间、更新时间和租户ID
|
||||
* </p>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String, Object> redisTemplate;
|
||||
|
||||
private final CodeGenerator codeGenerator;
|
||||
|
||||
public CaptchaValidationFilter(RedisTemplate<String, Object> 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)) {
|
||||
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
// 非登录接口直接放行
|
||||
if (!LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
|
||||
chain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
// 缓存中的验证码
|
||||
String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME);
|
||||
|
||||
// 仅支持 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, verifyCodeKey)
|
||||
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId)
|
||||
);
|
||||
if (cacheVerifyCode == null) {
|
||||
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED);
|
||||
} else {
|
||||
// 验证码比对
|
||||
return;
|
||||
}
|
||||
|
||||
if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
|
||||
chain.doFilter(request, response);
|
||||
HttpServletRequest repeatableRequest = new RepeatableReadRequestWrapper(requestWrapper, bodyBytes);
|
||||
chain.doFilter(repeatableRequest, response);
|
||||
} else {
|
||||
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 非登录接口放行
|
||||
chain.doFilter(request, response);
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,11 @@ public class OnlineUser {
|
||||
*/
|
||||
private Integer dataScope;
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 角色权限集合
|
||||
*/
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -54,4 +54,9 @@ public class UserAuthCredentials {
|
||||
*/
|
||||
private Integer dataScope;
|
||||
|
||||
/**
|
||||
* 租户ID(从登录上下文中获取)
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import java.util.*;
|
||||
/**
|
||||
* SpringSecurity 权限校验
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/2/22
|
||||
* @author Ray.Hao
|
||||
* @since 0.0.1
|
||||
*/
|
||||
@Component("ss")
|
||||
@RequiredArgsConstructor
|
||||
|
||||
@@ -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) {
|
||||
// 记录异常日志
|
||||
|
||||
@@ -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<String> roles = authentication.getAuthorities().stream()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<List<TenantVO>> getTenantList() {
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
List<TenantVO> tenantList = tenantService.getTenantListByUserId(userId);
|
||||
@@ -72,14 +72,13 @@ public class TenantController {
|
||||
* 切换租户
|
||||
* <p>
|
||||
* 切换当前用户的租户上下文,需要验证用户是否有权限访问该租户
|
||||
* 并记录审计日志
|
||||
* </p>
|
||||
*
|
||||
* @param tenantId 目标租户ID
|
||||
* @return 切换结果
|
||||
*/
|
||||
@Operation(summary = "切换租户")
|
||||
@PostMapping("/switch/{tenantId}")
|
||||
@PostMapping("/{tenantId}/switch")
|
||||
public Result<TenantVO> 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);
|
||||
log.warn("用户 {} 尝试切换到不存在的租户 {}", userId, tenantId);
|
||||
return Result.failed("租户不存在");
|
||||
}
|
||||
if (tenant.getStatus() == null || tenant.getStatus() != 1) {
|
||||
tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, "租户已禁用", request);
|
||||
log.warn("用户 {} 尝试切换到已禁用的租户 {}", userId, tenantId);
|
||||
return Result.failed("租户已禁用");
|
||||
}
|
||||
|
||||
// 设置新的租户上下文
|
||||
TenantContextHolder.setTenantId(tenantId);
|
||||
|
||||
// 记录成功日志
|
||||
tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, true, null, request);
|
||||
|
||||
log.info("用户 {} 成功切换租户到 {}", userId, tenantId);
|
||||
log.info("用户 {} 成功切换租户:{} -> {}", userId, fromTenantId, 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TenantSwitchLog> {
|
||||
}
|
||||
@@ -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<UserTenant> {
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<Menu> {
|
||||
* @param parentMenuId 父菜单ID
|
||||
* @param genConfig 实体名
|
||||
*/
|
||||
void addMenuForCodegen(Long parentMenuId, GenConfig genConfig);
|
||||
void addMenuForCodegen(Long parentMenuId, GenTable genTable);
|
||||
}
|
||||
|
||||
@@ -46,17 +46,4 @@ public interface TenantService extends IService<Tenant> {
|
||||
* @return true-有权限,false-无权限
|
||||
*/
|
||||
boolean hasTenantPermission(Long userId, Long tenantId);
|
||||
|
||||
/**
|
||||
* 记录租户切换审计日志
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param fromTenantId 原租户ID
|
||||
* @param toTenantId 目标租户ID
|
||||
* @param success 是否成功
|
||||
* @param failReason 失败原因
|
||||
* @param request HTTP请求对象
|
||||
*/
|
||||
void recordTenantSwitch(Long userId, Long fromTenantId, Long toTenantId,
|
||||
boolean success, String failReason, jakarta.servlet.http.HttpServletRequest request);
|
||||
}
|
||||
|
||||
@@ -71,9 +71,25 @@ public interface UserService extends IService<User> {
|
||||
* @param username 用户名
|
||||
* @return {@link UserAuthCredentials}
|
||||
*/
|
||||
|
||||
UserAuthCredentials getAuthCredentialsByUsername(String username);
|
||||
|
||||
/**
|
||||
* 根据用户名和租户ID获取认证信息(用于多租户登录)
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param tenantId 租户ID
|
||||
* @return {@link UserAuthCredentials}
|
||||
*/
|
||||
UserAuthCredentials getAuthCredentialsByUsernameAndTenant(String username, Long tenantId);
|
||||
|
||||
/**
|
||||
* 根据用户名查询该用户在所有租户下的记录(用于多租户登录时判断是否需要选择租户)
|
||||
*
|
||||
* @param username 用户名
|
||||
* @return 用户列表(每个租户一条记录)
|
||||
*/
|
||||
List<User> listUsersByUsername(String username);
|
||||
|
||||
|
||||
/**
|
||||
* 获取导出用户列表
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenConfig;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
import com.youlai.boot.system.converter.MenuConverter;
|
||||
import com.youlai.boot.system.mapper.MenuMapper;
|
||||
@@ -427,14 +427,14 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
||||
* 代码生成时添加菜单
|
||||
*
|
||||
* @param parentMenuId 父菜单ID
|
||||
* @param genConfig 实体名称
|
||||
* @param genTable 实体名称
|
||||
*/
|
||||
@Override
|
||||
public void addMenuForCodegen(Long parentMenuId, GenConfig genConfig) {
|
||||
public void addMenuForCodegen(Long parentMenuId, GenTable genTable) {
|
||||
Menu parentMenu = this.getById(parentMenuId);
|
||||
Assert.notNull(parentMenu, "上级菜单不存在");
|
||||
|
||||
String entityName = genConfig.getEntityName();
|
||||
String entityName = genTable.getEntityName();
|
||||
|
||||
long count = this.count(new LambdaQueryWrapper<Menu>().eq(Menu::getRouteName, entityName));
|
||||
if (count > 0) {
|
||||
@@ -453,11 +453,11 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
||||
|
||||
Menu menu = new Menu();
|
||||
menu.setParentId(parentMenuId);
|
||||
menu.setName(genConfig.getBusinessName());
|
||||
menu.setName(genTable.getBusinessName());
|
||||
|
||||
menu.setRouteName(entityName);
|
||||
menu.setRoutePath(StrUtil.toSymbolCase(entityName, '-'));
|
||||
menu.setComponent(genConfig.getModuleName() + "/" + StrUtil.toSymbolCase(entityName, '-') + "/index");
|
||||
menu.setComponent(genTable.getModuleName() + "/" + StrUtil.toSymbolCase(entityName, '-') + "/index");
|
||||
menu.setType(MenuTypeEnum.MENU.getValue());
|
||||
menu.setSort(sort);
|
||||
menu.setVisible(1);
|
||||
@@ -470,7 +470,7 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
||||
this.updateById(menu);
|
||||
|
||||
// 生成CURD按钮权限
|
||||
String permPrefix = genConfig.getModuleName() + ":" + genConfig.getTableName().replace("_", "-") + ":";
|
||||
String permPrefix = genTable.getModuleName() + ":" + genTable.getTableName().replace("_", "-") + ":";
|
||||
String[] actions = {"查询", "新增", "修改", "删除"};
|
||||
String[] perms = {"list", "create", "update", "delete"};
|
||||
|
||||
|
||||
@@ -1,28 +1,22 @@
|
||||
package com.youlai.boot.system.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.common.tenant.TenantContextHolder;
|
||||
import com.youlai.boot.system.mapper.TenantMapper;
|
||||
import com.youlai.boot.system.mapper.TenantSwitchLogMapper;
|
||||
import com.youlai.boot.system.mapper.UserMapper;
|
||||
import com.youlai.boot.system.mapper.UserTenantMapper;
|
||||
import com.youlai.boot.system.model.entity.Tenant;
|
||||
import com.youlai.boot.system.model.entity.TenantSwitchLog;
|
||||
import com.youlai.boot.system.model.entity.User;
|
||||
import com.youlai.boot.system.model.entity.UserTenant;
|
||||
import com.youlai.boot.system.model.vo.TenantVO;
|
||||
import com.youlai.boot.system.service.TenantService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* 租户服务实现类
|
||||
@@ -35,8 +29,6 @@ import java.util.stream.Collectors;
|
||||
@RequiredArgsConstructor
|
||||
public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> implements TenantService {
|
||||
|
||||
private final UserTenantMapper userTenantMapper;
|
||||
private final TenantSwitchLogMapper tenantSwitchLogMapper;
|
||||
private final UserMapper userMapper;
|
||||
|
||||
@Override
|
||||
@@ -44,21 +36,34 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
||||
// 临时忽略租户过滤,查询所有租户
|
||||
TenantContextHolder.setIgnoreTenant(true);
|
||||
try {
|
||||
// 查询用户关联的租户ID列表
|
||||
List<UserTenant> userTenants = userTenantMapper.selectList(
|
||||
new LambdaQueryWrapper<UserTenant>()
|
||||
.eq(UserTenant::getUserId, userId)
|
||||
);
|
||||
|
||||
if (userTenants.isEmpty()) {
|
||||
// 先根据用户ID查询用户信息(获取 username)
|
||||
User user = userMapper.selectById(userId);
|
||||
if (user == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// 提取租户ID列表
|
||||
List<Long> tenantIds = userTenants.stream()
|
||||
.map(UserTenant::getTenantId)
|
||||
// 通过 username 查询该用户在所有租户下的记录,获取租户ID列表
|
||||
List<User> users = userMapper.selectList(
|
||||
new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, user.getUsername())
|
||||
.eq(User::getIsDeleted, 0)
|
||||
);
|
||||
|
||||
if (users.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// 提取租户ID列表(去重)
|
||||
List<Long> tenantIds = users.stream()
|
||||
.map(User::getTenantId)
|
||||
.filter(tenantId -> tenantId != null)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (tenantIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// 查询租户信息
|
||||
List<Tenant> tenants = this.list(
|
||||
new LambdaQueryWrapper<Tenant>()
|
||||
@@ -67,17 +72,19 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
||||
.orderByDesc(Tenant::getId)
|
||||
);
|
||||
|
||||
// 转换为VO并标记默认租户
|
||||
return tenants.stream().map(tenant -> {
|
||||
// 转换为VO,第一个租户作为默认租户
|
||||
return IntStream.range(0, tenants.size())
|
||||
.mapToObj(index -> {
|
||||
Tenant tenant = tenants.get(index);
|
||||
TenantVO vo = new TenantVO();
|
||||
BeanUtils.copyProperties(tenant, vo);
|
||||
// 查找是否为默认租户
|
||||
userTenants.stream()
|
||||
.filter(ut -> ut.getTenantId().equals(tenant.getId()) && ut.getIsDefault() == 1)
|
||||
.findFirst()
|
||||
.ifPresent(ut -> vo.setIsDefault(true));
|
||||
// 第一个租户作为默认租户
|
||||
if (index == 0) {
|
||||
vo.setIsDefault(true);
|
||||
}
|
||||
return vo;
|
||||
}).collect(Collectors.toList());
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
} finally {
|
||||
TenantContextHolder.setIgnoreTenant(false);
|
||||
}
|
||||
@@ -119,94 +126,25 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
||||
public boolean hasTenantPermission(Long userId, Long tenantId) {
|
||||
TenantContextHolder.setIgnoreTenant(true);
|
||||
try {
|
||||
UserTenant userTenant = userTenantMapper.selectOne(
|
||||
new LambdaQueryWrapper<UserTenant>()
|
||||
.eq(UserTenant::getUserId, userId)
|
||||
.eq(UserTenant::getTenantId, tenantId)
|
||||
// 先根据用户ID查询用户信息(获取 username)
|
||||
User user = userMapper.selectById(userId);
|
||||
if (user == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查该 username 在指定租户下是否存在用户记录
|
||||
User tenantUser = userMapper.selectOne(
|
||||
new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, user.getUsername())
|
||||
.eq(User::getTenantId, tenantId)
|
||||
.eq(User::getIsDeleted, 0)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
return userTenant != null;
|
||||
return tenantUser != null;
|
||||
} finally {
|
||||
TenantContextHolder.setIgnoreTenant(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordTenantSwitch(Long userId, Long fromTenantId, Long toTenantId,
|
||||
boolean success, String failReason, HttpServletRequest request) {
|
||||
try {
|
||||
// 临时忽略租户过滤,确保日志可以写入
|
||||
TenantContextHolder.setIgnoreTenant(true);
|
||||
|
||||
// 创建审计日志
|
||||
TenantSwitchLog log = new TenantSwitchLog();
|
||||
log.setUserId(userId);
|
||||
log.setFromTenantId(fromTenantId);
|
||||
log.setToTenantId(toTenantId);
|
||||
log.setSwitchTime(LocalDateTime.now());
|
||||
log.setStatus(success ? 1 : 0);
|
||||
log.setFailReason(failReason);
|
||||
|
||||
// 获取用户名
|
||||
if (userId != null) {
|
||||
User user = userMapper.selectById(userId);
|
||||
if (user != null) {
|
||||
log.setUsername(user.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
// 获取租户名称
|
||||
if (fromTenantId != null) {
|
||||
Tenant fromTenant = this.getById(fromTenantId);
|
||||
if (fromTenant != null) {
|
||||
log.setFromTenantName(fromTenant.getName());
|
||||
}
|
||||
}
|
||||
if (toTenantId != null) {
|
||||
Tenant toTenant = this.getById(toTenantId);
|
||||
if (toTenant != null) {
|
||||
log.setToTenantName(toTenant.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// 获取IP地址和User-Agent
|
||||
if (request != null) {
|
||||
log.setIpAddress(getIpAddress(request));
|
||||
log.setUserAgent(request.getHeader("User-Agent"));
|
||||
}
|
||||
|
||||
// 保存审计日志
|
||||
tenantSwitchLogMapper.insert(log);
|
||||
} catch (Exception e) {
|
||||
// 记录日志失败不应影响业务,仅记录错误
|
||||
Slf4j.getLogger(this.getClass()).error("记录租户切换日志失败", e);
|
||||
} finally {
|
||||
TenantContextHolder.setIgnoreTenant(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP地址
|
||||
*/
|
||||
private String getIpAddress(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
// 处理多级代理的情况
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import com.youlai.boot.security.model.UserAuthCredentials;
|
||||
import com.youlai.boot.security.service.PermissionService;
|
||||
import com.youlai.boot.security.token.TokenManager;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
import com.youlai.boot.common.tenant.TenantContextHolder;
|
||||
import com.youlai.boot.platform.mail.service.MailService;
|
||||
import com.youlai.boot.system.converter.UserConverter;
|
||||
import com.youlai.boot.system.enums.DictCodeEnum;
|
||||
@@ -78,7 +79,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
|
||||
private final com.youlai.boot.config.property.TenantProperties tenantProperties;
|
||||
|
||||
private final com.youlai.boot.system.mapper.UserTenantMapper userTenantMapper;
|
||||
|
||||
/**
|
||||
* 获取用户分页列表
|
||||
@@ -127,28 +127,32 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
|
||||
String username = userForm.getUsername();
|
||||
|
||||
long count = this.count(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
|
||||
Assert.isTrue(count == 0, "用户名已存在");
|
||||
|
||||
// 实体转换 form->entity
|
||||
User entity = userConverter.toEntity(userForm);
|
||||
|
||||
// 获取当前操作员的租户ID(新增用户时,租户ID由 MyMetaObjectHandler 自动填充)
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
Assert.notNull(tenantId, "租户ID不能为空");
|
||||
|
||||
// 检查同一租户下用户名是否已存在(新设计:用户名在租户内唯一)
|
||||
long count = this.count(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, username)
|
||||
.eq(User::getTenantId, tenantId));
|
||||
Assert.isTrue(count == 0, "该租户下用户名已存在");
|
||||
|
||||
// 设置默认加密密码
|
||||
String defaultEncryptPwd = passwordEncoder.encode(SystemConstants.DEFAULT_PASSWORD);
|
||||
entity.setPassword(defaultEncryptPwd);
|
||||
entity.setCreateBy(SecurityUtils.getUserId());
|
||||
|
||||
// 注意:租户ID由 MyMetaObjectHandler.insertFill() 自动填充,无需手动设置
|
||||
|
||||
// 新增用户
|
||||
boolean result = this.save(entity);
|
||||
|
||||
if (result) {
|
||||
// 保存用户角色
|
||||
userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds());
|
||||
|
||||
// 如果启用多租户,保存用户租户关联
|
||||
if (Boolean.TRUE.equals(tenantProperties.getEnabled())) {
|
||||
saveUserTenantRelation(entity.getId(), entity.getTenantId(), true);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -166,43 +170,38 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
|
||||
String username = userForm.getUsername();
|
||||
|
||||
// 获取原用户信息
|
||||
User oldUser = this.getById(userId);
|
||||
Assert.notNull(oldUser, "用户不存在");
|
||||
|
||||
Long oldTenantId = oldUser.getTenantId();
|
||||
Long currentTenantId = TenantContextHolder.getTenantId();
|
||||
|
||||
// 验证:只能修改当前租户下的用户(防止跨租户修改)
|
||||
Assert.isTrue(oldTenantId != null && oldTenantId.equals(currentTenantId),
|
||||
"只能修改当前租户下的用户");
|
||||
|
||||
// 检查同一租户下用户名是否已存在(排除当前用户)
|
||||
long count = this.count(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, username)
|
||||
.eq(User::getTenantId, currentTenantId)
|
||||
.ne(User::getId, userId)
|
||||
);
|
||||
Assert.isTrue(count == 0, "用户名已存在");
|
||||
|
||||
// 获取原用户信息,用于比较租户是否变更
|
||||
User oldUser = this.getById(userId);
|
||||
Long oldTenantId = oldUser != null ? oldUser.getTenantId() : null;
|
||||
Assert.isTrue(count == 0, "该租户下用户名已存在");
|
||||
|
||||
// form -> entity
|
||||
User entity = userConverter.toEntity(userForm);
|
||||
entity.setUpdateBy(SecurityUtils.getUserId());
|
||||
|
||||
// 保持租户ID不变(不允许跨租户修改用户)
|
||||
entity.setTenantId(oldTenantId);
|
||||
|
||||
// 修改用户
|
||||
boolean result = this.updateById(entity);
|
||||
|
||||
if (result) {
|
||||
// 保存用户角色
|
||||
userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds());
|
||||
|
||||
// 如果启用多租户且租户发生变更,更新用户租户关联
|
||||
if (Boolean.TRUE.equals(tenantProperties.getEnabled())) {
|
||||
Long newTenantId = entity.getTenantId();
|
||||
if (newTenantId != null && !newTenantId.equals(oldTenantId)) {
|
||||
// 删除旧的租户关联
|
||||
if (oldTenantId != null) {
|
||||
userTenantMapper.delete(
|
||||
new LambdaQueryWrapper<com.youlai.boot.system.model.entity.UserTenant>()
|
||||
.eq(com.youlai.boot.system.model.entity.UserTenant::getUserId, userId)
|
||||
.eq(com.youlai.boot.system.model.entity.UserTenant::getTenantId, oldTenantId)
|
||||
);
|
||||
}
|
||||
// 保存新的租户关联
|
||||
saveUserTenantRelation(userId, newTenantId, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -224,16 +223,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
|
||||
boolean result = this.removeByIds(ids);
|
||||
|
||||
// 如果启用多租户,删除用户租户关联
|
||||
if (result && Boolean.TRUE.equals(tenantProperties.getEnabled())) {
|
||||
for (Long userId : ids) {
|
||||
userTenantMapper.delete(
|
||||
new LambdaQueryWrapper<com.youlai.boot.system.model.entity.UserTenant>()
|
||||
.eq(com.youlai.boot.system.model.entity.UserTenant::getUserId, userId)
|
||||
);
|
||||
log.info("删除用户租户关联:userId={}", userId);
|
||||
}
|
||||
}
|
||||
// 新设计:用户删除时,tenant_id 字段会随用户记录一起逻辑删除,无需额外处理
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -256,6 +246,46 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
return userAuthCredentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserAuthCredentials getAuthCredentialsByUsernameAndTenant(String username, Long tenantId) {
|
||||
// 临时忽略租户过滤,查询指定租户下的用户
|
||||
TenantContextHolder.setIgnoreTenant(true);
|
||||
try {
|
||||
// 先查询用户
|
||||
User user = this.getOne(
|
||||
new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, username)
|
||||
.eq(User::getTenantId, tenantId)
|
||||
.eq(User::getIsDeleted, 0)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
// 设置租户上下文,然后查询认证信息(这样会包含该租户下的角色)
|
||||
TenantContextHolder.setTenantId(tenantId);
|
||||
return getAuthCredentialsByUsername(username);
|
||||
} finally {
|
||||
TenantContextHolder.setIgnoreTenant(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> listUsersByUsername(String username) {
|
||||
// 临时忽略租户过滤,查询该用户名在所有租户下的记录
|
||||
TenantContextHolder.setIgnoreTenant(true);
|
||||
try {
|
||||
return this.list(
|
||||
new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, username)
|
||||
.eq(User::getIsDeleted, 0)
|
||||
.orderByAsc(User::getTenantId) // 按租户ID排序,优先返回较小的租户ID
|
||||
);
|
||||
} finally {
|
||||
TenantContextHolder.setIgnoreTenant(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据OpenID获取用户认证信息
|
||||
*
|
||||
@@ -731,45 +761,5 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
return userConverter.toOptions(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户租户关联关系
|
||||
* <p>
|
||||
* 仅在启用多租户时调用此方法
|
||||
* </p>
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID
|
||||
* @param isDefault 是否为默认租户
|
||||
*/
|
||||
private void saveUserTenantRelation(Long userId, Long tenantId, boolean isDefault) {
|
||||
if (userId == null || tenantId == null) {
|
||||
log.warn("用户ID或租户ID为空,跳过保存用户租户关联");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查关联是否已存在
|
||||
com.youlai.boot.system.model.entity.UserTenant existingRelation = userTenantMapper.selectOne(
|
||||
new LambdaQueryWrapper<com.youlai.boot.system.model.entity.UserTenant>()
|
||||
.eq(com.youlai.boot.system.model.entity.UserTenant::getUserId, userId)
|
||||
.eq(com.youlai.boot.system.model.entity.UserTenant::getTenantId, tenantId)
|
||||
);
|
||||
|
||||
if (existingRelation != null) {
|
||||
// 如果已存在,更新 is_default 标识
|
||||
if (isDefault && existingRelation.getIsDefault() != 1) {
|
||||
existingRelation.setIsDefault(1);
|
||||
userTenantMapper.updateById(existingRelation);
|
||||
log.info("更新用户租户关联:userId={}, tenantId={}, isDefault=true", userId, tenantId);
|
||||
}
|
||||
} else {
|
||||
// 不存在则新增
|
||||
com.youlai.boot.system.model.entity.UserTenant userTenant = new com.youlai.boot.system.model.entity.UserTenant();
|
||||
userTenant.setUserId(userId);
|
||||
userTenant.setTenantId(tenantId);
|
||||
userTenant.setIsDefault(isDefault ? 1 : 0);
|
||||
userTenantMapper.insert(userTenant);
|
||||
log.info("保存用户租户关联:userId={}, tenantId={}, isDefault={}", userId, tenantId, isDefault);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ spring:
|
||||
# === MySQL 数据源(默认启用) ===
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
url: jdbc:mysql://www.youlai.tech:3306/youlai_admin?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
|
||||
username: youlai
|
||||
password: 123456
|
||||
username: root
|
||||
password: Youlai@2025
|
||||
|
||||
# === PostgreSQL 数据源示例(按需启用) ===
|
||||
# driver-class-name: org.postgresql.Driver
|
||||
@@ -275,7 +275,7 @@ youlai:
|
||||
tenant:
|
||||
# 是否启用多租户功能(默认:false)
|
||||
# 设置为 true 启用多租户,设置为 false 禁用多租户(零成本切换)
|
||||
enabled: false
|
||||
enabled: true
|
||||
|
||||
# 租户字段名(默认:tenant_id)
|
||||
column: tenant_id
|
||||
|
||||
@@ -13,8 +13,8 @@ codegen:
|
||||
moduleName: system
|
||||
# 排除数据表
|
||||
excludeTables:
|
||||
- gen_config
|
||||
- gen_field_config
|
||||
- gen_table
|
||||
- gen_table_column
|
||||
## 模板配置
|
||||
templateConfigs:
|
||||
API:
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.youlai.boot.platform.ai.mapper.AiCommandLogMapper">
|
||||
<mapper namespace="com.youlai.boot.platform.ai.mapper.AiCommandRecordMapper">
|
||||
|
||||
<resultMap id="AiCommandLogVOResult" type="com.youlai.boot.platform.ai.model.vo.AiCommandLogVO">
|
||||
<resultMap id="AiCommandRecordResultMap" type="com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO">
|
||||
<id property="id" column="id"/>
|
||||
<result property="userId" column="user_id"/>
|
||||
<result property="username" column="username"/>
|
||||
@@ -28,55 +28,55 @@
|
||||
<result property="updateTime" column="update_time"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="getLogPage" resultMap="AiCommandLogVOResult">
|
||||
<select id="getRecordPage" resultMap="AiCommandRecordResultMap">
|
||||
SELECT
|
||||
acl.id,
|
||||
acl.user_id,
|
||||
acl.username,
|
||||
acl.original_command,
|
||||
acl.ai_provider,
|
||||
acl.ai_model,
|
||||
acl.parse_status,
|
||||
acl.function_calls,
|
||||
acl.explanation,
|
||||
acl.confidence,
|
||||
acl.parse_error_message,
|
||||
acl.input_tokens,
|
||||
acl.output_tokens,
|
||||
acl.parse_duration_ms,
|
||||
acl.function_name,
|
||||
acl.function_arguments,
|
||||
acl.execute_status,
|
||||
acl.execute_error_message,
|
||||
acl.ip_address,
|
||||
acl.create_time,
|
||||
acl.update_time
|
||||
FROM ai_command_log acl
|
||||
acr.id,
|
||||
acr.user_id,
|
||||
acr.username,
|
||||
acr.original_command,
|
||||
acr.ai_provider,
|
||||
acr.ai_model,
|
||||
acr.parse_status,
|
||||
acr.function_calls,
|
||||
acr.explanation,
|
||||
acr.confidence,
|
||||
acr.parse_error_message,
|
||||
acr.input_tokens,
|
||||
acr.output_tokens,
|
||||
acr.parse_duration_ms,
|
||||
acr.function_name,
|
||||
acr.function_arguments,
|
||||
acr.execute_status,
|
||||
acr.execute_error_message,
|
||||
acr.ip_address,
|
||||
acr.create_time,
|
||||
acr.update_time
|
||||
FROM ai_command_record acr
|
||||
<where>
|
||||
<if test="queryParams.keywords != null and queryParams.keywords != ''">
|
||||
(
|
||||
acl.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||
OR acl.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||
OR acl.username LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||
acr.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||
OR acr.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||
OR acr.username LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||
)
|
||||
</if>
|
||||
<if test="queryParams.executeStatus != null">
|
||||
AND acl.execute_status = #{queryParams.executeStatus}
|
||||
AND acr.execute_status = #{queryParams.executeStatus}
|
||||
</if>
|
||||
<if test="queryParams.userId != null">
|
||||
AND acl.user_id = #{queryParams.userId}
|
||||
AND acr.user_id = #{queryParams.userId}
|
||||
</if>
|
||||
<if test="queryParams.parseStatus != null">
|
||||
AND acl.parse_status = #{queryParams.parseStatus}
|
||||
AND acr.parse_status = #{queryParams.parseStatus}
|
||||
</if>
|
||||
<if test="queryParams.functionName != null and queryParams.functionName != ''">
|
||||
AND acl.function_name = #{queryParams.functionName}
|
||||
AND acr.function_name = #{queryParams.functionName}
|
||||
</if>
|
||||
<if test="queryParams.aiProvider != null and queryParams.aiProvider != ''">
|
||||
AND acl.ai_provider = #{queryParams.aiProvider}
|
||||
AND acr.ai_provider = #{queryParams.aiProvider}
|
||||
</if>
|
||||
<if test="queryParams.aiModel != null and queryParams.aiModel != ''">
|
||||
AND acl.ai_model = #{queryParams.aiModel}
|
||||
AND acr.ai_model = #{queryParams.aiModel}
|
||||
</if>
|
||||
<if test="
|
||||
queryParams.createTime != null
|
||||
@@ -86,10 +86,10 @@
|
||||
and queryParams.createTime[1] != null
|
||||
and queryParams.createTime[1] != ''
|
||||
">
|
||||
AND acl.create_time BETWEEN #{queryParams.createTime[0]} AND #{queryParams.createTime[1]}
|
||||
AND acr.create_time BETWEEN #{queryParams.createTime[0]} AND #{queryParams.createTime[1]}
|
||||
</if>
|
||||
</where>
|
||||
ORDER BY acl.create_time DESC
|
||||
ORDER BY acr.create_time DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -16,7 +16,7 @@
|
||||
CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured
|
||||
FROM
|
||||
information_schema.tables t1
|
||||
LEFT JOIN gen_config t2 on t1.TABLE_NAME = t2.table_name
|
||||
LEFT JOIN gen_table t2 on t1.TABLE_NAME = t2.table_name
|
||||
WHERE
|
||||
t1.TABLE_SCHEMA = (SELECT DATABASE())
|
||||
AND t1.table_type = 'BASE TABLE'
|
||||
@@ -46,7 +46,7 @@
|
||||
ALL_TABLES t1
|
||||
LEFT JOIN ALL_TAB_COMMENTS c ON t1.TABLE_NAME = c.TABLE_NAME AND t1.OWNER = c.OWNER
|
||||
LEFT JOIN ALL_OBJECTS o ON t1.TABLE_NAME = o.OBJECT_NAME AND t1.OWNER = o.OWNER AND o.OBJECT_TYPE = 'TABLE'
|
||||
LEFT JOIN gen_config t2 ON t1.TABLE_NAME = t2.table_name
|
||||
LEFT JOIN gen_table t2 ON t1.TABLE_NAME = t2.table_name
|
||||
WHERE
|
||||
t1.OWNER = 'YOULAI_BOOT'
|
||||
<if test="queryParams.keywords != null and queryParams.keywords.trim() neq ''">
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.youlai.boot.platform.codegen.mapper.GenConfigMapper">
|
||||
<mapper namespace="com.youlai.boot.platform.codegen.mapper.GenTableMapper">
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.youlai.boot.platform.generator.mapper.GenFieldConfigMapper">
|
||||
<mapper namespace="com.youlai.boot.platform.codegen.mapper.GenTableColumnMapper">
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.youlai.boot.platform.codegen.mapper.GenTableColumnMapper">
|
||||
|
||||
</mapper>
|
||||
7
src/main/resources/mapper/codegen/GenTableMapper.xml
Normal file
7
src/main/resources/mapper/codegen/GenTableMapper.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.youlai.boot.platform.codegen.mapper.GenTableMapper">
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user