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());
|
(1, '默认租户', 'DEFAULT', 1, NOW());
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 2. 创建租户切换审计日志表
|
-- 2. 为业务表添加 tenant_id 字段
|
||||||
-- ============================================
|
|
||||||
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 字段
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 注意:MySQL 5.7 不支持 IF NOT EXISTS,如果字段已存在会报错
|
-- 注意:MySQL 5.7 不支持 IF NOT EXISTS,如果字段已存在会报错
|
||||||
-- 建议先检查字段是否存在,或使用 MySQL 8.0+
|
-- 建议先检查字段是否存在,或使用 MySQL 8.0+
|
||||||
|
|
||||||
-- 用户表
|
-- 用户表:仅在不存在时添加列和索引,避免重复执行报错
|
||||||
ALTER TABLE `sys_user`
|
ALTER TABLE `sys_user`
|
||||||
ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`,
|
ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`,
|
||||||
ADD INDEX `idx_tenant_id` (`tenant_id`);
|
ADD INDEX `idx_tenant_id` (`tenant_id`);
|
||||||
|
|
||||||
-- 更新现有数据的 tenant_id(设置为默认租户)
|
|
||||||
UPDATE `sys_user` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL;
|
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`
|
ALTER TABLE `sys_role`
|
||||||
ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`,
|
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;
|
UPDATE `sys_log` SET `tenant_id` = 1 WHERE `tenant_id` IS NULL;
|
||||||
|
|
||||||
-- AI 命令记录表
|
-- AI 命令记录表
|
||||||
ALTER TABLE `ai_command_log`
|
ALTER TABLE `ai_command_record`
|
||||||
ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`,
|
ADD COLUMN `tenant_id` bigint DEFAULT 1 COMMENT '租户ID' AFTER `id`,
|
||||||
ADD INDEX `idx_tenant_id` (`tenant_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. 初始化现有用户的租户关联(默认租户)
|
-- 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. 添加租户管理菜单和权限(仅在菜单不存在时添加)
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 租户管理主菜单(放在部门管理之后,字典管理之前,ID=6)
|
-- 租户管理主菜单(放在部门管理之后,字典管理之前,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`)
|
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)
|
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` = '租户管理';
|
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)
|
-- 租户管理权限按钮(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`)
|
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`);
|
ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`);
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
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;
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 1. 删除用户租户关联表
|
-- 1. 删除租户表(可选)
|
||||||
-- ============================================
|
|
||||||
DROP TABLE IF EXISTS `sys_user_tenant`;
|
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 2. 删除租户表(可选)
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 注意:如果将来可能再次启用多租户,建议保留此表
|
-- 注意:如果将来可能再次启用多租户,建议保留此表
|
||||||
-- 如需删除,取消下面的注释
|
-- 如需删除,取消下面的注释
|
||||||
-- DROP TABLE IF EXISTS `sys_tenant`;
|
-- 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 INDEX `idx_tenant_id`;
|
||||||
ALTER TABLE `sys_role` DROP COLUMN IF EXISTS `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 INDEX `idx_tenant_id`;
|
||||||
ALTER TABLE `sys_dept` DROP COLUMN IF EXISTS `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 INDEX `idx_tenant_id`;
|
||||||
ALTER TABLE `sys_notice` DROP COLUMN IF EXISTS `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 INDEX `idx_tenant_id`;
|
||||||
ALTER TABLE `sys_log` DROP COLUMN IF EXISTS `tenant_id`;
|
ALTER TABLE `sys_log` DROP COLUMN `tenant_id`;
|
||||||
|
|
||||||
-- AI 命令记录表
|
-- AI 命令记录表
|
||||||
ALTER TABLE `ai_command_log` DROP INDEX IF EXISTS `idx_tenant_id`;
|
ALTER TABLE `ai_command_record` DROP INDEX `idx_tenant_id`;
|
||||||
ALTER TABLE `ai_command_log` DROP COLUMN IF EXISTS `tenant_id`;
|
ALTER TABLE `ai_command_record` DROP COLUMN `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`;
|
|
||||||
|
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 4. 删除租户管理菜单和权限
|
-- 3. 删除租户管理菜单和权限
|
||||||
-- ============================================
|
-- ============================================
|
||||||
-- 删除角色菜单关联
|
-- 删除角色菜单关联
|
||||||
DELETE FROM `sys_role_menu` WHERE `menu_id` IN (6, 141, 142, 143, 144, 145);
|
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;
|
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;
|
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 (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\"}');
|
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
|
-- 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`;
|
DROP TABLE IF EXISTS `sys_user`;
|
||||||
CREATE TABLE `sys_user` (
|
CREATE TABLE `sys_user` (
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
|
`tenant_id` bigint DEFAULT 1 COMMENT '租户ID',
|
||||||
`username` varchar(64) COMMENT '用户名',
|
`username` varchar(64) COMMENT '用户名',
|
||||||
`nickname` varchar(64) COMMENT '昵称',
|
`nickname` varchar(64) COMMENT '昵称',
|
||||||
`gender` tinyint(1) DEFAULT 1 COMMENT '性别((1-男 2-女 0-保密)',
|
`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-已删除)',
|
`is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',
|
||||||
`openid` char(28) COMMENT '微信 openid',
|
`openid` char(28) COMMENT '微信 openid',
|
||||||
PRIMARY KEY (`id`) USING BTREE,
|
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 = '系统用户表';
|
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统用户表';
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Records of sys_user
|
-- 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 (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, '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 (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, '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 (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
|
-- Table structure for sys_user_role
|
||||||
@@ -425,10 +414,10 @@ CREATE TABLE `sys_log` (
|
|||||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表';
|
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表';
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for gen_config
|
-- Table structure for gen_table
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
DROP TABLE IF EXISTS `gen_config`;
|
DROP TABLE IF EXISTS `gen_table`;
|
||||||
CREATE TABLE `gen_config` (
|
CREATE TABLE `gen_table` (
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||||
`table_name` varchar(100) NOT NULL COMMENT '表名',
|
`table_name` varchar(100) NOT NULL COMMENT '表名',
|
||||||
`module_name` varchar(100) COMMENT '模块名',
|
`module_name` varchar(100) COMMENT '模块名',
|
||||||
@@ -447,12 +436,12 @@ CREATE TABLE `gen_config` (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成配置表';
|
) 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`;
|
DROP TABLE IF EXISTS `gen_table_column`;
|
||||||
CREATE TABLE `gen_field_config` (
|
CREATE TABLE `gen_table_column` (
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
`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_name` varchar(100) ,
|
||||||
`column_type` varchar(50) ,
|
`column_type` varchar(50) ,
|
||||||
`column_length` int ,
|
`column_length` int ,
|
||||||
@@ -471,7 +460,7 @@ CREATE TABLE `gen_field_config` (
|
|||||||
`create_time` datetime COMMENT '创建时间',
|
`create_time` datetime COMMENT '创建时间',
|
||||||
`update_time` datetime COMMENT '更新时间',
|
`update_time` datetime COMMENT '更新时间',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `config_id` (`config_id`)
|
KEY `idx_table_id` (`table_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成字段配置表';
|
) 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 命令记录表
|
-- AI 命令记录表
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
DROP TABLE IF EXISTS `ai_command_log`;
|
DROP TABLE IF EXISTS `ai_command_record`;
|
||||||
CREATE TABLE `ai_command_log` (
|
CREATE TABLE `ai_command_record` (
|
||||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
`user_id` bigint DEFAULT NULL COMMENT '用户ID',
|
`user_id` bigint DEFAULT NULL COMMENT '用户ID',
|
||||||
`username` varchar(64) DEFAULT NULL COMMENT '用户名',
|
`username` varchar(64) DEFAULT NULL COMMENT '用户名',
|
||||||
@@ -589,7 +578,7 @@ CREATE TABLE `ai_command_log` (
|
|||||||
KEY `idx_model` (`ai_model`),
|
KEY `idx_model` (`ai_model`),
|
||||||
KEY `idx_parse_success` (`parse_status`),
|
KEY `idx_parse_success` (`parse_status`),
|
||||||
KEY `idx_execute_status` (`execute_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 (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());
|
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;
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|||||||
@@ -1,21 +1,33 @@
|
|||||||
package com.youlai.boot.auth.controller;
|
package com.youlai.boot.auth.controller;
|
||||||
|
|
||||||
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
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.auth.model.dto.WxMiniAppPhoneLoginDTO;
|
||||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
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.core.web.Result;
|
||||||
import com.youlai.boot.auth.service.AuthService;
|
import com.youlai.boot.auth.service.AuthService;
|
||||||
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
||||||
import com.youlai.boot.common.annotation.Log;
|
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.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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.web.bind.annotation.*;
|
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 {
|
public class AuthController {
|
||||||
|
|
||||||
private final AuthService authService;
|
private final AuthService authService;
|
||||||
|
private final UserService userService;
|
||||||
|
private final TenantService tenantService;
|
||||||
|
private final TenantProperties tenantProperties;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
@Operation(summary = "获取验证码")
|
@Operation(summary = "获取验证码")
|
||||||
@GetMapping("/captcha")
|
@GetMapping("/captcha")
|
||||||
@@ -42,14 +58,64 @@ public class AuthController {
|
|||||||
@Operation(summary = "账号密码登录")
|
@Operation(summary = "账号密码登录")
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
@Log(value = "登录", module = LogModuleEnum.LOGIN)
|
@Log(value = "登录", module = LogModuleEnum.LOGIN)
|
||||||
public Result<AuthenticationToken> login(
|
public Result<?> login(@RequestBody @Valid LoginRequest request) {
|
||||||
@Parameter(description = "用户名", example = "admin") @RequestParam String username,
|
String username = request.getUsername();
|
||||||
@Parameter(description = "密码", example = "123456") @RequestParam String password
|
String password = request.getPassword();
|
||||||
) {
|
Long tenantId = request.getTenantId();
|
||||||
AuthenticationToken authenticationToken = authService.login(username, password);
|
|
||||||
|
// 如果未启用多租户,直接登录
|
||||||
|
if (tenantProperties == null || !Boolean.TRUE.equals(tenantProperties.getEnabled())) {
|
||||||
|
AuthenticationToken authenticationToken = authService.login(username, password, null);
|
||||||
return Result.success(authenticationToken);
|
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 = "短信验证码登录")
|
@Operation(summary = "短信验证码登录")
|
||||||
@PostMapping("/login/sms")
|
@PostMapping("/login/sms")
|
||||||
@Log(value = "短信验证码登录", module = LogModuleEnum.LOGIN)
|
@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
|
@Builder
|
||||||
public class CaptchaVO {
|
public class CaptchaVO {
|
||||||
|
|
||||||
@Schema(description = "验证码缓存 Key")
|
@Schema(description = "验证码缓存 ID")
|
||||||
private String captchaKey;
|
private String captchaId;
|
||||||
|
|
||||||
@Schema(description = "验证码图片Base64字符串")
|
@Schema(description = "验证码图片Base64字符串")
|
||||||
private String captchaBase64;
|
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 username 用户名
|
||||||
* @param password 密码
|
* @param password 密码
|
||||||
|
* @param tenantId 租户ID(可选,多租户模式下用于指定租户)
|
||||||
* @return 登录结果
|
* @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.model.WxMiniAppPhoneAuthenticationToken;
|
||||||
import com.youlai.boot.security.token.TokenManager;
|
import com.youlai.boot.security.token.TokenManager;
|
||||||
import com.youlai.boot.security.util.SecurityUtils;
|
import com.youlai.boot.security.util.SecurityUtils;
|
||||||
|
import com.youlai.boot.common.tenant.TenantContextHolder;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
@@ -61,10 +62,16 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
*
|
*
|
||||||
* @param username 用户名
|
* @param username 用户名
|
||||||
* @param password 密码
|
* @param password 密码
|
||||||
|
* @param tenantId 租户ID(可选,多租户模式下用于指定租户)
|
||||||
* @return 访问令牌
|
* @return 访问令牌
|
||||||
*/
|
*/
|
||||||
@Override
|
@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. 创建用于密码认证的令牌(未认证)
|
// 1. 创建用于密码认证的令牌(未认证)
|
||||||
UsernamePasswordAuthenticationToken authenticationToken =
|
UsernamePasswordAuthenticationToken authenticationToken =
|
||||||
new UsernamePasswordAuthenticationToken(username.trim(), password);
|
new UsernamePasswordAuthenticationToken(username.trim(), password);
|
||||||
@@ -194,16 +201,16 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
String imageBase64Data = captcha.getImageBase64Data();
|
String imageBase64Data = captcha.getImageBase64Data();
|
||||||
|
|
||||||
// 验证码文本缓存至Redis,用于登录校验
|
// 验证码文本缓存至Redis,用于登录校验
|
||||||
String captchaKey = IdUtil.fastSimpleUUID();
|
String captchaId = IdUtil.fastSimpleUUID();
|
||||||
redisTemplate.opsForValue().set(
|
redisTemplate.opsForValue().set(
|
||||||
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaKey),
|
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId),
|
||||||
captchaCode,
|
captchaCode,
|
||||||
captchaProperties.getExpireSeconds(),
|
captchaProperties.getExpireSeconds(),
|
||||||
TimeUnit.SECONDS
|
TimeUnit.SECONDS
|
||||||
);
|
);
|
||||||
|
|
||||||
return CaptchaVO.builder()
|
return CaptchaVO.builder()
|
||||||
.captchaKey(captchaKey)
|
.captchaId(captchaId)
|
||||||
.captchaBase64(imageBase64Data)
|
.captchaBase64(imageBase64Data)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ public interface JwtClaimConstants {
|
|||||||
*/
|
*/
|
||||||
String AUTHORITIES = "authorities";
|
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.config.property.TenantProperties;
|
||||||
import com.youlai.boot.plugin.mybatis.MyDataPermissionHandler;
|
import com.youlai.boot.plugin.mybatis.MyDataPermissionHandler;
|
||||||
import com.youlai.boot.plugin.mybatis.MyMetaObjectHandler;
|
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.DatabaseIdProvider;
|
||||||
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
|
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||||
@@ -35,7 +34,7 @@ public class MybatisConfig {
|
|||||||
private String dbType;
|
private String dbType;
|
||||||
|
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private TenantLineHandler tenantLineHandler;
|
private MyTenantLineHandler myTenantLineHandler;
|
||||||
|
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private TenantProperties tenantProperties;
|
private TenantProperties tenantProperties;
|
||||||
@@ -51,8 +50,8 @@ public class MybatisConfig {
|
|||||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||||
|
|
||||||
// 多租户插件(如果启用,必须在最前面)
|
// 多租户插件(如果启用,必须在最前面)
|
||||||
if (tenantProperties != null && Boolean.TRUE.equals(tenantProperties.getEnabled()) && tenantLineHandler != null) {
|
if (tenantProperties != null && Boolean.TRUE.equals(tenantProperties.getEnabled()) && myTenantLineHandler != null) {
|
||||||
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(tenantLineHandler));
|
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.JavaTimeModule;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||||
import com.youlai.boot.core.interceptor.TenantValidationInterceptor;
|
|
||||||
import jakarta.validation.Validation;
|
import jakarta.validation.Validation;
|
||||||
import jakarta.validation.Validator;
|
import jakarta.validation.Validator;
|
||||||
import jakarta.validation.ValidatorFactory;
|
import jakarta.validation.ValidatorFactory;
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.hibernate.validator.HibernateValidator;
|
import org.hibernate.validator.HibernateValidator;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.converter.HttpMessageConverter;
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||||
import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
|
import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
|
||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
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");
|
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
|
* @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory
|
||||||
* @return Validator 实例
|
* @return Validator 实例
|
||||||
*/
|
*/
|
||||||
/**
|
|
||||||
* 配置拦截器
|
|
||||||
*
|
|
||||||
* @param registry 拦截器注册器
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void addInterceptors(InterceptorRegistry registry) {
|
|
||||||
// 注册租户校验拦截器(仅在多租户模式启用时生效)
|
|
||||||
if (tenantValidationInterceptor != null) {
|
|
||||||
registry.addInterceptor(tenantValidationInterceptor)
|
|
||||||
.addPathPatterns("/api/**")
|
|
||||||
.order(2); // 在认证拦截器之后执行
|
|
||||||
log.info("租户校验拦截器已注册");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) {
|
public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) {
|
||||||
try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
|
try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ public class TenantProperties {
|
|||||||
ignoreTables.add("sys_dict");
|
ignoreTables.add("sys_dict");
|
||||||
ignoreTables.add("sys_dict_item");
|
ignoreTables.add("sys_dict_item");
|
||||||
ignoreTables.add("sys_config");
|
ignoreTables.add("sys_config");
|
||||||
|
// 代码生成表(平台共用,不做租户隔离)
|
||||||
|
ignoreTables.add("gen_table");
|
||||||
|
ignoreTables.add("gen_table_column");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package com.youlai.boot.core.filter;
|
package com.youlai.boot.core.filter;
|
||||||
|
|
||||||
|
import com.youlai.boot.common.constant.SecurityConstants;
|
||||||
import com.youlai.boot.common.tenant.TenantContextHolder;
|
import com.youlai.boot.common.tenant.TenantContextHolder;
|
||||||
import com.youlai.boot.config.property.TenantProperties;
|
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.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
@@ -10,6 +13,8 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.core.annotation.Order;
|
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.stereotype.Component;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
@@ -34,39 +39,59 @@ import java.io.IOException;
|
|||||||
public class TenantContextFilter extends OncePerRequestFilter {
|
public class TenantContextFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final TenantProperties tenantProperties;
|
private final TenantProperties tenantProperties;
|
||||||
|
private final TokenManager tokenManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 从请求头获取租户ID
|
// 1) 优先从已认证用户中获取租户ID
|
||||||
String tenantIdStr = request.getHeader(tenantProperties.getHeaderName());
|
Long tenantId = resolveTenantFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
|
||||||
|
|
||||||
if (StringUtils.hasText(tenantIdStr)) {
|
// 2) 如果尚未获取到,尝试从 Token 中解析
|
||||||
try {
|
if (tenantId == null) {
|
||||||
Long tenantId = Long.parseLong(tenantIdStr);
|
tenantId = resolveTenantFromToken(request);
|
||||||
TenantContextHolder.setTenantId(tenantId);
|
|
||||||
log.debug("从请求头获取租户ID: {}", tenantId);
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
log.warn("租户ID格式错误: {}", tenantIdStr);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 如果未提供租户ID,使用默认租户ID
|
// 3) 仍为空则使用默认租户
|
||||||
|
if (tenantId == null) {
|
||||||
Long defaultTenantId = tenantProperties.getDefaultTenantId();
|
Long defaultTenantId = tenantProperties.getDefaultTenantId();
|
||||||
if (defaultTenantId != null) {
|
if (defaultTenantId != null) {
|
||||||
TenantContextHolder.setTenantId(defaultTenantId);
|
tenantId = defaultTenantId;
|
||||||
log.debug("使用默认租户ID: {}", defaultTenantId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 继续执行过滤器链
|
if (tenantId != null) {
|
||||||
|
TenantContextHolder.setTenantId(tenantId);
|
||||||
|
log.debug("TenantContextFilter set tenantId: {}", tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
// 请求结束时清除租户上下文,避免线程池复用导致的数据泄露
|
|
||||||
TenantContextHolder.clear();
|
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);
|
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) {
|
private static <T> Result<T> result(IResultCode resultCode, T data) {
|
||||||
return result(resultCode.getCode(), resultCode.getMsg(), 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_ATTEMPT_LIMIT_EXCEEDED("A0241", "用户验证码尝试次数超限"),
|
||||||
USER_VERIFICATION_CODE_EXPIRED("A0242", "用户验证码过期"),
|
USER_VERIFICATION_CODE_EXPIRED("A0242", "用户验证码过期"),
|
||||||
|
|
||||||
|
// 多租户登录
|
||||||
|
CHOOSE_TENANT("A0250", "请选择登录租户"),
|
||||||
|
|
||||||
/** 二级宏观错误码 */
|
/** 二级宏观错误码 */
|
||||||
ACCESS_PERMISSION_EXCEPTION("A0300", "访问权限异常"),
|
ACCESS_PERMISSION_EXCEPTION("A0300", "访问权限异常"),
|
||||||
ACCESS_UNAUTHORIZED("A0301", "访问未授权"),
|
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.AiParseRequestDTO;
|
||||||
import com.youlai.boot.platform.ai.model.dto.AiParseResponseDTO;
|
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.query.AiCommandPageQuery;
|
||||||
import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
|
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
|
||||||
import com.youlai.boot.platform.ai.service.AiCommandLogService;
|
import com.youlai.boot.platform.ai.service.AiCommandRecordService;
|
||||||
import com.youlai.boot.platform.ai.service.AiCommandService;
|
import com.youlai.boot.platform.ai.service.AiCommandService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
@@ -32,7 +32,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
public class AiCommandController {
|
public class AiCommandController {
|
||||||
|
|
||||||
private final AiCommandService aiCommandService;
|
private final AiCommandService aiCommandService;
|
||||||
private final AiCommandLogService logService;
|
private final AiCommandRecordService recordService;
|
||||||
|
|
||||||
@Operation(summary = "解析自然语言命令")
|
@Operation(summary = "解析自然语言命令")
|
||||||
@PostMapping("/parse")
|
@PostMapping("/parse")
|
||||||
@@ -72,17 +72,17 @@ public class AiCommandController {
|
|||||||
|
|
||||||
@Operation(summary = "获取AI命令记录分页列表")
|
@Operation(summary = "获取AI命令记录分页列表")
|
||||||
@GetMapping("/records")
|
@GetMapping("/records")
|
||||||
public PageResult<AiCommandLogVO> getLogPage(AiCommandPageQuery queryParams) {
|
public PageResult<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams) {
|
||||||
IPage<AiCommandLogVO> page = logService.getLogPage(queryParams);
|
IPage<AiCommandRecordVO> page = recordService.getRecordPage(queryParams);
|
||||||
return PageResult.success(page);
|
return PageResult.success(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "撤销命令执行")
|
@Operation(summary = "撤销命令执行")
|
||||||
@PostMapping("/rollback/{logId}")
|
@PostMapping("/rollback/{recordId}")
|
||||||
public Result<?> rollbackCommand(
|
public Result<?> rollbackCommand(
|
||||||
@Parameter(description = "记录ID") @PathVariable String logId
|
@Parameter(description = "记录ID") @PathVariable String recordId
|
||||||
) {
|
) {
|
||||||
logService.rollbackCommand(logId);
|
recordService.rollbackCommand(recordId);
|
||||||
return Result.success("撤销成功");
|
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.mapper.BaseMapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
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.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;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,12 +15,12 @@ import org.apache.ibatis.annotations.Mapper;
|
|||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
*/
|
*/
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface AiCommandLogMapper extends BaseMapper<AiCommandLog> {
|
public interface AiCommandRecordMapper extends BaseMapper<AiCommandRecord> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 AI 命令记录分页列表
|
* 获取 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
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
@TableName("ai_command_log")
|
@TableName("ai_command_record")
|
||||||
public class AiCommandLog extends BaseEntity {
|
public class AiCommandRecord extends BaseEntity {
|
||||||
|
|
||||||
/** 用户ID */
|
/** 用户ID */
|
||||||
private Long userId;
|
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
|
@Data
|
||||||
@Schema(description = "AI命令记录VO")
|
@Schema(description = "AI命令记录VO")
|
||||||
public class AiCommandLogVO implements Serializable {
|
public class AiCommandRecordVO implements Serializable {
|
||||||
|
|
||||||
@Schema(description = "主键ID")
|
@Schema(description = "主键ID")
|
||||||
private String id;
|
private String id;
|
||||||
@@ -90,4 +90,3 @@ public class AiCommandLogVO implements Serializable {
|
|||||||
private LocalDateTime updateTime;
|
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.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
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.query.AiCommandPageQuery;
|
||||||
import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
|
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AI 命令记录服务接口
|
* AI 命令记录服务接口
|
||||||
@@ -12,7 +12,7 @@ import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
|
|||||||
* @author Ray.Hao
|
* @author Ray.Hao
|
||||||
* @since 3.0.0
|
* @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 查询参数
|
* @param queryParams 查询参数
|
||||||
* @return 命令记录分页列表
|
* @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.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.youlai.boot.platform.ai.mapper.AiCommandLogMapper;
|
import com.youlai.boot.platform.ai.mapper.AiCommandRecordMapper;
|
||||||
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.query.AiCommandPageQuery;
|
||||||
import com.youlai.boot.platform.ai.model.vo.AiCommandLogVO;
|
import com.youlai.boot.platform.ai.model.vo.AiCommandRecordVO;
|
||||||
import com.youlai.boot.platform.ai.service.AiCommandLogService;
|
import com.youlai.boot.platform.ai.service.AiCommandRecordService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -21,28 +21,28 @@ import org.springframework.stereotype.Service;
|
|||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class AiCommandLogServiceImpl extends ServiceImpl<AiCommandLogMapper, AiCommandLog>
|
public class AiCommandRecordServiceImpl extends ServiceImpl<AiCommandRecordMapper, AiCommandRecord>
|
||||||
implements AiCommandLogService {
|
implements AiCommandRecordService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IPage<AiCommandLogVO> getLogPage(AiCommandPageQuery queryParams) {
|
public IPage<AiCommandRecordVO> getRecordPage(AiCommandPageQuery queryParams) {
|
||||||
Page<AiCommandLogVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
|
Page<AiCommandRecordVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize());
|
||||||
return this.baseMapper.getLogPage(page, queryParams);
|
return this.baseMapper.getRecordPage(page, queryParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void rollbackCommand(String logId) {
|
public void rollbackCommand(String logId) {
|
||||||
AiCommandLog commandLog = this.getById(logId);
|
AiCommandRecord commandRecord = this.getById(logId);
|
||||||
if (commandLog == null) {
|
if (commandRecord == null) {
|
||||||
throw new RuntimeException("命令记录不存在");
|
throw new RuntimeException("命令记录不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (commandLog.getExecuteStatus() == null || commandLog.getExecuteStatus() != 1) {
|
if (commandRecord.getExecuteStatus() == null || commandRecord.getExecuteStatus() != 1) {
|
||||||
throw new RuntimeException("只能撤销成功执行的命令");
|
throw new RuntimeException("只能撤销成功执行的命令");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 实现具体的回滚逻辑
|
// TODO: 实现具体的回滚逻辑
|
||||||
log.info("撤销命令执行: logId={}, function={}", logId, commandLog.getFunctionName());
|
log.info("撤销命令执行: logId={}, function={}", logId, commandRecord.getFunctionName());
|
||||||
throw new UnsupportedOperationException("回滚功能尚未实现");
|
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.AiFunctionCallDTO;
|
||||||
import com.youlai.boot.platform.ai.model.dto.AiParseRequestDTO;
|
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.dto.AiParseResponseDTO;
|
||||||
import com.youlai.boot.platform.ai.model.entity.AiCommandLog;
|
import com.youlai.boot.platform.ai.model.entity.AiCommandRecord;
|
||||||
import com.youlai.boot.platform.ai.service.AiCommandLogService;
|
import com.youlai.boot.platform.ai.service.AiCommandRecordService;
|
||||||
import com.youlai.boot.platform.ai.service.AiCommandService;
|
import com.youlai.boot.platform.ai.service.AiCommandService;
|
||||||
import com.youlai.boot.platform.ai.tools.UserTools;
|
import com.youlai.boot.platform.ai.tools.UserTools;
|
||||||
import com.youlai.boot.security.util.SecurityUtils;
|
import com.youlai.boot.security.util.SecurityUtils;
|
||||||
@@ -50,7 +50,7 @@ public class AiCommandServiceImpl implements AiCommandService {
|
|||||||
当无法识别命令时,success=false,并给出 error。
|
当无法识别命令时,success=false,并给出 error。
|
||||||
""";
|
""";
|
||||||
|
|
||||||
private final AiCommandLogService logService;
|
private final AiCommandRecordService recordService;
|
||||||
private final UserTools userTools;
|
private final UserTools userTools;
|
||||||
private final ChatClient chatClient;
|
private final ChatClient chatClient;
|
||||||
|
|
||||||
@@ -71,13 +71,13 @@ public class AiCommandServiceImpl implements AiCommandService {
|
|||||||
String username = SecurityUtils.getUsername();
|
String username = SecurityUtils.getUsername();
|
||||||
String ipAddress = JakartaServletUtil.getClientIP(httpRequest);
|
String ipAddress = JakartaServletUtil.getClientIP(httpRequest);
|
||||||
|
|
||||||
AiCommandLog commandLog = new AiCommandLog();
|
AiCommandRecord commandRecord = new AiCommandRecord();
|
||||||
commandLog.setUserId(userId);
|
commandRecord.setUserId(userId);
|
||||||
commandLog.setUsername(username);
|
commandRecord.setUsername(username);
|
||||||
commandLog.setOriginalCommand(command);
|
commandRecord.setOriginalCommand(command);
|
||||||
commandLog.setIpAddress(ipAddress);
|
commandRecord.setIpAddress(ipAddress);
|
||||||
commandLog.setAiProvider("spring-ai");
|
commandRecord.setAiProvider("spring-ai");
|
||||||
commandLog.setAiModel("auto");
|
commandRecord.setAiModel("auto");
|
||||||
|
|
||||||
String systemPrompt = buildSystemPrompt();
|
String systemPrompt = buildSystemPrompt();
|
||||||
String userPrompt = buildUserPrompt(request);
|
String userPrompt = buildUserPrompt(request);
|
||||||
@@ -95,20 +95,20 @@ public class AiCommandServiceImpl implements AiCommandService {
|
|||||||
|
|
||||||
ParseResult parseResult = parseAiResponse(rawContent);
|
ParseResult parseResult = parseAiResponse(rawContent);
|
||||||
|
|
||||||
commandLog.setAiProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai"));
|
commandRecord.setAiProvider(StrUtil.emptyToDefault(parseResult.provider(), "spring-ai"));
|
||||||
commandLog.setAiModel(StrUtil.emptyToDefault(parseResult.model(), "auto"));
|
commandRecord.setAiModel(StrUtil.emptyToDefault(parseResult.model(), "auto"));
|
||||||
commandLog.setParseStatus(parseResult.success() ? 1 : 0);
|
commandRecord.setParseStatus(parseResult.success() ? 1 : 0);
|
||||||
commandLog.setExplanation(parseResult.explanation());
|
commandRecord.setExplanation(parseResult.explanation());
|
||||||
commandLog.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls()));
|
commandRecord.setFunctionCalls(JSONUtil.toJsonStr(parseResult.functionCalls()));
|
||||||
commandLog.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null);
|
commandRecord.setConfidence(parseResult.confidence() != null ? BigDecimal.valueOf(parseResult.confidence()) : null);
|
||||||
commandLog.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败"));
|
commandRecord.setParseErrorMessage(parseResult.success() ? null : StrUtil.emptyToDefault(parseResult.error(), "解析失败"));
|
||||||
long duration = System.currentTimeMillis() - startTime;
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
commandLog.setParseDurationMs((int) duration);
|
commandRecord.setParseDurationMs((int) duration);
|
||||||
|
|
||||||
logService.save(commandLog);
|
recordService.save(commandRecord);
|
||||||
|
|
||||||
AiParseResponseDTO response = AiParseResponseDTO.builder()
|
AiParseResponseDTO response = AiParseResponseDTO.builder()
|
||||||
.parseLogId(commandLog.getId())
|
.parseLogId(commandRecord.getId())
|
||||||
.success(parseResult.success())
|
.success(parseResult.success())
|
||||||
.functionCalls(parseResult.functionCalls())
|
.functionCalls(parseResult.functionCalls())
|
||||||
.explanation(parseResult.explanation())
|
.explanation(parseResult.explanation())
|
||||||
@@ -120,17 +120,17 @@ public class AiCommandServiceImpl implements AiCommandService {
|
|||||||
if (!parseResult.success()) {
|
if (!parseResult.success()) {
|
||||||
log.warn("❗️ AI 未能解析命令: {}", parseResult.error());
|
log.warn("❗️ AI 未能解析命令: {}", parseResult.error());
|
||||||
} else {
|
} else {
|
||||||
log.info("✅ 解析成功,审计记录ID: {}", commandLog.getId());
|
log.info("✅ 解析成功,审计记录ID: {}", commandRecord.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
long duration = System.currentTimeMillis() - startTime;
|
long duration = System.currentTimeMillis() - startTime;
|
||||||
commandLog.setParseStatus(0);
|
commandRecord.setParseStatus(0);
|
||||||
commandLog.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList()));
|
commandRecord.setFunctionCalls(JSONUtil.toJsonStr(Collections.emptyList()));
|
||||||
commandLog.setParseErrorMessage(e.getMessage());
|
commandRecord.setParseErrorMessage(e.getMessage());
|
||||||
commandLog.setParseDurationMs((int) duration);
|
commandRecord.setParseDurationMs((int) duration);
|
||||||
logService.save(commandLog);
|
recordService.save(commandRecord);
|
||||||
|
|
||||||
log.error("❌ 解析命令失败: {}", e.getMessage(), e);
|
log.error("❌ 解析命令失败: {}", e.getMessage(), e);
|
||||||
throw new RuntimeException("解析命令失败: " + e.getMessage(), e);
|
throw new RuntimeException("解析命令失败: " + e.getMessage(), e);
|
||||||
@@ -232,52 +232,52 @@ public class AiCommandServiceImpl implements AiCommandService {
|
|||||||
AiFunctionCallDTO functionCall = request.getFunctionCall();
|
AiFunctionCallDTO functionCall = request.getFunctionCall();
|
||||||
|
|
||||||
// 根据解析日志ID获取审计记录,如果不存在则创建新记录
|
// 根据解析日志ID获取审计记录,如果不存在则创建新记录
|
||||||
AiCommandLog commandLog;
|
AiCommandRecord commandRecord ;
|
||||||
if (StrUtil.isNotBlank(request.getParseLogId())) {
|
if (StrUtil.isNotBlank(request.getParseLogId())) {
|
||||||
// 更新已存在的审计记录(解析阶段已创建)
|
// 更新已存在的审计记录(解析阶段已创建)
|
||||||
commandLog = logService.getById(request.getParseLogId());
|
commandRecord = recordService.getById(request.getParseLogId());
|
||||||
if (commandLog == null) {
|
if (commandRecord == null) {
|
||||||
throw new IllegalStateException("未找到对应的解析记录,ID: " + request.getParseLogId());
|
throw new IllegalStateException("未找到对应的解析记录,ID: " + request.getParseLogId());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 如果没有解析日志ID,创建新记录(兼容直接执行的情况)
|
// 如果没有解析日志ID,创建新记录(兼容直接执行的情况)
|
||||||
commandLog = new AiCommandLog();
|
commandRecord = new AiCommandRecord();
|
||||||
commandLog.setUserId(userId);
|
commandRecord.setUserId(userId);
|
||||||
commandLog.setUsername(username);
|
commandRecord.setUsername(username);
|
||||||
commandLog.setOriginalCommand(request.getOriginalCommand());
|
commandRecord.setOriginalCommand(request.getOriginalCommand());
|
||||||
commandLog.setIpAddress(ipAddress);
|
commandRecord.setIpAddress(ipAddress);
|
||||||
logService.save(commandLog);
|
recordService.save(commandRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新执行相关字段
|
// 更新执行相关字段
|
||||||
commandLog.setFunctionName(functionCall.getName());
|
commandRecord.setFunctionName(functionCall.getName());
|
||||||
commandLog.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments()));
|
commandRecord.setFunctionArguments(JSONUtil.toJsonStr(functionCall.getArguments()));
|
||||||
commandLog.setExecuteStatus(0); // 0-待执行
|
commandRecord.setExecuteStatus(0); // 0-待执行
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 🎯 执行具体的函数调用
|
// 🎯 执行具体的函数调用
|
||||||
Object result = executeFunctionCall(functionCall);
|
Object result = executeFunctionCall(functionCall);
|
||||||
|
|
||||||
// 更新执行成功
|
// 更新执行成功
|
||||||
commandLog.setExecuteStatus(1); // 1-成功
|
commandRecord.setExecuteStatus(1); // 1-成功
|
||||||
commandLog.setExecuteErrorMessage(null);
|
commandRecord.setExecuteErrorMessage(null);
|
||||||
|
|
||||||
// 更新审计记录
|
// 更新审计记录
|
||||||
logService.updateById(commandLog);
|
recordService.updateById(commandRecord);
|
||||||
|
|
||||||
log.info("✅ 命令执行成功,审计记录ID: {}", commandLog.getId());
|
log.info("✅ 命令执行成功,审计记录ID: {}", commandRecord.getId());
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 更新执行失败
|
// 更新执行失败
|
||||||
commandLog.setExecuteStatus(-1); // -1-失败
|
commandRecord.setExecuteStatus(-1); // -1-失败
|
||||||
commandLog.setExecuteErrorMessage(e.getMessage());
|
commandRecord.setExecuteErrorMessage(e.getMessage());
|
||||||
|
|
||||||
// 更新审计记录
|
// 更新审计记录
|
||||||
logService.updateById(commandLog);
|
recordService.updateById(commandRecord);
|
||||||
|
|
||||||
log.error("❌ 命令执行失败,审计记录ID: {}", commandLog.getId(), e);
|
log.error("❌ 命令执行失败,审计记录ID: {}", commandRecord.getId(), e);
|
||||||
|
|
||||||
// 抛出异常,由 Controller 统一处理
|
// 抛出异常,由 Controller 统一处理
|
||||||
throw e;
|
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.CodegenPreviewVO;
|
||||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||||
import com.youlai.boot.common.annotation.Log;
|
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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -40,7 +40,7 @@ import java.util.List;
|
|||||||
public class CodegenController {
|
public class CodegenController {
|
||||||
|
|
||||||
private final CodegenService codegenService;
|
private final CodegenService codegenService;
|
||||||
private final GenConfigService genConfigService;
|
private final GenTableService genTableService;
|
||||||
private final CodegenProperties codegenProperties;
|
private final CodegenProperties codegenProperties;
|
||||||
|
|
||||||
@Operation(summary = "获取数据表分页列表")
|
@Operation(summary = "获取数据表分页列表")
|
||||||
@@ -55,10 +55,10 @@ public class CodegenController {
|
|||||||
|
|
||||||
@Operation(summary = "获取代码生成配置")
|
@Operation(summary = "获取代码生成配置")
|
||||||
@GetMapping("/{tableName}/config")
|
@GetMapping("/{tableName}/config")
|
||||||
public Result<GenConfigForm> getGenConfigFormData(
|
public Result<GenConfigForm> getGenTableFormData(
|
||||||
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
|
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
|
||||||
) {
|
) {
|
||||||
GenConfigForm formData = genConfigService.getGenConfigFormData(tableName);
|
GenConfigForm formData = genTableService.getGenTableFormData(tableName);
|
||||||
return Result.success(formData);
|
return Result.success(formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ public class CodegenController {
|
|||||||
@PostMapping("/{tableName}/config")
|
@PostMapping("/{tableName}/config")
|
||||||
@Log(value = "生成代码", module = LogModuleEnum.OTHER)
|
@Log(value = "生成代码", module = LogModuleEnum.OTHER)
|
||||||
public Result<?> saveGenConfig(@RequestBody GenConfigForm formData) {
|
public Result<?> saveGenConfig(@RequestBody GenConfigForm formData) {
|
||||||
genConfigService.saveGenConfig(formData);
|
genTableService.saveGenConfig(formData);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ public class CodegenController {
|
|||||||
public Result<?> deleteGenConfig(
|
public Result<?> deleteGenConfig(
|
||||||
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
|
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
|
||||||
) {
|
) {
|
||||||
genConfigService.deleteGenConfig(tableName);
|
genTableService.deleteGenConfig(tableName);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.youlai.boot.platform.codegen.converter;
|
package com.youlai.boot.platform.codegen.converter;
|
||||||
|
|
||||||
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.entity.GenFieldConfig;
|
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||||
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||||
import org.mapstruct.Mapper;
|
import org.mapstruct.Mapper;
|
||||||
import org.mapstruct.Mapping;
|
import org.mapstruct.Mapping;
|
||||||
@@ -17,25 +17,25 @@ import java.util.List;
|
|||||||
@Mapper(componentModel = "spring")
|
@Mapper(componentModel = "spring")
|
||||||
public interface CodegenConverter {
|
public interface CodegenConverter {
|
||||||
|
|
||||||
@Mapping(source = "genConfig.tableName", target = "tableName")
|
@Mapping(source = "genTable.tableName", target = "tableName")
|
||||||
@Mapping(source = "genConfig.businessName", target = "businessName")
|
@Mapping(source = "genTable.businessName", target = "businessName")
|
||||||
@Mapping(source = "genConfig.moduleName", target = "moduleName")
|
@Mapping(source = "genTable.moduleName", target = "moduleName")
|
||||||
@Mapping(source = "genConfig.packageName", target = "packageName")
|
@Mapping(source = "genTable.packageName", target = "packageName")
|
||||||
@Mapping(source = "genConfig.entityName", target = "entityName")
|
@Mapping(source = "genTable.entityName", target = "entityName")
|
||||||
@Mapping(source = "genConfig.author", target = "author")
|
@Mapping(source = "genTable.author", target = "author")
|
||||||
@Mapping(source = "genConfig.pageType", target = "pageType")
|
@Mapping(source = "genTable.pageType", target = "pageType")
|
||||||
@Mapping(source = "genConfig.removeTablePrefix", target = "removeTablePrefix")
|
@Mapping(source = "genTable.removeTablePrefix", target = "removeTablePrefix")
|
||||||
@Mapping(source = "fieldConfigs", target = "fieldConfigs")
|
@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;
|
package com.youlai.boot.platform.codegen.mapper;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
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;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 代码生成字段配置访问层
|
* 代码生成表字段配置访问层
|
||||||
*
|
*
|
||||||
* @author Ray
|
* @author Ray
|
||||||
* @since 2.10.0
|
* @since 2.10.0
|
||||||
*/
|
*/
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface GenFieldConfigMapper extends BaseMapper<GenFieldConfig> {
|
public interface GenTableColumnMapper extends BaseMapper<GenTableColumn> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
package com.youlai.boot.platform.codegen.mapper;
|
package com.youlai.boot.platform.codegen.mapper;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
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;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 代码生成基础配置访问层
|
* 代码生成表配置访问层
|
||||||
*
|
*
|
||||||
* @author Ray
|
* @author Ray
|
||||||
* @since 2.10.0
|
* @since 2.10.0
|
||||||
*/
|
*/
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface GenConfigMapper extends BaseMapper<GenConfig> {
|
public interface GenTableMapper extends BaseMapper<GenTable> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7,15 +7,15 @@ import lombok.Getter;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 代码生成基础配置
|
* 代码生成表配置
|
||||||
*
|
*
|
||||||
* @author Ray
|
* @author Ray
|
||||||
* @since 2.10.0
|
* @since 2.10.0
|
||||||
*/
|
*/
|
||||||
@TableName(value = "gen_config")
|
@TableName(value = "gen_table")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public class GenConfig extends BaseEntity {
|
public class GenTable extends BaseEntity {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 表名
|
* 表名
|
||||||
@@ -62,3 +62,4 @@ public class GenConfig extends BaseEntity {
|
|||||||
*/
|
*/
|
||||||
private String removeTablePrefix;
|
private String removeTablePrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,21 +11,21 @@ import lombok.Getter;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字段生成配置实体
|
* 代码生成表字段配置实体
|
||||||
*
|
*
|
||||||
* @author Ray
|
* @author Ray
|
||||||
* @since 2.10.0
|
* @since 2.10.0
|
||||||
*/
|
*/
|
||||||
@TableName(value = "gen_field_config")
|
@TableName(value = "gen_table_column")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@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;
|
private String dictType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.youlai.boot.platform.codegen.service;
|
package com.youlai.boot.platform.codegen.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
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
|
* @author Ray
|
||||||
* @since 2.10.0
|
* @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;
|
package com.youlai.boot.platform.codegen.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
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;
|
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,7 +10,7 @@ import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
|||||||
* @author Ray
|
* @author Ray
|
||||||
* @since 2.10.0
|
* @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 表名
|
* @param tableName 表名
|
||||||
* @return
|
* @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.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.youlai.boot.platform.codegen.enums.JavaTypeEnum;
|
import com.youlai.boot.platform.codegen.enums.JavaTypeEnum;
|
||||||
import com.youlai.boot.config.property.CodegenProperties;
|
import com.youlai.boot.config.property.CodegenProperties;
|
||||||
import com.youlai.boot.platform.codegen.service.GenConfigService;
|
import com.youlai.boot.platform.codegen.service.GenTableService;
|
||||||
import com.youlai.boot.platform.codegen.service.GenFieldConfigService;
|
import com.youlai.boot.platform.codegen.service.GenTableColumnService;
|
||||||
import com.youlai.boot.platform.codegen.service.CodegenService;
|
import com.youlai.boot.platform.codegen.service.CodegenService;
|
||||||
import com.youlai.boot.core.exception.BusinessException;
|
import com.youlai.boot.core.exception.BusinessException;
|
||||||
import com.youlai.boot.platform.codegen.mapper.DatabaseMapper;
|
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.GenTable;
|
||||||
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
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.query.TablePageQuery;
|
||||||
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
||||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||||
@@ -48,8 +48,8 @@ public class CodegenServiceImpl implements CodegenService {
|
|||||||
|
|
||||||
private final DatabaseMapper databaseMapper;
|
private final DatabaseMapper databaseMapper;
|
||||||
private final CodegenProperties codegenProperties;
|
private final CodegenProperties codegenProperties;
|
||||||
private final GenConfigService genConfigService;
|
private final GenTableService genTableService;
|
||||||
private final GenFieldConfigService genFieldConfigService;
|
private final GenTableColumnService genTableColumnService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据表分页列表
|
* 数据表分页列表
|
||||||
@@ -77,16 +77,16 @@ public class CodegenServiceImpl implements CodegenService {
|
|||||||
|
|
||||||
List<CodegenPreviewVO> list = new ArrayList<>();
|
List<CodegenPreviewVO> list = new ArrayList<>();
|
||||||
|
|
||||||
GenConfig genConfig = genConfigService.getOne(new LambdaQueryWrapper<GenConfig>()
|
GenTable genTable = genTableService.getOne(new LambdaQueryWrapper<GenTable>()
|
||||||
.eq(GenConfig::getTableName, tableName)
|
.eq(GenTable::getTableName, tableName)
|
||||||
);
|
);
|
||||||
if (genConfig == null) {
|
if (genTable == null) {
|
||||||
throw new BusinessException("未找到表生成配置");
|
throw new BusinessException("未找到表生成配置");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<GenFieldConfig> fieldConfigs = genFieldConfigService.list(new LambdaQueryWrapper<GenFieldConfig>()
|
List<GenTableColumn> fieldConfigs = genTableColumnService.list(new LambdaQueryWrapper<GenTableColumn>()
|
||||||
.eq(GenFieldConfig::getConfigId, genConfig.getId())
|
.eq(GenTableColumn::getTableId, genTable.getId())
|
||||||
.orderByAsc(GenFieldConfig::getFieldSort)
|
.orderByAsc(GenTableColumn::getFieldSort)
|
||||||
|
|
||||||
);
|
);
|
||||||
if (CollectionUtil.isEmpty(fieldConfigs)) {
|
if (CollectionUtil.isEmpty(fieldConfigs)) {
|
||||||
@@ -102,7 +102,7 @@ public class CodegenServiceImpl implements CodegenService {
|
|||||||
|
|
||||||
/* 1. 生成文件名 UserController */
|
/* 1. 生成文件名 UserController */
|
||||||
// User Role Menu Dept
|
// User Role Menu Dept
|
||||||
String entityName = genConfig.getEntityName();
|
String entityName = genTable.getEntityName();
|
||||||
// Controller Service Mapper Entity
|
// Controller Service Mapper Entity
|
||||||
String templateName = templateConfigEntry.getKey();
|
String templateName = templateConfigEntry.getKey();
|
||||||
// .java .ts .vue
|
// .java .ts .vue
|
||||||
@@ -114,9 +114,9 @@ public class CodegenServiceImpl implements CodegenService {
|
|||||||
|
|
||||||
/* 2. 生成文件路径 */
|
/* 2. 生成文件路径 */
|
||||||
// 包名:com.youlai.boot
|
// 包名:com.youlai.boot
|
||||||
String packageName = genConfig.getPackageName();
|
String packageName = genTable.getPackageName();
|
||||||
// 模块名:system
|
// 模块名:system
|
||||||
String moduleName = genConfig.getModuleName();
|
String moduleName = genTable.getModuleName();
|
||||||
// 子包名:controller
|
// 子包名:controller
|
||||||
String subpackageName = templateConfig.getSubpackageName();
|
String subpackageName = templateConfig.getSubpackageName();
|
||||||
// 组合成文件路径:src/main/java/com/youlai/boot/system/controller
|
// 组合成文件路径:src/main/java/com/youlai/boot/system/controller
|
||||||
@@ -126,8 +126,8 @@ public class CodegenServiceImpl implements CodegenService {
|
|||||||
/* 3. 生成文件内容 */
|
/* 3. 生成文件内容 */
|
||||||
// 将模板文件中的变量替换为具体的值 生成代码内容
|
// 将模板文件中的变量替换为具体的值 生成代码内容
|
||||||
// 优先使用保存的 ui,没有则使用请求参数
|
// 优先使用保存的 ui,没有则使用请求参数
|
||||||
String finalType = StrUtil.blankToDefault(genConfig.getPageType(), pageType);
|
String finalType = StrUtil.blankToDefault(genTable.getPageType(), pageType);
|
||||||
String content = getCodeContent(templateConfig, genConfig, fieldConfigs, finalType);
|
String content = getCodeContent(templateConfig, genTable, fieldConfigs, finalType);
|
||||||
previewVO.setContent(content);
|
previewVO.setContent(content);
|
||||||
|
|
||||||
list.add(previewVO);
|
list.add(previewVO);
|
||||||
@@ -215,29 +215,29 @@ public class CodegenServiceImpl implements CodegenService {
|
|||||||
* @param fieldConfigs 字段配置
|
* @param fieldConfigs 字段配置
|
||||||
* @return 代码内容
|
* @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<>();
|
Map<String, Object> bindMap = new HashMap<>();
|
||||||
|
|
||||||
String entityName = genConfig.getEntityName();
|
String entityName = genTable.getEntityName();
|
||||||
|
|
||||||
bindMap.put("packageName", genConfig.getPackageName());
|
bindMap.put("packageName", genTable.getPackageName());
|
||||||
bindMap.put("moduleName", genConfig.getModuleName());
|
bindMap.put("moduleName", genTable.getModuleName());
|
||||||
bindMap.put("subpackageName", templateConfig.getSubpackageName());
|
bindMap.put("subpackageName", templateConfig.getSubpackageName());
|
||||||
bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm"));
|
bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm"));
|
||||||
bindMap.put("entityName", entityName);
|
bindMap.put("entityName", entityName);
|
||||||
bindMap.put("tableName", genConfig.getTableName());
|
bindMap.put("tableName", genTable.getTableName());
|
||||||
bindMap.put("author", genConfig.getAuthor());
|
bindMap.put("author", genTable.getAuthor());
|
||||||
bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest
|
bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest
|
||||||
bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-test
|
bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-test
|
||||||
bindMap.put("businessName", genConfig.getBusinessName());
|
bindMap.put("businessName", genTable.getBusinessName());
|
||||||
bindMap.put("fieldConfigs", fieldConfigs);
|
bindMap.put("fieldConfigs", fieldConfigs);
|
||||||
|
|
||||||
boolean hasLocalDateTime = false;
|
boolean hasLocalDateTime = false;
|
||||||
boolean hasBigDecimal = false;
|
boolean hasBigDecimal = false;
|
||||||
boolean hasRequiredField = false;
|
boolean hasRequiredField = false;
|
||||||
|
|
||||||
for (GenFieldConfig fieldConfig : fieldConfigs) {
|
for (GenTableColumn fieldConfig : fieldConfigs) {
|
||||||
|
|
||||||
if ("LocalDateTime".equals(fieldConfig.getFieldType())) {
|
if ("LocalDateTime".equals(fieldConfig.getFieldType())) {
|
||||||
hasLocalDateTime = true;
|
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.config.property.CodegenProperties;
|
||||||
import com.youlai.boot.platform.codegen.converter.CodegenConverter;
|
import com.youlai.boot.platform.codegen.converter.CodegenConverter;
|
||||||
import com.youlai.boot.platform.codegen.mapper.DatabaseMapper;
|
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.ColumnMetaData;
|
||||||
import com.youlai.boot.platform.codegen.model.bo.TableMetaData;
|
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.GenTable;
|
||||||
import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig;
|
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||||
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||||
import com.youlai.boot.platform.codegen.service.GenConfigService;
|
import com.youlai.boot.platform.codegen.service.GenTableService;
|
||||||
import com.youlai.boot.platform.codegen.service.GenFieldConfigService;
|
import com.youlai.boot.platform.codegen.service.GenTableColumnService;
|
||||||
import com.youlai.boot.system.service.MenuService;
|
import com.youlai.boot.system.service.MenuService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -40,11 +40,11 @@ import java.util.Objects;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@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 DatabaseMapper databaseMapper;
|
||||||
private final CodegenProperties codegenProperties;
|
private final CodegenProperties codegenProperties;
|
||||||
private final GenFieldConfigService genFieldConfigService;
|
private final GenTableColumnService genTableColumnService;
|
||||||
private final CodegenConverter codegenConverter;
|
private final CodegenConverter codegenConverter;
|
||||||
|
|
||||||
@Value("${spring.profiles.active}")
|
@Value("${spring.profiles.active}")
|
||||||
@@ -59,64 +59,64 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
|||||||
* @return 代码生成配置
|
* @return 代码生成配置
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public GenConfigForm getGenConfigFormData(String tableName) {
|
public GenConfigForm getGenTableFormData(String tableName) {
|
||||||
// 查询表生成配置
|
// 查询表生成配置
|
||||||
GenConfig genConfig = this.getOne(
|
GenTable genTable = this.getOne(
|
||||||
new LambdaQueryWrapper<>(GenConfig.class)
|
new LambdaQueryWrapper<>(GenTable.class)
|
||||||
.eq(GenConfig::getTableName, tableName)
|
.eq(GenTable::getTableName, tableName)
|
||||||
.last("LIMIT 1")
|
.last("LIMIT 1")
|
||||||
);
|
);
|
||||||
|
|
||||||
// 是否有代码生成配置
|
// 是否有代码生成配置
|
||||||
boolean hasGenConfig = genConfig != null;
|
boolean hasGenTable = genTable != null;
|
||||||
|
|
||||||
// 如果没有代码生成配置,则根据表的元数据生成默认配置
|
// 如果没有代码生成配置,则根据表的元数据生成默认配置
|
||||||
if (genConfig == null) {
|
if (genTable == null) {
|
||||||
TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName);
|
TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName);
|
||||||
Assert.isTrue(tableMetadata != null, "未找到表元数据");
|
Assert.isTrue(tableMetadata != null, "未找到表元数据");
|
||||||
|
|
||||||
genConfig = new GenConfig();
|
genTable = new GenTable();
|
||||||
genConfig.setTableName(tableName);
|
genTable.setTableName(tableName);
|
||||||
|
|
||||||
// 表注释作为业务名称,去掉表字 例如:用户表 -> 用户
|
// 表注释作为业务名称,去掉表字 例如:用户表 -> 用户
|
||||||
String tableComment = tableMetadata.getTableComment();
|
String tableComment = tableMetadata.getTableComment();
|
||||||
if (StrUtil.isNotBlank(tableComment)) {
|
if (StrUtil.isNotBlank(tableComment)) {
|
||||||
genConfig.setBusinessName(tableComment.replace("表", "").trim());
|
genTable.setBusinessName(tableComment.replace("表", "").trim());
|
||||||
}
|
}
|
||||||
// 根据表名生成实体类名,支持去除前缀 例如:sys_user -> SysUser
|
// 根据表名生成实体类名,支持去除前缀 例如:sys_user -> SysUser
|
||||||
String removePrefix = genConfig.getRemoveTablePrefix();
|
String removePrefix = genTable.getRemoveTablePrefix();
|
||||||
String processedTable = tableName;
|
String processedTable = tableName;
|
||||||
if (StrUtil.isNotBlank(removePrefix) && StrUtil.startWith(tableName, removePrefix)) {
|
if (StrUtil.isNotBlank(removePrefix) && StrUtil.startWith(tableName, removePrefix)) {
|
||||||
processedTable = StrUtil.removePrefix(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());
|
genTable.setPackageName(YouLaiBootApplication.class.getPackageName());
|
||||||
genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名
|
genTable.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名
|
||||||
genConfig.setAuthor(codegenProperties.getDefaultConfig().getAuthor());
|
genTable.setAuthor(codegenProperties.getDefaultConfig().getAuthor());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置
|
// 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置
|
||||||
List<GenFieldConfig> genFieldConfigs = new ArrayList<>();
|
List<GenTableColumn> genTableColumns = new ArrayList<>();
|
||||||
|
|
||||||
// 获取表的列
|
// 获取表的列
|
||||||
List<ColumnMetaData> tableColumns = databaseMapper.getTableColumns(tableName);
|
List<ColumnMetaData> tableColumns = databaseMapper.getTableColumns(tableName);
|
||||||
if (CollectionUtil.isNotEmpty(tableColumns)) {
|
if (CollectionUtil.isNotEmpty(tableColumns)) {
|
||||||
// 查询字段生成配置
|
// 查询字段生成配置
|
||||||
List<GenFieldConfig> fieldConfigList = genFieldConfigService.list(
|
List<GenTableColumn> fieldConfigList = genTableColumnService.list(
|
||||||
new LambdaQueryWrapper<GenFieldConfig>()
|
new LambdaQueryWrapper<GenTableColumn>()
|
||||||
.eq(GenFieldConfig::getConfigId, genConfig.getId())
|
.eq(GenTableColumn::getTableId, genTable.getId())
|
||||||
.orderByAsc(GenFieldConfig::getFieldSort)
|
.orderByAsc(GenTableColumn::getFieldSort)
|
||||||
);
|
);
|
||||||
Integer maxSort = fieldConfigList.stream()
|
Integer maxSort = fieldConfigList.stream()
|
||||||
.map(GenFieldConfig::getFieldSort)
|
.map(GenTableColumn::getFieldSort)
|
||||||
.filter(Objects::nonNull) // 过滤掉空值
|
.filter(Objects::nonNull) // 过滤掉空值
|
||||||
.max(Integer::compareTo)
|
.max(Integer::compareTo)
|
||||||
.orElse(0);
|
.orElse(0);
|
||||||
for (ColumnMetaData tableColumn : tableColumns) {
|
for (ColumnMetaData tableColumn : tableColumns) {
|
||||||
// 根据列名获取字段生成配置
|
// 根据列名获取字段生成配置
|
||||||
String columnName = tableColumn.getColumnName();
|
String columnName = tableColumn.getColumnName();
|
||||||
GenFieldConfig fieldConfig = fieldConfigList.stream()
|
GenTableColumn fieldConfig = fieldConfigList.stream()
|
||||||
.filter(item -> StrUtil.equals(item.getColumnName(), columnName))
|
.filter(item -> StrUtil.equals(item.getColumnName(), columnName))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseGet(() -> createDefaultFieldConfig(tableColumn));
|
.orElseGet(() -> createDefaultFieldConfig(tableColumn));
|
||||||
@@ -130,16 +130,16 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
|||||||
fieldConfig.setFieldType(javaType);
|
fieldConfig.setFieldType(javaType);
|
||||||
}
|
}
|
||||||
// 如果没有代码生成配置,则默认展示在列表和表单
|
// 如果没有代码生成配置,则默认展示在列表和表单
|
||||||
if (!hasGenConfig) {
|
if (!hasGenTable) {
|
||||||
fieldConfig.setIsShowInList(1);
|
fieldConfig.setIsShowInList(1);
|
||||||
fieldConfig.setIsShowInForm(1);
|
fieldConfig.setIsShowInForm(1);
|
||||||
}
|
}
|
||||||
genFieldConfigs.add(fieldConfig);
|
genTableColumns.add(fieldConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 对 genFieldConfigs 按照 fieldSort 排序
|
// 对 genTableColumns 按照 fieldSort 排序
|
||||||
genFieldConfigs = genFieldConfigs.stream().sorted(Comparator.comparing(GenFieldConfig::getFieldSort)).toList();
|
genTableColumns = genTableColumns.stream().sorted(Comparator.comparing(GenTableColumn::getFieldSort)).toList();
|
||||||
GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genConfig, genFieldConfigs);
|
GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genTable, genTableColumns);
|
||||||
|
|
||||||
genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName());
|
genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName());
|
||||||
genConfigForm.setBackendAppName(codegenProperties.getBackendAppName());
|
genConfigForm.setBackendAppName(codegenProperties.getBackendAppName());
|
||||||
@@ -153,8 +153,8 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
|||||||
* @param columnMetaData 表字段元数据
|
* @param columnMetaData 表字段元数据
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private GenFieldConfig createDefaultFieldConfig(ColumnMetaData columnMetaData) {
|
private GenTableColumn createDefaultFieldConfig(ColumnMetaData columnMetaData) {
|
||||||
GenFieldConfig fieldConfig = new GenFieldConfig();
|
GenTableColumn fieldConfig = new GenTableColumn();
|
||||||
fieldConfig.setColumnName(columnMetaData.getColumnName());
|
fieldConfig.setColumnName(columnMetaData.getColumnName());
|
||||||
fieldConfig.setColumnType(columnMetaData.getDataType());
|
fieldConfig.setColumnType(columnMetaData.getDataType());
|
||||||
fieldConfig.setFieldComment(columnMetaData.getColumnComment());
|
fieldConfig.setFieldComment(columnMetaData.getColumnComment());
|
||||||
@@ -181,24 +181,24 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void saveGenConfig(GenConfigForm formData) {
|
public void saveGenConfig(GenConfigForm formData) {
|
||||||
GenConfig genConfig = codegenConverter.toGenConfig(formData);
|
GenTable genTable = codegenConverter.toGenTable(formData);
|
||||||
this.saveOrUpdate(genConfig);
|
this.saveOrUpdate(genTable);
|
||||||
|
|
||||||
// 如果选择上级菜单且当前环境不是生产环境,则保存菜单
|
// 如果选择上级菜单且当前环境不是生产环境,则保存菜单
|
||||||
Long parentMenuId = formData.getParentMenuId();
|
Long parentMenuId = formData.getParentMenuId();
|
||||||
if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) {
|
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("字段配置不能为空");
|
throw new BusinessException("字段配置不能为空");
|
||||||
}
|
}
|
||||||
genFieldConfigs.forEach(genFieldConfig -> {
|
genTableColumns.forEach(genTableColumn -> {
|
||||||
genFieldConfig.setConfigId(genConfig.getId());
|
genTableColumn.setTableId(genTable.getId());
|
||||||
});
|
});
|
||||||
genFieldConfigService.saveOrUpdateBatch(genFieldConfigs);
|
genTableColumnService.saveOrUpdateBatch(genTableColumns);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,15 +208,15 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void deleteGenConfig(String tableName) {
|
public void deleteGenConfig(String tableName) {
|
||||||
GenConfig genConfig = this.getOne(new LambdaQueryWrapper<GenConfig>()
|
GenTable genTable = this.getOne(new LambdaQueryWrapper<GenTable>()
|
||||||
.eq(GenConfig::getTableName, tableName));
|
.eq(GenTable::getTableName, tableName));
|
||||||
|
|
||||||
boolean result = this.remove(new LambdaQueryWrapper<GenConfig>()
|
boolean result = this.remove(new LambdaQueryWrapper<GenTable>()
|
||||||
.eq(GenConfig::getTableName, tableName)
|
.eq(GenTable::getTableName, tableName)
|
||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
genFieldConfigService.remove(new LambdaQueryWrapper<GenFieldConfig>()
|
genTableColumnService.remove(new LambdaQueryWrapper<GenTableColumn>()
|
||||||
.eq(GenFieldConfig::getConfigId, genConfig.getId())
|
.eq(GenTableColumn::getTableId, genTable.getId())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ import java.time.LocalDateTime;
|
|||||||
* 支持自动填充创建时间、更新时间和租户ID
|
* 支持自动填充创建时间、更新时间和租户ID
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @author haoxr
|
* @author Ray.Hao
|
||||||
* @since 2022/10/14
|
* @since 2022/10/14
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
@@ -50,7 +50,8 @@ public class MyMetaObjectHandler implements MetaObjectHandler {
|
|||||||
if (tenantId != null) {
|
if (tenantId != null) {
|
||||||
// 使用 strictInsertFill 自动填充租户ID
|
// 使用 strictInsertFill 自动填充租户ID
|
||||||
// 注意:由于 TenantDynamicFieldConfig 已将 exist 设置为 true,这里可以正常填充
|
// 注意:由于 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
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true", matchIfMissing = false)
|
@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;
|
private final TenantProperties tenantProperties;
|
||||||
|
|
||||||
@@ -2,38 +2,45 @@ package com.youlai.boot.security.filter;
|
|||||||
|
|
||||||
import cn.hutool.captcha.generator.CodeGenerator;
|
import cn.hutool.captcha.generator.CodeGenerator;
|
||||||
import cn.hutool.core.util.StrUtil;
|
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.RedisConstants;
|
||||||
import com.youlai.boot.common.constant.SecurityConstants;
|
import com.youlai.boot.common.constant.SecurityConstants;
|
||||||
import com.youlai.boot.core.web.ResultCode;
|
import com.youlai.boot.core.web.ResultCode;
|
||||||
import com.youlai.boot.core.web.WebResponseHelper;
|
import com.youlai.boot.core.web.WebResponseHelper;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.ServletInputStream;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
|
||||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
import org.springframework.util.StreamUtils;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
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.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 图形验证码校验过滤器
|
* 图形验证码校验过滤器
|
||||||
*
|
|
||||||
* @author haoxr
|
|
||||||
* @since 2022/10/1
|
|
||||||
*/
|
*/
|
||||||
public class CaptchaValidationFilter extends OncePerRequestFilter {
|
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_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 RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
private final CodeGenerator codeGenerator;
|
private final CodeGenerator codeGenerator;
|
||||||
|
|
||||||
public CaptchaValidationFilter(RedisTemplate<String, Object> redisTemplate, CodeGenerator codeGenerator) {
|
public CaptchaValidationFilter(RedisTemplate<String, Object> redisTemplate, CodeGenerator codeGenerator) {
|
||||||
@@ -41,37 +48,111 @@ public class CaptchaValidationFilter extends OncePerRequestFilter {
|
|||||||
this.codeGenerator = codeGenerator;
|
this.codeGenerator = codeGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
|
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);
|
if (!LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
|
||||||
// TODO 兼容没有验证码的版本(线上请移除这个判断)
|
|
||||||
if (StrUtil.isBlank(captchaCode)) {
|
|
||||||
chain.doFilter(request, response);
|
chain.doFilter(request, response);
|
||||||
return;
|
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(
|
String cacheVerifyCode = (String) redisTemplate.opsForValue().get(
|
||||||
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey)
|
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId)
|
||||||
);
|
);
|
||||||
if (cacheVerifyCode == null) {
|
if (cacheVerifyCode == null) {
|
||||||
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED);
|
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED);
|
||||||
} else {
|
return;
|
||||||
// 验证码比对
|
}
|
||||||
|
|
||||||
if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
|
if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
|
||||||
chain.doFilter(request, response);
|
HttpServletRequest repeatableRequest = new RepeatableReadRequestWrapper(requestWrapper, bodyBytes);
|
||||||
|
chain.doFilter(repeatableRequest, response);
|
||||||
} else {
|
} else {
|
||||||
WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR);
|
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;
|
private Integer dataScope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户ID
|
||||||
|
*/
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 角色权限集合
|
* 角色权限集合
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ public class SysUserDetails implements UserDetails {
|
|||||||
*/
|
*/
|
||||||
private Integer dataScope;
|
private Integer dataScope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户ID
|
||||||
|
*/
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户角色权限集合
|
* 用户角色权限集合
|
||||||
*/
|
*/
|
||||||
@@ -73,6 +78,7 @@ public class SysUserDetails implements UserDetails {
|
|||||||
this.enabled = ObjectUtil.equal(user.getStatus(), 1);
|
this.enabled = ObjectUtil.equal(user.getStatus(), 1);
|
||||||
this.deptId = user.getDeptId();
|
this.deptId = user.getDeptId();
|
||||||
this.dataScope = user.getDataScope();
|
this.dataScope = user.getDataScope();
|
||||||
|
this.tenantId = user.getTenantId();
|
||||||
|
|
||||||
// 初始化角色权限集合
|
// 初始化角色权限集合
|
||||||
this.authorities = CollectionUtil.isNotEmpty(user.getRoles())
|
this.authorities = CollectionUtil.isNotEmpty(user.getRoles())
|
||||||
|
|||||||
@@ -54,4 +54,9 @@ public class UserAuthCredentials {
|
|||||||
*/
|
*/
|
||||||
private Integer dataScope;
|
private Integer dataScope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户ID(从登录上下文中获取)
|
||||||
|
*/
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import java.util.*;
|
|||||||
/**
|
/**
|
||||||
* SpringSecurity 权限校验
|
* SpringSecurity 权限校验
|
||||||
*
|
*
|
||||||
* @author haoxr
|
* @author Ray.Hao
|
||||||
* @since 2022/2/22
|
* @since 0.0.1
|
||||||
*/
|
*/
|
||||||
@Component("ss")
|
@Component("ss")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.youlai.boot.security.service;
|
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.SysUserDetails;
|
||||||
import com.youlai.boot.security.model.UserAuthCredentials;
|
import com.youlai.boot.security.model.UserAuthCredentials;
|
||||||
import com.youlai.boot.system.service.UserService;
|
import com.youlai.boot.system.service.UserService;
|
||||||
@@ -37,6 +38,8 @@ public class SysUserDetailsService implements UserDetailsService {
|
|||||||
if (userAuthCredentials == null) {
|
if (userAuthCredentials == null) {
|
||||||
throw new UsernameNotFoundException(username);
|
throw new UsernameNotFoundException(username);
|
||||||
}
|
}
|
||||||
|
// 将当前上下文中的租户ID写入认证凭证,便于后续 Token 携带租户信息
|
||||||
|
userAuthCredentials.setTenantId(TenantContextHolder.getTenantId());
|
||||||
return new SysUserDetails(userAuthCredentials);
|
return new SysUserDetails(userAuthCredentials);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 记录异常日志
|
// 记录异常日志
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ public class JwtTokenManager implements TokenManager {
|
|||||||
userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID
|
userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID
|
||||||
userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID
|
userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID
|
||||||
userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围
|
userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围
|
||||||
|
userDetails.setTenantId(payloads.getLong(JwtClaimConstants.TENANT_ID)); // 租户ID
|
||||||
|
|
||||||
userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名
|
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.USER_ID, userDetails.getUserId()); // 用户ID
|
||||||
payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID
|
payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID
|
||||||
payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围
|
payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围
|
||||||
|
payload.put(JwtClaimConstants.TENANT_ID, userDetails.getTenantId()); // 租户ID
|
||||||
|
|
||||||
// claims 中添加角色信息
|
// claims 中添加角色信息
|
||||||
Set<String> roles = authentication.getAuthorities().stream()
|
Set<String> roles = authentication.getAuthorities().stream()
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ public class RedisTokenManager implements TokenManager {
|
|||||||
user.getUsername(),
|
user.getUsername(),
|
||||||
user.getDeptId(),
|
user.getDeptId(),
|
||||||
user.getDataScope(),
|
user.getDataScope(),
|
||||||
|
user.getTenantId(),
|
||||||
user.getAuthorities().stream()
|
user.getAuthorities().stream()
|
||||||
.map(GrantedAuthority::getAuthority)
|
.map(GrantedAuthority::getAuthority)
|
||||||
.collect(Collectors.toSet())
|
.collect(Collectors.toSet())
|
||||||
@@ -268,6 +269,7 @@ public class RedisTokenManager implements TokenManager {
|
|||||||
userDetails.setUsername(onlineUser.getUsername());
|
userDetails.setUsername(onlineUser.getUsername());
|
||||||
userDetails.setDeptId(onlineUser.getDeptId());
|
userDetails.setDeptId(onlineUser.getDeptId());
|
||||||
userDetails.setDataScope(onlineUser.getDataScope());
|
userDetails.setDataScope(onlineUser.getDataScope());
|
||||||
|
userDetails.setTenantId(onlineUser.getTenantId());
|
||||||
userDetails.setAuthorities(authorities);
|
userDetails.setAuthorities(authorities);
|
||||||
return userDetails;
|
return userDetails;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Tag(name = "租户管理接口")
|
@Tag(name = "租户管理接口")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/tenant")
|
@RequestMapping("/api/v1/tenants")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true", matchIfMissing = false)
|
@ConditionalOnProperty(prefix = "youlai.tenant", name = "enabled", havingValue = "true", matchIfMissing = false)
|
||||||
@@ -44,7 +44,7 @@ public class TenantController {
|
|||||||
* @return 租户列表
|
* @return 租户列表
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "获取当前用户的租户列表")
|
@Operation(summary = "获取当前用户的租户列表")
|
||||||
@GetMapping("/list")
|
@GetMapping
|
||||||
public Result<List<TenantVO>> getTenantList() {
|
public Result<List<TenantVO>> getTenantList() {
|
||||||
Long userId = SecurityUtils.getUserId();
|
Long userId = SecurityUtils.getUserId();
|
||||||
List<TenantVO> tenantList = tenantService.getTenantListByUserId(userId);
|
List<TenantVO> tenantList = tenantService.getTenantListByUserId(userId);
|
||||||
@@ -72,14 +72,13 @@ public class TenantController {
|
|||||||
* 切换租户
|
* 切换租户
|
||||||
* <p>
|
* <p>
|
||||||
* 切换当前用户的租户上下文,需要验证用户是否有权限访问该租户
|
* 切换当前用户的租户上下文,需要验证用户是否有权限访问该租户
|
||||||
* 并记录审计日志
|
|
||||||
* </p>
|
* </p>
|
||||||
*
|
*
|
||||||
* @param tenantId 目标租户ID
|
* @param tenantId 目标租户ID
|
||||||
* @return 切换结果
|
* @return 切换结果
|
||||||
*/
|
*/
|
||||||
@Operation(summary = "切换租户")
|
@Operation(summary = "切换租户")
|
||||||
@PostMapping("/switch/{tenantId}")
|
@PostMapping("/{tenantId}/switch")
|
||||||
public Result<TenantVO> switchTenant(
|
public Result<TenantVO> switchTenant(
|
||||||
@Parameter(description = "租户ID") @PathVariable Long tenantId,
|
@Parameter(description = "租户ID") @PathVariable Long tenantId,
|
||||||
HttpServletRequest request
|
HttpServletRequest request
|
||||||
@@ -89,41 +88,30 @@ public class TenantController {
|
|||||||
|
|
||||||
log.info("用户 {} 请求切换租户:{} -> {}", userId, fromTenantId, tenantId);
|
log.info("用户 {} 请求切换租户:{} -> {}", userId, fromTenantId, tenantId);
|
||||||
|
|
||||||
try {
|
|
||||||
// 验证用户是否有权限访问该租户
|
// 验证用户是否有权限访问该租户
|
||||||
boolean hasPermission = tenantService.hasTenantPermission(userId, tenantId);
|
boolean hasPermission = tenantService.hasTenantPermission(userId, tenantId);
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
log.warn("用户 {} 无权限访问租户 {}", userId, tenantId);
|
log.warn("用户 {} 无权限访问租户 {}", userId, tenantId);
|
||||||
// 记录失败日志
|
|
||||||
tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, "无权限访问该租户", request);
|
|
||||||
return Result.failed("无权限访问该租户");
|
return Result.failed("无权限访问该租户");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证租户是否存在且正常
|
// 验证租户是否存在且正常
|
||||||
TenantVO tenant = tenantService.getTenantById(tenantId);
|
TenantVO tenant = tenantService.getTenantById(tenantId);
|
||||||
if (tenant == null) {
|
if (tenant == null) {
|
||||||
tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, "租户不存在", request);
|
log.warn("用户 {} 尝试切换到不存在的租户 {}", userId, tenantId);
|
||||||
return Result.failed("租户不存在");
|
return Result.failed("租户不存在");
|
||||||
}
|
}
|
||||||
if (tenant.getStatus() == null || tenant.getStatus() != 1) {
|
if (tenant.getStatus() == null || tenant.getStatus() != 1) {
|
||||||
tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, false, "租户已禁用", request);
|
log.warn("用户 {} 尝试切换到已禁用的租户 {}", userId, tenantId);
|
||||||
return Result.failed("租户已禁用");
|
return Result.failed("租户已禁用");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置新的租户上下文
|
// 设置新的租户上下文
|
||||||
TenantContextHolder.setTenantId(tenantId);
|
TenantContextHolder.setTenantId(tenantId);
|
||||||
|
|
||||||
// 记录成功日志
|
log.info("用户 {} 成功切换租户:{} -> {}", userId, fromTenantId, tenantId);
|
||||||
tenantService.recordTenantSwitch(userId, fromTenantId, tenantId, true, null, request);
|
|
||||||
|
|
||||||
log.info("用户 {} 成功切换租户到 {}", userId, tenantId);
|
|
||||||
|
|
||||||
return Result.success(tenant);
|
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;
|
package com.youlai.boot.system.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
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.system.model.form.MenuForm;
|
||||||
import com.youlai.boot.common.model.Option;
|
import com.youlai.boot.common.model.Option;
|
||||||
import com.youlai.boot.system.model.entity.Menu;
|
import com.youlai.boot.system.model.entity.Menu;
|
||||||
@@ -79,5 +79,5 @@ public interface MenuService extends IService<Menu> {
|
|||||||
* @param parentMenuId 父菜单ID
|
* @param parentMenuId 父菜单ID
|
||||||
* @param genConfig 实体名
|
* @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-无权限
|
* @return true-有权限,false-无权限
|
||||||
*/
|
*/
|
||||||
boolean hasTenantPermission(Long userId, Long tenantId);
|
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 用户名
|
* @param username 用户名
|
||||||
* @return {@link UserAuthCredentials}
|
* @return {@link UserAuthCredentials}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
UserAuthCredentials getAuthCredentialsByUsername(String username);
|
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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
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.security.util.SecurityUtils;
|
||||||
import com.youlai.boot.system.converter.MenuConverter;
|
import com.youlai.boot.system.converter.MenuConverter;
|
||||||
import com.youlai.boot.system.mapper.MenuMapper;
|
import com.youlai.boot.system.mapper.MenuMapper;
|
||||||
@@ -427,14 +427,14 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
|||||||
* 代码生成时添加菜单
|
* 代码生成时添加菜单
|
||||||
*
|
*
|
||||||
* @param parentMenuId 父菜单ID
|
* @param parentMenuId 父菜单ID
|
||||||
* @param genConfig 实体名称
|
* @param genTable 实体名称
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void addMenuForCodegen(Long parentMenuId, GenConfig genConfig) {
|
public void addMenuForCodegen(Long parentMenuId, GenTable genTable) {
|
||||||
Menu parentMenu = this.getById(parentMenuId);
|
Menu parentMenu = this.getById(parentMenuId);
|
||||||
Assert.notNull(parentMenu, "上级菜单不存在");
|
Assert.notNull(parentMenu, "上级菜单不存在");
|
||||||
|
|
||||||
String entityName = genConfig.getEntityName();
|
String entityName = genTable.getEntityName();
|
||||||
|
|
||||||
long count = this.count(new LambdaQueryWrapper<Menu>().eq(Menu::getRouteName, entityName));
|
long count = this.count(new LambdaQueryWrapper<Menu>().eq(Menu::getRouteName, entityName));
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
@@ -453,11 +453,11 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
|||||||
|
|
||||||
Menu menu = new Menu();
|
Menu menu = new Menu();
|
||||||
menu.setParentId(parentMenuId);
|
menu.setParentId(parentMenuId);
|
||||||
menu.setName(genConfig.getBusinessName());
|
menu.setName(genTable.getBusinessName());
|
||||||
|
|
||||||
menu.setRouteName(entityName);
|
menu.setRouteName(entityName);
|
||||||
menu.setRoutePath(StrUtil.toSymbolCase(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.setType(MenuTypeEnum.MENU.getValue());
|
||||||
menu.setSort(sort);
|
menu.setSort(sort);
|
||||||
menu.setVisible(1);
|
menu.setVisible(1);
|
||||||
@@ -470,7 +470,7 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
|||||||
this.updateById(menu);
|
this.updateById(menu);
|
||||||
|
|
||||||
// 生成CURD按钮权限
|
// 生成CURD按钮权限
|
||||||
String permPrefix = genConfig.getModuleName() + ":" + genConfig.getTableName().replace("_", "-") + ":";
|
String permPrefix = genTable.getModuleName() + ":" + genTable.getTableName().replace("_", "-") + ":";
|
||||||
String[] actions = {"查询", "新增", "修改", "删除"};
|
String[] actions = {"查询", "新增", "修改", "删除"};
|
||||||
String[] perms = {"list", "create", "update", "delete"};
|
String[] perms = {"list", "create", "update", "delete"};
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
package com.youlai.boot.system.service.impl;
|
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.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.youlai.boot.common.tenant.TenantContextHolder;
|
import com.youlai.boot.common.tenant.TenantContextHolder;
|
||||||
import com.youlai.boot.system.mapper.TenantMapper;
|
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.UserMapper;
|
||||||
import com.youlai.boot.system.mapper.UserTenantMapper;
|
|
||||||
import com.youlai.boot.system.model.entity.Tenant;
|
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.User;
|
||||||
import com.youlai.boot.system.model.entity.UserTenant;
|
|
||||||
import com.youlai.boot.system.model.vo.TenantVO;
|
import com.youlai.boot.system.model.vo.TenantVO;
|
||||||
import com.youlai.boot.system.service.TenantService;
|
import com.youlai.boot.system.service.TenantService;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 租户服务实现类
|
* 租户服务实现类
|
||||||
@@ -35,8 +29,6 @@ import java.util.stream.Collectors;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> implements TenantService {
|
public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> implements TenantService {
|
||||||
|
|
||||||
private final UserTenantMapper userTenantMapper;
|
|
||||||
private final TenantSwitchLogMapper tenantSwitchLogMapper;
|
|
||||||
private final UserMapper userMapper;
|
private final UserMapper userMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -44,21 +36,34 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
|||||||
// 临时忽略租户过滤,查询所有租户
|
// 临时忽略租户过滤,查询所有租户
|
||||||
TenantContextHolder.setIgnoreTenant(true);
|
TenantContextHolder.setIgnoreTenant(true);
|
||||||
try {
|
try {
|
||||||
// 查询用户关联的租户ID列表
|
// 先根据用户ID查询用户信息(获取 username)
|
||||||
List<UserTenant> userTenants = userTenantMapper.selectList(
|
User user = userMapper.selectById(userId);
|
||||||
new LambdaQueryWrapper<UserTenant>()
|
if (user == null) {
|
||||||
.eq(UserTenant::getUserId, userId)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userTenants.isEmpty()) {
|
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取租户ID列表
|
// 通过 username 查询该用户在所有租户下的记录,获取租户ID列表
|
||||||
List<Long> tenantIds = userTenants.stream()
|
List<User> users = userMapper.selectList(
|
||||||
.map(UserTenant::getTenantId)
|
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());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (tenantIds.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
// 查询租户信息
|
// 查询租户信息
|
||||||
List<Tenant> tenants = this.list(
|
List<Tenant> tenants = this.list(
|
||||||
new LambdaQueryWrapper<Tenant>()
|
new LambdaQueryWrapper<Tenant>()
|
||||||
@@ -67,17 +72,19 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
|||||||
.orderByDesc(Tenant::getId)
|
.orderByDesc(Tenant::getId)
|
||||||
);
|
);
|
||||||
|
|
||||||
// 转换为VO并标记默认租户
|
// 转换为VO,第一个租户作为默认租户
|
||||||
return tenants.stream().map(tenant -> {
|
return IntStream.range(0, tenants.size())
|
||||||
|
.mapToObj(index -> {
|
||||||
|
Tenant tenant = tenants.get(index);
|
||||||
TenantVO vo = new TenantVO();
|
TenantVO vo = new TenantVO();
|
||||||
BeanUtils.copyProperties(tenant, vo);
|
BeanUtils.copyProperties(tenant, vo);
|
||||||
// 查找是否为默认租户
|
// 第一个租户作为默认租户
|
||||||
userTenants.stream()
|
if (index == 0) {
|
||||||
.filter(ut -> ut.getTenantId().equals(tenant.getId()) && ut.getIsDefault() == 1)
|
vo.setIsDefault(true);
|
||||||
.findFirst()
|
}
|
||||||
.ifPresent(ut -> vo.setIsDefault(true));
|
|
||||||
return vo;
|
return vo;
|
||||||
}).collect(Collectors.toList());
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
} finally {
|
} finally {
|
||||||
TenantContextHolder.setIgnoreTenant(false);
|
TenantContextHolder.setIgnoreTenant(false);
|
||||||
}
|
}
|
||||||
@@ -119,94 +126,25 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
|||||||
public boolean hasTenantPermission(Long userId, Long tenantId) {
|
public boolean hasTenantPermission(Long userId, Long tenantId) {
|
||||||
TenantContextHolder.setIgnoreTenant(true);
|
TenantContextHolder.setIgnoreTenant(true);
|
||||||
try {
|
try {
|
||||||
UserTenant userTenant = userTenantMapper.selectOne(
|
// 先根据用户ID查询用户信息(获取 username)
|
||||||
new LambdaQueryWrapper<UserTenant>()
|
User user = userMapper.selectById(userId);
|
||||||
.eq(UserTenant::getUserId, userId)
|
if (user == null) {
|
||||||
.eq(UserTenant::getTenantId, tenantId)
|
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")
|
.last("LIMIT 1")
|
||||||
);
|
);
|
||||||
return userTenant != null;
|
return tenantUser != null;
|
||||||
} finally {
|
} finally {
|
||||||
TenantContextHolder.setIgnoreTenant(false);
|
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.service.PermissionService;
|
||||||
import com.youlai.boot.security.token.TokenManager;
|
import com.youlai.boot.security.token.TokenManager;
|
||||||
import com.youlai.boot.security.util.SecurityUtils;
|
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.platform.mail.service.MailService;
|
||||||
import com.youlai.boot.system.converter.UserConverter;
|
import com.youlai.boot.system.converter.UserConverter;
|
||||||
import com.youlai.boot.system.enums.DictCodeEnum;
|
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.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();
|
String username = userForm.getUsername();
|
||||||
|
|
||||||
long count = this.count(new LambdaQueryWrapper<User>().eq(User::getUsername, username));
|
|
||||||
Assert.isTrue(count == 0, "用户名已存在");
|
|
||||||
|
|
||||||
// 实体转换 form->entity
|
// 实体转换 form->entity
|
||||||
User entity = userConverter.toEntity(userForm);
|
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);
|
String defaultEncryptPwd = passwordEncoder.encode(SystemConstants.DEFAULT_PASSWORD);
|
||||||
entity.setPassword(defaultEncryptPwd);
|
entity.setPassword(defaultEncryptPwd);
|
||||||
entity.setCreateBy(SecurityUtils.getUserId());
|
entity.setCreateBy(SecurityUtils.getUserId());
|
||||||
|
|
||||||
|
// 注意:租户ID由 MyMetaObjectHandler.insertFill() 自动填充,无需手动设置
|
||||||
|
|
||||||
// 新增用户
|
// 新增用户
|
||||||
boolean result = this.save(entity);
|
boolean result = this.save(entity);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
// 保存用户角色
|
// 保存用户角色
|
||||||
userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds());
|
userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds());
|
||||||
|
|
||||||
// 如果启用多租户,保存用户租户关联
|
|
||||||
if (Boolean.TRUE.equals(tenantProperties.getEnabled())) {
|
|
||||||
saveUserTenantRelation(entity.getId(), entity.getTenantId(), true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -166,43 +170,38 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
|
|
||||||
String username = userForm.getUsername();
|
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>()
|
long count = this.count(new LambdaQueryWrapper<User>()
|
||||||
.eq(User::getUsername, username)
|
.eq(User::getUsername, username)
|
||||||
|
.eq(User::getTenantId, currentTenantId)
|
||||||
.ne(User::getId, userId)
|
.ne(User::getId, userId)
|
||||||
);
|
);
|
||||||
Assert.isTrue(count == 0, "用户名已存在");
|
Assert.isTrue(count == 0, "该租户下用户名已存在");
|
||||||
|
|
||||||
// 获取原用户信息,用于比较租户是否变更
|
|
||||||
User oldUser = this.getById(userId);
|
|
||||||
Long oldTenantId = oldUser != null ? oldUser.getTenantId() : null;
|
|
||||||
|
|
||||||
// form -> entity
|
// form -> entity
|
||||||
User entity = userConverter.toEntity(userForm);
|
User entity = userConverter.toEntity(userForm);
|
||||||
entity.setUpdateBy(SecurityUtils.getUserId());
|
entity.setUpdateBy(SecurityUtils.getUserId());
|
||||||
|
|
||||||
|
// 保持租户ID不变(不允许跨租户修改用户)
|
||||||
|
entity.setTenantId(oldTenantId);
|
||||||
|
|
||||||
// 修改用户
|
// 修改用户
|
||||||
boolean result = this.updateById(entity);
|
boolean result = this.updateById(entity);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
// 保存用户角色
|
// 保存用户角色
|
||||||
userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds());
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -224,16 +223,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
|
|
||||||
boolean result = this.removeByIds(ids);
|
boolean result = this.removeByIds(ids);
|
||||||
|
|
||||||
// 如果启用多租户,删除用户租户关联
|
// 新设计:用户删除时,tenant_id 字段会随用户记录一起逻辑删除,无需额外处理
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -256,6 +246,46 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
return userAuthCredentials;
|
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获取用户认证信息
|
* 根据OpenID获取用户认证信息
|
||||||
*
|
*
|
||||||
@@ -731,45 +761,5 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
return userConverter.toOptions(list);
|
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 数据源(默认启用) ===
|
# === MySQL 数据源(默认启用) ===
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
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
|
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
|
username: root
|
||||||
password: 123456
|
password: Youlai@2025
|
||||||
|
|
||||||
# === PostgreSQL 数据源示例(按需启用) ===
|
# === PostgreSQL 数据源示例(按需启用) ===
|
||||||
# driver-class-name: org.postgresql.Driver
|
# driver-class-name: org.postgresql.Driver
|
||||||
@@ -275,7 +275,7 @@ youlai:
|
|||||||
tenant:
|
tenant:
|
||||||
# 是否启用多租户功能(默认:false)
|
# 是否启用多租户功能(默认:false)
|
||||||
# 设置为 true 启用多租户,设置为 false 禁用多租户(零成本切换)
|
# 设置为 true 启用多租户,设置为 false 禁用多租户(零成本切换)
|
||||||
enabled: false
|
enabled: true
|
||||||
|
|
||||||
# 租户字段名(默认:tenant_id)
|
# 租户字段名(默认:tenant_id)
|
||||||
column: tenant_id
|
column: tenant_id
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ codegen:
|
|||||||
moduleName: system
|
moduleName: system
|
||||||
# 排除数据表
|
# 排除数据表
|
||||||
excludeTables:
|
excludeTables:
|
||||||
- gen_config
|
- gen_table
|
||||||
- gen_field_config
|
- gen_table_column
|
||||||
## 模板配置
|
## 模板配置
|
||||||
templateConfigs:
|
templateConfigs:
|
||||||
API:
|
API:
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<!DOCTYPE mapper
|
<!DOCTYPE mapper
|
||||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"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"/>
|
<id property="id" column="id"/>
|
||||||
<result property="userId" column="user_id"/>
|
<result property="userId" column="user_id"/>
|
||||||
<result property="username" column="username"/>
|
<result property="username" column="username"/>
|
||||||
@@ -28,55 +28,55 @@
|
|||||||
<result property="updateTime" column="update_time"/>
|
<result property="updateTime" column="update_time"/>
|
||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<select id="getLogPage" resultMap="AiCommandLogVOResult">
|
<select id="getRecordPage" resultMap="AiCommandRecordResultMap">
|
||||||
SELECT
|
SELECT
|
||||||
acl.id,
|
acr.id,
|
||||||
acl.user_id,
|
acr.user_id,
|
||||||
acl.username,
|
acr.username,
|
||||||
acl.original_command,
|
acr.original_command,
|
||||||
acl.ai_provider,
|
acr.ai_provider,
|
||||||
acl.ai_model,
|
acr.ai_model,
|
||||||
acl.parse_status,
|
acr.parse_status,
|
||||||
acl.function_calls,
|
acr.function_calls,
|
||||||
acl.explanation,
|
acr.explanation,
|
||||||
acl.confidence,
|
acr.confidence,
|
||||||
acl.parse_error_message,
|
acr.parse_error_message,
|
||||||
acl.input_tokens,
|
acr.input_tokens,
|
||||||
acl.output_tokens,
|
acr.output_tokens,
|
||||||
acl.parse_duration_ms,
|
acr.parse_duration_ms,
|
||||||
acl.function_name,
|
acr.function_name,
|
||||||
acl.function_arguments,
|
acr.function_arguments,
|
||||||
acl.execute_status,
|
acr.execute_status,
|
||||||
acl.execute_error_message,
|
acr.execute_error_message,
|
||||||
acl.ip_address,
|
acr.ip_address,
|
||||||
acl.create_time,
|
acr.create_time,
|
||||||
acl.update_time
|
acr.update_time
|
||||||
FROM ai_command_log acl
|
FROM ai_command_record acr
|
||||||
<where>
|
<where>
|
||||||
<if test="queryParams.keywords != null and queryParams.keywords != ''">
|
<if test="queryParams.keywords != null and queryParams.keywords != ''">
|
||||||
(
|
(
|
||||||
acl.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
acr.original_command LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||||
OR acl.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
OR acr.function_name LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||||
OR acl.username LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
OR acr.username LIKE CONCAT('%', #{queryParams.keywords}, '%')
|
||||||
)
|
)
|
||||||
</if>
|
</if>
|
||||||
<if test="queryParams.executeStatus != null">
|
<if test="queryParams.executeStatus != null">
|
||||||
AND acl.execute_status = #{queryParams.executeStatus}
|
AND acr.execute_status = #{queryParams.executeStatus}
|
||||||
</if>
|
</if>
|
||||||
<if test="queryParams.userId != null">
|
<if test="queryParams.userId != null">
|
||||||
AND acl.user_id = #{queryParams.userId}
|
AND acr.user_id = #{queryParams.userId}
|
||||||
</if>
|
</if>
|
||||||
<if test="queryParams.parseStatus != null">
|
<if test="queryParams.parseStatus != null">
|
||||||
AND acl.parse_status = #{queryParams.parseStatus}
|
AND acr.parse_status = #{queryParams.parseStatus}
|
||||||
</if>
|
</if>
|
||||||
<if test="queryParams.functionName != null and queryParams.functionName != ''">
|
<if test="queryParams.functionName != null and queryParams.functionName != ''">
|
||||||
AND acl.function_name = #{queryParams.functionName}
|
AND acr.function_name = #{queryParams.functionName}
|
||||||
</if>
|
</if>
|
||||||
<if test="queryParams.aiProvider != null and queryParams.aiProvider != ''">
|
<if test="queryParams.aiProvider != null and queryParams.aiProvider != ''">
|
||||||
AND acl.ai_provider = #{queryParams.aiProvider}
|
AND acr.ai_provider = #{queryParams.aiProvider}
|
||||||
</if>
|
</if>
|
||||||
<if test="queryParams.aiModel != null and queryParams.aiModel != ''">
|
<if test="queryParams.aiModel != null and queryParams.aiModel != ''">
|
||||||
AND acl.ai_model = #{queryParams.aiModel}
|
AND acr.ai_model = #{queryParams.aiModel}
|
||||||
</if>
|
</if>
|
||||||
<if test="
|
<if test="
|
||||||
queryParams.createTime != null
|
queryParams.createTime != null
|
||||||
@@ -86,10 +86,10 @@
|
|||||||
and queryParams.createTime[1] != null
|
and queryParams.createTime[1] != null
|
||||||
and queryParams.createTime[1] != ''
|
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>
|
</if>
|
||||||
</where>
|
</where>
|
||||||
ORDER BY acl.create_time DESC
|
ORDER BY acr.create_time DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured
|
CASE WHEN t2.id IS NOT NULL THEN 1 ELSE 0 END AS isConfigured
|
||||||
FROM
|
FROM
|
||||||
information_schema.tables t1
|
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
|
WHERE
|
||||||
t1.TABLE_SCHEMA = (SELECT DATABASE())
|
t1.TABLE_SCHEMA = (SELECT DATABASE())
|
||||||
AND t1.table_type = 'BASE TABLE'
|
AND t1.table_type = 'BASE TABLE'
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
ALL_TABLES t1
|
ALL_TABLES t1
|
||||||
LEFT JOIN ALL_TAB_COMMENTS c ON t1.TABLE_NAME = c.TABLE_NAME AND t1.OWNER = c.OWNER
|
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 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
|
WHERE
|
||||||
t1.OWNER = 'YOULAI_BOOT'
|
t1.OWNER = 'YOULAI_BOOT'
|
||||||
<if test="queryParams.keywords != null and queryParams.keywords.trim() neq ''">
|
<if test="queryParams.keywords != null and queryParams.keywords.trim() neq ''">
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
<!DOCTYPE mapper
|
<!DOCTYPE mapper
|
||||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"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>
|
</mapper>
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
<!DOCTYPE mapper
|
<!DOCTYPE mapper
|
||||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
"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>
|
</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