From 41cbe2bd548d07d9e96400d624136fea0dc73b34 Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Thu, 18 Dec 2025 16:48:45 +0800 Subject: [PATCH] =?UTF-8?q?ai=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../database/postgres/sql/createTableAI.sql | 6 +- .../postgres/sql/createTableBidding.sql | 4 +- .../database/postgres/sql/createTableFile.sql | 2 +- .../postgres/sql/createTableWorkcase.sql | 11 +- .../postgres/sql/initDataPermission.sql | 213 +++--- urbanLifelineServ/.gitignore | 3 +- .../.windsurf/rules/urbanlife.md | 3 +- urbanLifelineServ/ai/AI模块功能说明.md | 51 ++ urbanLifelineServ/ai/pom.xml | 46 +- .../org/xyzh/ai/client/DifyApiClient.java | 5 +- .../org/xyzh/ai/client/dto/DifyFileInfo.java | 97 +++ .../xyzh/ai/controller/AgentController.java | 147 ++++ .../xyzh/ai/controller/ChatController.java | 305 ++++++++ .../ai/controller/KnowledgeController.java | 350 ++++++++++ .../ai/handler/PromptCardsTypeHandler.java | 64 ++ .../org/xyzh/ai/mapper/TbAgentMapper.java | 70 ++ .../java/org/xyzh/ai/mapper/TbChatMapper.java | 60 ++ .../xyzh/ai/mapper/TbChatMessageMapper.java | 62 ++ .../xyzh/ai/mapper/TbKnowledgeFileMapper.java | 78 +++ .../org/xyzh/ai/mapper/TbKnowledgeMapper.java | 62 ++ .../service/impl/AIFileUploadServiceImpl.java | 207 ++++++ .../ai/service/impl/AgentChatServiceImpl.java | 476 +++++++++++++ .../ai/service/impl/AgentServiceImpl.java | 253 +++++++ .../ai/service/impl/DifyProxyServiceImpl.java | 165 +++++ .../ai/service/impl/KnowledgeServiceImpl.java | 661 ++++++++++++++++++ .../ai/src/main/resources/application.yml | 25 +- .../main/resources/mapper/TbAgentMapper.xml | 146 ++++ .../main/resources/mapper/TbChatMapper.xml | 103 +++ .../resources/mapper/TbChatMessageMapper.xml | 89 +++ .../mapper/TbKnowledgeFileMapper.xml | 96 +++ .../resources/mapper/TbKnowledgeMapper.xml | 198 ++++++ .../org/xyzh/api/ai/dto/ChatPrepareData.java | 32 + .../java/org/xyzh/api/ai/dto/TbAgent.java | 3 + .../org/xyzh/api/ai/dto/TbChatMessage.java | 3 + .../java/org/xyzh/api/ai/dto/TbKnowledge.java | 2 +- .../org/xyzh/api/ai/dto/TbKnowledgeFile.java | 2 +- .../api/ai/service/AIFileUploadService.java | 80 ++- .../xyzh/api/ai/service/AgentChatService.java | 38 +- .../org/xyzh/api/ai/service/AgentService.java | 25 + .../xyzh/api/ai/service/KnowledgeService.java | 164 +++-- .../main/java/org/xyzh/api/ai/vo/AgentVO.java | 50 ++ .../xyzh/api/bidding/dto/BidResponseDTO.java | 2 +- .../api/bidding/dto/BiddingDocumentDTO.java | 2 +- .../xyzh/api/bidding/vo/BidResponseVO.java | 2 +- .../api/bidding/vo/BiddingDocumentVO.java | 2 +- .../org/xyzh/api/file/dto/TbSysFileDTO.java | 2 +- .../xyzh/api/file/service/FileService.java | 12 + .../xyzh/api/system/service/GuestService.java | 63 ++ .../auth/service/impl/AuthServiceImpl.java | 10 +- urbanLifelineServ/common/common-all/pom.xml | 4 + .../xyzh/common/auth/utils/JwtTokenUtil.java | 4 +- .../org/xyzh/common/auth/utils/LoginUtil.java | 116 +++ .../org/xyzh/common/core/page/PageDomain.java | 3 + .../org/xyzh/common/core/page/PageParam.java | 7 + .../org/xyzh/common/dto/sys/TbGuestDTO.java | 27 + .../main/java/org/xyzh/common/vo/BaseVO.java | 6 + .../common/common-exception/pom.xml | 46 ++ .../common/exception/BusinessException.java | 36 + .../handler/GlobalExceptionHandler.java | 124 ++++ .../java/org/xyzh/common/utils/IDUtils.java | 23 - .../java/org/xyzh/common/utils/id/IdUtil.java | 46 ++ .../utils/validation/ValidationUtils.java | 82 ++- .../method/AtLeastOneValidateMethod.java | 0 .../method/InSetValidateMethod.java | 68 ++ .../method/MinFieldsValidateMethod.java | 98 +++ .../method/ObjectValidateMethod.java | 11 + urbanLifelineServ/common/pom.xml | 6 + .../java/org/xyzh/file/mapper/FileMapper.java | 10 + .../file/service/impl/FileServiceImpl.java | 102 ++- urbanLifelineServ/pom.xml | 5 + .../system/controller/GuestController.java | 78 +++ .../system/mapper/user/TbGuestMapper.java | 71 ++ .../system/service/impl/AclServiceImpl.java | 6 +- .../service/impl/DeptRoleServiceImpl.java | 6 +- .../system/service/impl/GuestServiceImpl.java | 81 +++ .../impl/ModulePermissionServiceImpl.java | 6 +- .../service/impl/SysConfigServiceImpl.java | 4 +- .../service/impl/SysUserServiceImpl.java | 6 +- .../system/service/impl/ViewServiceImpl.java | 4 +- .../resources/mapper/user/TbGuestMapper.xml | 147 ++++ 80 files changed, 5434 insertions(+), 351 deletions(-) create mode 100644 urbanLifelineServ/ai/AI模块功能说明.md create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/DifyFileInfo.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/AgentController.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/ChatController.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/KnowledgeController.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/handler/PromptCardsTypeHandler.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbAgentMapper.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbChatMapper.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbChatMessageMapper.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbKnowledgeFileMapper.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbKnowledgeMapper.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AIFileUploadServiceImpl.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentServiceImpl.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/DifyProxyServiceImpl.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/KnowledgeServiceImpl.java create mode 100644 urbanLifelineServ/ai/src/main/resources/mapper/TbAgentMapper.xml create mode 100644 urbanLifelineServ/ai/src/main/resources/mapper/TbChatMapper.xml create mode 100644 urbanLifelineServ/ai/src/main/resources/mapper/TbChatMessageMapper.xml create mode 100644 urbanLifelineServ/ai/src/main/resources/mapper/TbKnowledgeFileMapper.xml create mode 100644 urbanLifelineServ/ai/src/main/resources/mapper/TbKnowledgeMapper.xml create mode 100644 urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/ChatPrepareData.java create mode 100644 urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/vo/AgentVO.java create mode 100644 urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/GuestService.java create mode 100644 urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/utils/LoginUtil.java create mode 100644 urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/dto/sys/TbGuestDTO.java create mode 100644 urbanLifelineServ/common/common-exception/pom.xml create mode 100644 urbanLifelineServ/common/common-exception/src/main/java/org/xyzh/common/exception/BusinessException.java create mode 100644 urbanLifelineServ/common/common-exception/src/main/java/org/xyzh/common/exception/handler/GlobalExceptionHandler.java delete mode 100644 urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/IDUtils.java create mode 100644 urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/id/IdUtil.java create mode 100644 urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/AtLeastOneValidateMethod.java create mode 100644 urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/InSetValidateMethod.java create mode 100644 urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/MinFieldsValidateMethod.java create mode 100644 urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/ObjectValidateMethod.java create mode 100644 urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java create mode 100644 urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/user/TbGuestMapper.java create mode 100644 urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/GuestServiceImpl.java create mode 100644 urbanLifelineServ/system/src/main/resources/mapper/user/TbGuestMapper.xml diff --git a/urbanLifelineServ/.bin/database/postgres/sql/createTableAI.sql b/urbanLifelineServ/.bin/database/postgres/sql/createTableAI.sql index 845e53e5..6e5c68a0 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/createTableAI.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/createTableAI.sql @@ -9,6 +9,7 @@ CREATE TABLE ai.tb_agent( description VARCHAR(500) DEFAULT NULL, -- 智能体描述 link VARCHAR(500) DEFAULT NULL, -- 智能体url api_key VARCHAR(500) NOT NULL, -- dify智能体APIKEY + outer BOOLEAN DEFAULT false, -- 是否是对外智能体,未登录可用 introduce VARCHAR(500) NOT NULL, -- 引导词 prompt_cards JSONB DEFAULT '[]'::jsonb, -- 提示卡片数组 [{file_id:'', prompt:''}] category VARCHAR(50) NOT NULL, -- 分类 @@ -45,8 +46,9 @@ DROP TABLE IF EXISTS ai.tb_chat_message CASCADE; CREATE TABLE ai.tb_chat_message( optsn VARCHAR(50) NOT NULL, -- 流水号 message_id VARCHAR(50) NOT NULL, -- 消息ID + dify_message_id VARCHAR(100) DEFAULT NULL, -- Dify消息ID chat_id VARCHAR(50) NOT NULL, -- 对话ID - role VARCHAR(50) NOT NULL, -- 角色:user-用户/assistant-智能体/recipient-来客 + role VARCHAR(50) NOT NULL, -- 角色:user-用户/ai-智能体/recipient-来客 content TEXT NOT NULL, -- 消息内容 files VARCHAR(50)[] DEFAULT NULL, -- 文件id数组 comment VARCHAR(50) DEFAULT NULL, -- 评价 @@ -144,7 +146,7 @@ CREATE TABLE ai.tb_knowledge_file( file_root_id VARCHAR(50) NOT NULL, -- 文件根ID file_id VARCHAR(50) NOT NULL, -- 文件ID dify_file_id VARCHAR(50) NOT NULL, -- dify文件ID - version VARCHAR(50) NOT NULL, -- 文件版本 + version INTEGER NOT NULL DEFAULT 1, -- 文件版本 create_time TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间 update_time TIMESTAMPTZ DEFAULT NULL, -- 更新时间 delete_time TIMESTAMPTZ DEFAULT NULL, -- 删除时间 diff --git a/urbanLifelineServ/.bin/database/postgres/sql/createTableBidding.sql b/urbanLifelineServ/.bin/database/postgres/sql/createTableBidding.sql index 533724a0..4818df60 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/createTableBidding.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/createTableBidding.sql @@ -64,7 +64,7 @@ CREATE TABLE bidding.tb_bidding_document ( file_path VARCHAR(500), -- 文件路径 file_size BIGINT, -- 文件大小 mime_type VARCHAR(100), -- MIME类型 - version VARCHAR(20) DEFAULT '1.0', -- 版本号 + version INTEGER DEFAULT 1, -- 版本号 language VARCHAR(20) DEFAULT 'zh-CN', -- 语言 page_count INTEGER, -- 页数 parse_status VARCHAR(30) DEFAULT 'pending', -- 解析状态:pending-待解析/parsing-解析中/completed-已完成/failed-失败 @@ -145,7 +145,7 @@ CREATE TABLE bidding.tb_bid_response ( generation_status VARCHAR(30) DEFAULT 'draft', -- 生成状态:draft-草稿/reviewing-审核中/approved-已批准/rejected-已拒绝/submitted-已提交 file_id VARCHAR(50), -- 生成的文件ID file_path VARCHAR(500), -- 文件路径 - version VARCHAR(20) DEFAULT '1.0', -- 版本号 + version INTEGER DEFAULT 1, -- 版本号 parent_version_id VARCHAR(50), -- 父版本ID review_comments TEXT, -- 审核意见 dept_path VARCHAR(255) DEFAULT NULL, -- 部门全路径 diff --git a/urbanLifelineServ/.bin/database/postgres/sql/createTableFile.sql b/urbanLifelineServ/.bin/database/postgres/sql/createTableFile.sql index b1bc95ac..28d46ebb 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/createTableFile.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/createTableFile.sql @@ -16,7 +16,7 @@ CREATE TABLE file.tb_sys_file ( -- TbSysFileDTO 特有字段 file_id VARCHAR(50) NOT NULL, -- 文件ID (主键) file_root_id VARCHAR(50) DEFAULT NULL, -- 文件根ID - version VARCHAR(50) DEFAULT NULL, -- 文件版本 + version INTEGER DEFAULT 1, -- 文件版本 name VARCHAR(255) NOT NULL, -- 文件名 path VARCHAR(500) NOT NULL, -- 文件路径 size BIGINT NOT NULL, -- 文件大小(字节) diff --git a/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql b/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql index 4ccdb093..6c7c7de8 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql @@ -1,15 +1,16 @@ CREATE SCHEMA IF NOT EXISTS workcase; -- 系统外部人员(来客)管理 用于给系统外人员创建id -DROP TABLE IF EXISTS workcase.tb_receptionist CASCADE; -CREATE TABLE workcase.tb_receptionist( +DROP TABLE IF EXISTS sys.tb_guest CASCADE; +CREATE TABLE sys.tb_guest( optsn VARCHAR(50) NOT NULL, -- 流水号 user_id VARCHAR(50) NOT NULL, -- 来客ID name VARCHAR(50) NOT NULL, -- 姓名 - phone VARCHAR(50) NOT NULL, -- 电话 - email VARCHAR(50) NOT NULL, -- 邮箱 - wechat_id VARCHAR(50) NOT NULL, -- 微信号 + phone VARCHAR(50) DEFAULT NULL, -- 电话 + email VARCHAR(50) DEFAULT NULL, -- 邮箱 + wechat_id VARCHAR(50) DEFAULT NULL, -- 微信号 create_time TIMESTAMPTZ NOT NULL DEFAULT now(), -- 创建时间 + update_time TIMESTAMPTZ DEFAULT NULL, -- 创建时间 delete_time TIMESTAMPTZ DEFAULT NULL, -- 删除时间 deleted BOOLEAN NOT NULL DEFAULT false, -- 是否删除 PRIMARY KEY (user_id), diff --git a/urbanLifelineServ/.bin/database/postgres/sql/initDataPermission.sql b/urbanLifelineServ/.bin/database/postgres/sql/initDataPermission.sql index bb8f42f0..3c17047f 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/initDataPermission.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/initDataPermission.sql @@ -38,158 +38,107 @@ INSERT INTO sys.tb_sys_role ( -- 3. 初始化系统模块 -- ============================= INSERT INTO sys.tb_sys_module ( - optsn, module_id, name, description, - creator, dept_path, create_time, deleted + optsn, module_id, name, description, creator, dept_path, create_time, deleted ) VALUES -('MODULE-0001', 'module_system', '系统管理', '用户、角色、权限、部门管理', - 'system', NULL, now(), false), - -('MODULE-0002', 'module_file', '文件管理', '文件上传、下载、关联管理', - 'system', NULL, now(), false), - -('MODULE-0003', 'module_message', '消息通知', '消息发送、接收、模板管理', - 'system', NULL, now(), false), - -('MODULE-0004', 'module_config', '配置管理', '系统配置参数管理', - 'system', NULL, now(), false), - -('MODULE-0005', 'module_knowledge', '知识库', '知识文档管理', - 'system', NULL, now(), false), - -('MODULE-0006', 'module_bidding', '招投标', '招投标业务管理', - 'system', NULL, now(), false), - -('MODULE-0007', 'module_workcase', '智能客服', '客服工单管理', - 'system', NULL, now(), false); +('MODULE-0001', 'module_system', '系统管理', '用户、角色、权限、部门管理', 'system', NULL, now(), false), +('MODULE-0002', 'module_file', '文件管理', '文件上传、下载、关联管理', 'system', NULL, now(), false), +('MODULE-0003', 'module_message', '消息通知', '消息发送、接收、模板管理', 'system', NULL, now(), false), +('MODULE-0004', 'module_config', '配置管理', '系统配置参数管理', 'system', NULL, now(), false), +('MODULE-0008', 'module_agent', '智能体', '智能体管理', 'system', NULL, now(), false), +('MODULE-0005', 'module_knowledge', '知识库', '知识文档管理', 'system', NULL, now(), false), +('MODULE-0006', 'module_bidding', '招投标', '招投标业务管理', 'system', NULL, now(), false), +('MODULE-0007', 'module_workcase', '智能客服', '客服工单管理', 'system', NULL, now(), false); -- ============================= -- 4. 初始化系统权限 -- ============================= INSERT INTO sys.tb_sys_permission ( - optsn, permission_id, name, code, description, module_id, - status, creator, dept_path, create_time, deleted + optsn, permission_id, name, code, description, module_id, status, creator, dept_path, create_time, deleted ) VALUES -- 系统管理模块权限 -('PERM-0001', 'perm_user_view', '用户查看', 'system:user:view', '查看用户列表和详情', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0002', 'perm_user_create', '用户创建', 'system:user:create', '创建新用户', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0003', 'perm_user_edit', '用户编辑', 'system:user:edit', '编辑用户信息', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0004', 'perm_user_delete', '用户删除', 'system:user:delete', '删除用户', 'module_system', - true, 'system', NULL, now(), false), - -('PERM-0011', 'perm_role_view', '角色查看', 'system:role:view', '查看角色列表和详情', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0012', 'perm_role_create', '角色创建', 'system:role:create', '创建新角色', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0013', 'perm_role_edit', '角色编辑', 'system:role:edit', '编辑角色信息', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0014', 'perm_role_delete', '角色删除', 'system:role:delete', '删除角色', 'module_system', - true, 'system', NULL, now(), false), - -('PERM-0021', 'perm_dept_view', '部门查看', 'system:dept:view', '查看部门列表和详情', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0022', 'perm_dept_create', '部门创建', 'system:dept:create', '创建新部门', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0023', 'perm_dept_edit', '部门编辑', 'system:dept:edit', '编辑部门信息', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0024', 'perm_dept_delete', '部门删除', 'system:dept:delete', '删除部门', 'module_system', - true, 'system', NULL, now(), false), - -('PERM-0031', 'perm_permission_view', '权限查看', 'system:permission:view', '查看权限列表', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0032', 'perm_permission_manage', '权限管理', 'system:permission:manage', '管理权限配置', 'module_system', - true, 'system', NULL, now(), false), +('PERM-0001', 'perm_user_view', '用户查看', 'system:user:view', '查看用户列表和详情', 'module_system', true, 'system', NULL, now(), false), +('PERM-0002', 'perm_user_create', '用户创建', 'system:user:create', '创建新用户', 'module_system', true, 'system', NULL, now(), false), +('PERM-0003', 'perm_user_edit', '用户编辑', 'system:user:edit', '编辑用户信息', 'module_system', true, 'system', NULL, now(), false), +('PERM-0004', 'perm_user_delete', '用户删除', 'system:user:delete', '删除用户', 'module_system', true, 'system', NULL, now(), false), +('PERM-0011', 'perm_role_view', '角色查看', 'system:role:view', '查看角色列表和详情', 'module_system', true, 'system', NULL, now(), false), +('PERM-0012', 'perm_role_create', '角色创建', 'system:role:create', '创建新角色', 'module_system', true, 'system', NULL, now(), false), +('PERM-0013', 'perm_role_edit', '角色编辑', 'system:role:edit', '编辑角色信息', 'module_system', true, 'system', NULL, now(), false), +('PERM-0014', 'perm_role_delete', '角色删除', 'system:role:delete', '删除角色', 'module_system', true, 'system', NULL, now(), false), +('PERM-0021', 'perm_dept_view', '部门查看', 'system:dept:view', '查看部门列表和详情', 'module_system', true, 'system', NULL, now(), false), +('PERM-0022', 'perm_dept_create', '部门创建', 'system:dept:create', '创建新部门', 'module_system', true, 'system', NULL, now(), false), +('PERM-0023', 'perm_dept_edit', '部门编辑', 'system:dept:edit', '编辑部门信息', 'module_system', true, 'system', NULL, now(), false), +('PERM-0024', 'perm_dept_delete', '部门删除', 'system:dept:delete', '删除部门', 'module_system', true, 'system', NULL, now(), false), +('PERM-0031', 'perm_permission_view', '权限查看', 'system:permission:view', '查看权限列表', 'module_system', true, 'system', NULL, now(), false), +('PERM-0032', 'perm_permission_manage', '权限管理', 'system:permission:manage', '管理权限配置', 'module_system', true, 'system', NULL, now(), false), -- 系统管理模块导出权限 -('PERM-0041', 'perm_user_export', '用户导出', 'system:user:export', '导出用户数据', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0042', 'perm_role_export', '角色导出', 'system:role:export', '导出角色数据', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0043', 'perm_dept_export', '部门导出', 'system:dept:export', '导出部门数据', 'module_system', - true, 'system', NULL, now(), false), +('PERM-0041', 'perm_user_export', '用户导出', 'system:user:export', '导出用户数据', 'module_system', true, 'system', NULL, now(), false), +('PERM-0042', 'perm_role_export', '角色导出', 'system:role:export', '导出角色数据', 'module_system', true, 'system', NULL, now(), false), +('PERM-0043', 'perm_dept_export', '部门导出', 'system:dept:export', '导出部门数据', 'module_system', true, 'system', NULL, now(), false), -- 文件管理模块权限 -('PERM-0101', 'perm_file_view', '文件查看', 'file:file:view', '查看文件列表', 'module_file', - true, 'system', NULL, now(), false), -('PERM-0102', 'perm_file_upload', '文件上传', 'file:file:upload', '上传文件', 'module_file', - true, 'system', NULL, now(), false), -('PERM-0103', 'perm_file_download', '文件下载', 'file:file:download', '下载文件', 'module_file', - true, 'system', NULL, now(), false), -('PERM-0104', 'perm_file_delete', '文件删除', 'file:file:delete', '删除文件', 'module_file', - true, 'system', NULL, now(), false), -('PERM-0105', 'perm_file_export', '文件导出', 'file:file:export', '导出文件列表数据', 'module_file', - true, 'system', NULL, now(), false), +('PERM-0101', 'perm_file_view', '文件查看', 'file:file:view', '查看文件列表', 'module_file', true, 'system', NULL, now(), false), +('PERM-0102', 'perm_file_upload', '文件上传', 'file:file:upload', '上传文件', 'module_file', true, 'system', NULL, now(), false), +('PERM-0103', 'perm_file_download', '文件下载', 'file:file:download', '下载文件', 'module_file', true, 'system', NULL, now(), false), +('PERM-0104', 'perm_file_delete', '文件删除', 'file:file:delete', '删除文件', 'module_file', true, 'system', NULL, now(), false), +('PERM-0105', 'perm_file_export', '文件导出', 'file:file:export', '导出文件列表数据', 'module_file', true, 'system', NULL, now(), false), +-- 智能体权限 +('PERM-0120', 'perm_ai_create', '智能体创建', 'ai:agent:create', '创建智能体', 'module_agent', true, 'system', NULL, now(), false), +('PERM-0121', 'perm_ai_update', '智能体更新', 'ai:agent:update', '更新智能体', 'module_agent', true, 'system', NULL, now(), false), +('PERM-0122', 'perm_ai_delete', '智能体删除', 'ai:agent:delete', '删除智能体', 'module_agent', true, 'system', NULL, now(), false), +('PERM-0123', 'perm_ai_view', '智能体查询', 'ai:agent:view', '查询智能体', 'module_agent', true, 'system', NULL, now(), false), +-- 智能体对话权限 没有,因为所有人都可以 +-- Dify代理功能权限(知识库分段管理) +('PERM-0130', 'perm_dify_segment_view', '分段查看', 'ai:dify:segment:view', '查看文档分段列表', 'module_knowledge', true, 'system', NULL, now(), false), +('PERM-0131', 'perm_dify_segment_create', '分段创建', 'ai:dify:segment:create', '创建文档分段', 'module_knowledge', true, 'system', NULL, now(), false), +('PERM-0132', 'perm_dify_segment_update', '分段更新', 'ai:dify:segment:update', '更新文档分段', 'module_knowledge', true, 'system', NULL, now(), false), +('PERM-0133', 'perm_dify_segment_delete', '分段删除', 'ai:dify:segment:delete', '删除文档分段', 'module_knowledge', true, 'system', NULL, now(), false), +('PERM-0134', 'perm_dify_document_status', '文档状态管理', 'ai:dify:document:status', '更新文档状态(启用/禁用/归档)', 'module_knowledge', true, 'system', NULL, now(), false), +-- 知识库管理权限 +('PERM-0140', 'perm_knowledge_create', '知识库创建', 'ai:knowledge:create', '创建知识库', 'module_knowledge', true, 'system', NULL, now(), false), +('PERM-0141', 'perm_knowledge_update', '知识库更新', 'ai:knowledge:update', '更新知识库', 'module_knowledge', true, 'system', NULL, now(), false), +('PERM-0142', 'perm_knowledge_delete', '知识库删除', 'ai:knowledge:delete', '删除知识库', 'module_knowledge', true, 'system', NULL, now(), false), +('PERM-0143', 'perm_knowledge_view', '知识库查看', 'ai:knowledge:view', '查看知识库列表和详情', 'module_knowledge', true, 'system', NULL, now(), false), +-- 知识库文件管理权限 +('PERM-0150', 'perm_knowledge_file_upload', '知识库文件上传', 'ai:knowledge:file:upload', '上传文件到知识库', 'module_knowledge', true, 'system', NULL, now(), false), +('PERM-0151', 'perm_knowledge_file_update', '知识库文件更新', 'ai:knowledge:file:update', '更新知识库文件信息', 'module_knowledge', true, 'system', NULL, now(), false), +('PERM-0152', 'perm_knowledge_file_delete', '知识库文件删除', 'ai:knowledge:file:delete', '删除知识库文件', 'module_knowledge', true, 'system', NULL, now(), false), +('PERM-0153', 'perm_knowledge_file_view', '知识库文件查看', 'ai:knowledge:file:view', '查看知识库文件历史', 'module_knowledge', true, 'system', NULL, now(), false), -- 消息通知模块权限 -('PERM-0201', 'perm_message_view', '消息查看', 'message:message:view', '查看消息列表', 'module_message', - true, 'system', NULL, now(), false), -('PERM-0202', 'perm_message_send', '消息发送', 'message:message:send', '发送消息通知', 'module_message', - true, 'system', NULL, now(), false), -('PERM-0203', 'perm_message_manage', '消息管理', 'message:message:manage', '管理消息模板和配置', 'module_message', - true, 'system', NULL, now(), false), -('PERM-0204', 'perm_message_export', '消息导出', 'message:message:export', '导出消息数据', 'module_message', - true, 'system', NULL, now(), false), - +('PERM-0201', 'perm_message_view', '消息查看', 'message:message:view', '查看消息列表', 'module_message', true, 'system', NULL, now(), false), +('PERM-0202', 'perm_message_send', '消息发送', 'message:message:send', '发送消息通知', 'module_message', true, 'system', NULL, now(), false), +('PERM-0203', 'perm_message_manage', '消息管理', 'message:message:manage', '管理消息模板和配置', 'module_message', true, 'system', NULL, now(), false), +('PERM-0204', 'perm_message_export', '消息导出', 'message:message:export', '导出消息数据', 'module_message', true, 'system', NULL, now(), false), -- 配置管理模块权限 -('PERM-0301', 'perm_config_view', '配置查看', 'config:config:view', '查看系统配置', 'module_config', - true, 'system', NULL, now(), false), -('PERM-0302', 'perm_config_edit', '配置编辑', 'config:config:edit', '修改系统配置', 'module_config', - true, 'system', NULL, now(), false), -('PERM-0303', 'perm_config_export', '配置导出', 'config:config:export', '导出系统配置数据', 'module_config', - true, 'system', NULL, now(), false), - +('PERM-0301', 'perm_config_view', '配置查看', 'config:config:view', '查看系统配置', 'module_config', true, 'system', NULL, now(), false), +('PERM-0302', 'perm_config_edit', '配置编辑', 'config:config:edit', '修改系统配置', 'module_config', true, 'system', NULL, now(), false), +('PERM-0303', 'perm_config_export', '配置导出', 'config:config:export', '导出系统配置数据', 'module_config', true, 'system', NULL, now(), false), -- 日志模块权限 -('PERM-0401', 'perm_log_view', '日志查看', 'log:log:view', '查看系统日志', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0402', 'perm_log_export', '日志导出', 'log:log:export', '导出系统日志数据', 'module_system', - true, 'system', NULL, now(), false), - +('PERM-0401', 'perm_log_view', '日志查看', 'log:log:view', '查看系统日志', 'module_system', true, 'system', NULL, now(), false), +('PERM-0402', 'perm_log_export', '日志导出', 'log:log:export', '导出系统日志数据', 'module_system', true, 'system', NULL, now(), false), -- 平台基础菜单访问权限(所有登录用户都有) -('PERM-0501', 'perm_platform_home', '工作台访问', 'platform:home:view', '访问平台工作台', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0502', 'perm_platform_chat', 'AI助手访问', 'platform:chat:view', '访问AI助手', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0503', 'perm_platform_bidding', '招标助手访问', 'platform:bidding:view', '访问招标助手(iframe)', 'module_bidding', - true, 'system', NULL, now(), false), -('PERM-0504', 'perm_platform_workcase', '泰豪小电访问', 'platform:workcase:view', '访问泰豪小电客服(iframe)', 'module_workcase', - true, 'system', NULL, now(), false), -('PERM-0505', 'perm_platform_workflow', '智能体编排访问', 'platform:workflow:view', '访问智能体编排(iframe)', 'module_system', - true, 'system', NULL, now(), false), - +('PERM-0501', 'perm_platform_home', '工作台访问', 'platform:home:view', '访问平台工作台', 'module_system', true, 'system', NULL, now(), false), +('PERM-0502', 'perm_platform_chat', 'AI助手访问', 'platform:chat:view', '访问AI助手', 'module_system', true, 'system', NULL, now(), false), +('PERM-0503', 'perm_platform_bidding', '招标助手访问', 'platform:bidding:view', '访问招标助手(iframe)', 'module_bidding', true, 'system', NULL, now(), false), +('PERM-0504', 'perm_platform_workcase', '泰豪小电访问', 'platform:workcase:view', '访问泰豪小电客服(iframe)', 'module_workcase', true, 'system', NULL, now(), false), +('PERM-0505', 'perm_platform_workflow', '智能体编排访问', 'platform:workflow:view', '访问智能体编排(iframe)', 'module_system', true, 'system', NULL, now(), false), -- Platform 管理后台功能权限 -('PERM-0601', 'perm_platform_admin', '平台管理后台', 'platform:admin:view', '访问平台管理后台', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0602', 'perm_platform_admin_overview', '平台数据概览', 'platform:admin:overview', '访问平台数据概览', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0603', 'perm_platform_admin_user', '平台用户管理', 'platform:admin:user', '访问平台用户管理', 'module_system', - true, 'system', NULL, now(), false), -('PERM-0604', 'perm_platform_admin_knowledge', '平台知识库', 'platform:admin:knowledge', '访问平台知识库', 'module_knowledge', - true, 'system', NULL, now(), false), -('PERM-0605', 'perm_platform_admin_config', '平台系统配置', 'platform:admin:config', '访问平台系统配置', 'module_config', - true, 'system', NULL, now(), false), - +('PERM-0601', 'perm_platform_admin', '平台管理后台', 'platform:admin:view', '访问平台管理后台', 'module_system', true, 'system', NULL, now(), false), +('PERM-0602', 'perm_platform_admin_overview', '平台数据概览', 'platform:admin:overview', '访问平台数据概览', 'module_system', true, 'system', NULL, now(), false), +('PERM-0603', 'perm_platform_admin_user', '平台用户管理', 'platform:admin:user', '访问平台用户管理', 'module_system', true, 'system', NULL, now(), false), +('PERM-0604', 'perm_platform_admin_knowledge', '平台知识库', 'platform:admin:knowledge', '访问平台知识库', 'module_knowledge', true, 'system', NULL, now(), false), +('PERM-0605', 'perm_platform_admin_config', '平台系统配置', 'platform:admin:config', '访问平台系统配置', 'module_config', true, 'system', NULL, now(), false), -- Bidding 管理后台功能权限 -('PERM-0611', 'perm_bidding_admin', '招标管理后台', 'bidding:admin:view', '访问招标管理后台', 'module_bidding', - true, 'system', NULL, now(), false), - +('PERM-0611', 'perm_bidding_admin', '招标管理后台', 'bidding:admin:view', '访问招标管理后台', 'module_bidding', true, 'system', NULL, now(), false), -- Workcase 管理后台功能权限 -('PERM-0621', 'perm_workcase_admin', '客服管理后台', 'workcase:admin:view', '访问客服管理后台', 'module_workcase', - true, 'system', NULL, now(), false), -('PERM-0622', 'perm_workcase_overview', '数据概览', 'workcase:overview:view', '访问泰豪小电数据概览', 'module_workcase', - true, 'system', NULL, now(), false), -('PERM-0623', 'perm_workcase_knowledge', '知识库管理', 'workcase:knowledge:view', '访问知识库管理', 'module_workcase', - true, 'system', NULL, now(), false), -('PERM-0624', 'perm_workcase_tickets', '工单管理', 'workcase:tickets:view', '访问工单管理', 'module_workcase', - true, 'system', NULL, now(), false), -('PERM-0625', 'perm_workcase_conversation', '对话数据', 'workcase:conversation:view', '访问对话数据管理', 'module_workcase', - true, 'system', NULL, now(), false), -('PERM-0626', 'perm_workcase_agent', '智能体管理', 'workcase:agent:view', '访问智能体管理', 'module_workcase', - true, 'system', NULL, now(), false), -('PERM-0627', 'perm_workcase_log', '日志管理', 'workcase:log:view', '访问日志管理', 'module_workcase', - true, 'system', NULL, now(), false); - +('PERM-0621', 'perm_workcase_admin', '客服管理后台', 'workcase:admin:view', '访问客服管理后台', 'module_workcase', true, 'system', NULL, now(), false), +('PERM-0622', 'perm_workcase_overview', '数据概览', 'workcase:overview:view', '访问泰豪小电数据概览', 'module_workcase', true, 'system', NULL, now(), false), +('PERM-0623', 'perm_workcase_knowledge', '知识库管理', 'workcase:knowledge:view', '访问知识库管理', 'module_workcase', true, 'system', NULL, now(), false), +('PERM-0624', 'perm_workcase_tickets', '工单管理', 'workcase:tickets:view', '访问工单管理', 'module_workcase', true, 'system', NULL, now(), false), +('PERM-0625', 'perm_workcase_conversation', '对话数据', 'workcase:conversation:view', '访问对话数据管理', 'module_workcase', true, 'system', NULL, now(), false), +('PERM-0626', 'perm_workcase_agent', '智能体管理', 'workcase:agent:view', '访问智能体管理', 'module_workcase', true, 'system', NULL, now(), false), +('PERM-0627', 'perm_workcase_log', '日志管理', 'workcase:log:view', '访问日志管理', 'module_workcase', true, 'system', NULL, now(), false); -- ============================= -- 5. 初始化视图(菜单) -- ============================= diff --git a/urbanLifelineServ/.gitignore b/urbanLifelineServ/.gitignore index 0f5aef70..1bfb4993 100644 --- a/urbanLifelineServ/.gitignore +++ b/urbanLifelineServ/.gitignore @@ -24,4 +24,5 @@ hs_err_pid* .data .idea */target -*/*/target \ No newline at end of file +*/*/target +# example/* \ No newline at end of file diff --git a/urbanLifelineServ/.windsurf/rules/urbanlife.md b/urbanLifelineServ/.windsurf/rules/urbanlife.md index 3c0148e1..83137f98 100644 --- a/urbanLifelineServ/.windsurf/rules/urbanlife.md +++ b/urbanLifelineServ/.windsurf/rules/urbanlife.md @@ -3,4 +3,5 @@ trigger: manual --- 1. 有BaseDTO基类,DTO\VO在api模块下面 -2. 用Dubbo注册和,引用服务 \ No newline at end of file +2. 用Dubbo注册和,引用服务 +3. 当个端Insert语句,对应sql表的必填项才生成(有默认值也是if生成),其他应该if条件生成 diff --git a/urbanLifelineServ/ai/AI模块功能说明.md b/urbanLifelineServ/ai/AI模块功能说明.md new file mode 100644 index 00000000..48f91a9e --- /dev/null +++ b/urbanLifelineServ/ai/AI模块功能说明.md @@ -0,0 +1,51 @@ +# 智能体创建 + +说明: 本服务只对智能体进行对话的转发,不能对dify智能体进行修改 +1. 智能体创建,用户上传tb_agent需要的字段进行智能体创建 + +# 智能体更新 +1. 更新数据库和redis缓存 +2. 注意加锁,避免并发时,其他线程用了错误数据 + +# 智能体删除 +软删除本服务的智能体数据 + +# 智能体对话 +1. 校验智能体是否可以用 +2. 用户会先对1个智能体创建一个会话 +3. 用户先发起对话预处理请求,传入会话携带的对象数据到redis中,产生一个临时的消息id +4. 真正发起对话数据请求时,会传入3个必须参数,1智能体id,2会话id,3本次消息id +5. 优先从redis缓存中获取agentid对应的agent配置,没有找数据库。这里要进行双检加锁,避免智能体修改、重复加载等问题 +6. 从redis中获取消息id真正的数据,包含各种数据对象和fileid等等内容 +7. 和dify构建sse流式对话,并回应前端的sse流式对话。 不要乱生成无用的data事件,直接返回dify的事件和数据 +8. 前端会自动处理dify的事件 + +# 对话评价 +1. 用户对对话进行评价,评价会存储到tb_chat_message的comment字段中 +2. 调用dify代理服务,更新dify智能体本消息的评价 + +# 知识库创建 +1. 用户上传tb_knowledge需要的字段进行知识库创建 + +# 知识库文件上传 +1. 用户上传文件到本服务,存储到文件表和minio中,并生成version版本 +2. 上传文件到dify知识库,dify返回上传成功后插入tb_knowledge_file表,并更新version版本,否则提示用户 + +# 知识库更新文件 +1. 用户上传文件到本服务,并生成version版本,存储到文件表和minio中 +2. 上传文件到dify知识库,dify返回上传成功后插入tb_knowledge_file表,并更新version版本,否则提示用户 +3. 删除dify旧的version的文件document,来控制版本 +4. 本服务中,旧的文件对象不会删除,本条新的file对象会有file_root_id指向原始第一个file对象的file_id,用来进行版本展示,但dify知识库实际只有1个最新的文件 + +# 知识库删除文件 +1. 软删除本服务服务中tb_knowledge_file的该文件所有版本 +2. 真删除dify本文件 + + +# 知识库创建、更新、删除文件分段 +1. 直接调用dify代理服务实现,不存储到本服务,也不生成新的version版本 + +# 知识库删除 +1. 软删除本服务服务中tb_knowledge的该知识库 +2. 真删除dify知识库 + diff --git a/urbanLifelineServ/ai/pom.xml b/urbanLifelineServ/ai/pom.xml index 7f1f1db1..370b25af 100644 --- a/urbanLifelineServ/ai/pom.xml +++ b/urbanLifelineServ/ai/pom.xml @@ -28,7 +28,51 @@ api-system ${urban-lifeline.version} - + + org.xyzh.apis + api-file + ${urban-lifeline.version} + + + org.xyzh.common + common-redis + + + org.xyzh.common + common-auth + + + org.xyzh.common + common-utils + + + org.xyzh.common + common-exception + + + org.springframework.boot + spring-boot-starter-web + + + org.apache.dubbo + dubbo-spring-boot-starter + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + + + com.baomidou + mybatis-plus-boot-starter + + + com.squareup.okhttp3 + okhttp + + + com.squareup.okhttp3 + okhttp-sse + \ No newline at end of file diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java index ee62b8e3..c0cb735f 100644 --- a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java @@ -13,7 +13,6 @@ import org.xyzh.ai.client.dto.*; import org.xyzh.ai.client.callback.StreamCallback; import org.xyzh.ai.config.DifyConfig; import org.xyzh.ai.exception.DifyException; -import org.xyzh.api.ai.dto.DifyFileInfo; import java.io.BufferedReader; import java.io.File; @@ -483,11 +482,11 @@ public class DifyApiClient { } if (!data.isEmpty()) { - // 使用Fastjson2解析SSE数据 + // 解析SSE数据 JSONObject jsonNode = JSON.parseObject(data); String event = jsonNode.containsKey("event") ? jsonNode.getString("event") : ""; - // 转发所有事件到回调(包含完整数据) + // 转发事件给回调(用于前端SSE转发) callback.onEvent(event, data); switch (event) { diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/DifyFileInfo.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/DifyFileInfo.java new file mode 100644 index 00000000..956232c2 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/DifyFileInfo.java @@ -0,0 +1,97 @@ +package org.xyzh.ai.client.dto; + +import com.alibaba.fastjson2.annotation.JSONField; + +import lombok.Data; + +/** + * Dify文件上传响应DTO + * 对应 Dify API /files/upload 的响应结构 + */ +@Data +public class DifyFileInfo { + + /** + * 文件ID(Dify返回) + */ + private String id; + + /** + * 文件名 + */ + private String name; + + /** + * 文件大小(字节) + */ + private Integer size; + + /** + * 文件扩展名 + */ + private String extension; + + /** + * 文件MIME类型 + */ + @JSONField(name="mime_type") + private String mimeType; + + /** + * 上传人ID + */ + @JSONField(name="created_by") + private String createdBy; + + /** + * 上传时间(时间戳) + */ + @JSONField(name="created_at") + private Long createdAt; + + /** + * 预览URL + */ + @JSONField(name="preview_url") + private String previewUrl; + + /** + * 源文件URL + */ + @JSONField(name="source_url") + private String sourceUrl; + + /** + * 文件类型:image、document、audio、video、file + */ + private String type; + + /** + * 传输方式:remote_url、local_file + */ + @JSONField(name="transfer_method") + private String transferMethod; + + /** + * 文件URL或ID + */ + private String url; + + /** + * 本地文件上传ID + */ + @JSONField(name="upload_file_id") + private String uploadFileId; + + /** + * 系统文件ID + */ + @JSONField(name="sys_file_id") + private String sysFileId; + + /** + * 文件路径(从系统文件表获取) + */ + @JSONField(name="file_path") + private String filePath; +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/AgentController.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/AgentController.java new file mode 100644 index 00000000..31230884 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/AgentController.java @@ -0,0 +1,147 @@ +package org.xyzh.ai.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.xyzh.ai.service.impl.AgentServiceImpl; +import org.xyzh.api.ai.dto.TbAgent; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageRequest; +import org.xyzh.common.utils.validation.ValidationResult; +import org.xyzh.common.utils.validation.ValidationParam; +import org.xyzh.common.utils.validation.ValidationUtils; + +import jakarta.validation.constraints.NotNull; + +import java.util.Arrays; + +/** + * @description 智能体控制器 + * @filename AgentController.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@Slf4j +@Validated +@RestController +@RequestMapping("/ai/agent") +public class AgentController { + + @Autowired + private AgentServiceImpl agentService; + + /** + * @description 创建智能体 + * @param agent + * @author yslg + * @since 2025-12-17 + */ + @PostMapping + @PreAuthorize("@ss.hasPermi('ai:agent:create')") + public ResultDomain createAgent(@RequestBody TbAgent agent) { + log.info("创建智能体: name={}", agent.getName()); + // 参数校验 + ValidationResult result = ValidationUtils.validate(agent, Arrays.asList( + ValidationUtils.requiredString("name", "智能体名称", 1, 100), + ValidationUtils.requiredString("apiKey", "API密钥", 1, 100), + ValidationUtils.requiredString("link", "智能体url",10,500) + )); + if (!result.isValid()) { + return ResultDomain.failure(result.getAllErrors()); + } + return agentService.addAgent(agent); + } + + /** + * @description 更新智能体 + * @param agent + * @author yslg + * @since 2025-12-17 + */ + @PutMapping + @PreAuthorize("@ss.hasPermi('ai:agent:update')") + public ResultDomain updateAgent(@RequestBody TbAgent agent) { + log.info("更新智能体: agentId={}", agent.getAgentId()); + // 参数校验 + ValidationResult result = ValidationUtils.validate(agent, Arrays.asList( + ValidationUtils.requiredString("agentId", "智能体ID"), + ValidationUtils.requiredString("name", "智能体名称", 1, 100), + ValidationUtils.requiredString("apiKey", "API密钥", 1, 100), + ValidationUtils.requiredString("link", "智能体url",10,500) + )); + if (!result.isValid()) { + return ResultDomain.failure(result.getAllErrors()); + } + return agentService.updateAgent(agent); + } + + /** + * @description 删除智能体 + * @param agentId + * @author yslg + * @since 2025-12-17 + */ + @DeleteMapping("/{agentId}") + @PreAuthorize("@ss.hasPermi('ai:agent:delete')") + public ResultDomain deleteAgent(@PathVariable("agentId") @NotNull String agentId) { + log.info("删除智能体: agentId={}", agentId); + TbAgent agent = new TbAgent(); + agent.setAgentId(agentId); + return agentService.deleteAgent(agent); + } + + /** + * @description 获取智能体详情 + * @param agentId + * @author yslg + * @since 2025-12-17 + */ + @GetMapping("/{agentId}") + @PreAuthorize("@ss.hasPermi('ai:agent:view')") + public ResultDomain getAgent(@PathVariable("agentId") @NotNull String agentId) { + log.info("获取智能体: agentId={}", agentId); + ResultDomain agentResult = agentService.selectAgentById(agentId); + if (agentResult.getSuccess() && agentResult.getData() != null) { + return ResultDomain.success("查询成功", agentResult.getData()); + } + return ResultDomain.failure("智能体不存在"+agentResult.getMessage()); + } + + /** + * @description 分页查询智能体 + * @param pageRequest + * @author yslg + * @since 2025-12-17 + */ + @PostMapping("/page") + @PreAuthorize("@ss.hasPermi('ai:agent:view')") + public ResultDomain getAgentPage(@RequestBody PageRequest pageRequest) { + log.info("分页查询智能体"); + // 参数校验(支持嵌套属性路径) + ValidationResult result = ValidationUtils.validate(pageRequest, Arrays.asList( + ValidationParam.builder().fieldName("pageParam").fieldLabel("分页参数").required().build(), + ValidationUtils.requiredNumber("pageParam.page", "页码", 1, null), + ValidationUtils.requiredNumber("pageParam.pageSize", "每页条数", 1, 100) + )); + if (!result.isValid()) { + return ResultDomain.failure(result.getAllErrors()); + } + return agentService.getAgentPage(pageRequest); + } + + /** + * @description 获取智能体列表 + * @param tbAgent + * @author yslg + * @since 2025-12-17 + */ + @GetMapping("/list") + @PreAuthorize("@ss.hasPermi('ai:agent:view')") + public ResultDomain getAgentList(TbAgent tbAgent) { + log.info("获取智能体列表"); + return agentService.getAgentList(tbAgent); + } +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/ChatController.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/ChatController.java new file mode 100644 index 00000000..0e028785 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/ChatController.java @@ -0,0 +1,305 @@ +package org.xyzh.ai.controller; + +import jakarta.validation.constraints.NotNull; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import org.xyzh.api.ai.dto.ChatPrepareData; +import org.xyzh.api.ai.dto.TbChat; +import org.xyzh.api.ai.dto.TbChatMessage; +import org.xyzh.api.ai.service.AIFileUploadService; +import org.xyzh.api.ai.service.AgentChatService; +import org.xyzh.common.auth.utils.LoginUtil; +import org.xyzh.common.core.domain.LoginDomain; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.utils.NonUtils; +import org.xyzh.common.utils.validation.ValidationParam; +import org.xyzh.common.utils.validation.ValidationResult; +import org.xyzh.common.utils.validation.ValidationUtils; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Map; + +/** + * @description 智能体对话控制器 所有接口开放 + * @filename ChatController.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@Slf4j +@RestController +@Validated +@RequestMapping("/ai/chat") +public class ChatController { + + @Autowired + private AgentChatService chatService; + + @Autowired + private AIFileUploadService fileUploadService; + + // ====================== 会话管理 ====================== + + /** + * @description 创建会话 + * @param + * @author yslg + * @since 2025-12-17 + */ + @PostMapping("/conversation") + public ResultDomain createChat(@RequestBody TbChat chat, @RequestHeader("Authorization") String token) { + ValidationResult result = ValidationUtils.validate(chat, Arrays.asList( + ValidationUtils.requiredString("agentId", "智能体id", 1, 100), + ValidationUtils.requiredString("title", "对话标题", 1, 100), + ValidationUtils.requiredString("userId", "用户id", 1, 100) + )); + if (!result.isValid()) { + return ResultDomain.failure(result.getAllErrors()); + } + // 默认来客,如果token不为空且token对应的用户存在,说明是员工 + chat.setUserType(false); + if(NonUtils.isNotEmpty(token)){ + LoginDomain loginDomain = LoginUtil.getCurrentLogin(); + if (NonUtils.isNotEmpty(loginDomain)) { + chat.setUserType(true); + } + } + + log.info("创建会话: agentId={}, title={}, userId={}, userType={}", chat.getAgentId(), chat.getTitle(), chat.getUserId()); + return chatService.createChat(chat); + } + + /** + * @description 更新对话 + * @param + * @author yslg + * @since 2025-12-17 + */ + @PutMapping("/conversation") + public ResultDomain updateChat(@RequestBody TbChat chat, @RequestHeader("Authorization") String token) { + ValidationResult result = ValidationUtils.validate(chat, Arrays.asList( + ValidationUtils.requiredString("agentId", "智能体id", 1, 100), + ValidationUtils.requiredString("title", "对话标题", 1, 100), + ValidationUtils.requiredString("userId", "用户id", 1, 100), + ValidationParam.builder().fieldName("userType").fieldLabel("用户类型").required().fieldType(Boolean.class).build() + )); + if (!result.isValid()) { + return ResultDomain.failure(result.getAllErrors()); + } + chat.setUserType(false); + if(NonUtils.isNotEmpty(token)){ + LoginDomain loginDomain = LoginUtil.getCurrentLogin(); + if (NonUtils.isNotEmpty(loginDomain)) { + chat.setUserType(true); + } + } + log.info("更新会话: chatId={}", chat.getChatId()); + return chatService.updateChat(chat); + } + + /** + * @description 删除对话 + * @param + * @author yslg + * @since 2025-12-17 + */ + @DeleteMapping("/conversation") + public ResultDomain deleteChat(@RequestBody TbChat chat, @RequestHeader("Authorization") String token) { + chat.setUserType(false); + if(NonUtils.isNotEmpty(token)){ + LoginDomain loginDomain = LoginUtil.getCurrentLogin(); + if (NonUtils.isNotEmpty(loginDomain)) { + chat.setUserType(true); + } + } + log.info("删除会话: chatId={}", chat.getChatId()); + return chatService.deleteChat(chat); + } + + /** + * @description 获取对话列表 + * @param + * @author yslg + * @since 2025-12-17 + */ + @GetMapping("/conversations") + public ResultDomain getChatList(@RequestBody TbChat filter, @RequestHeader("Authorization") String token) { + log.info("获取会话列表: agentId={}", filter.getAgentId()); + + filter.setUserType(false); + if(NonUtils.isNotEmpty(token)){ + LoginDomain loginDomain = LoginUtil.getCurrentLogin(); + if (NonUtils.isNotEmpty(loginDomain)) { + filter.setUserType(true); + } + } + return chatService.getChatList(filter); + } + + // ====================== 消息管理 ====================== + + /** + * @description 获取对话消息列表 + * @param + * @author yslg + * @since 2025-12-17 + */ + @PostMapping("/messages") + public ResultDomain getMessageList(@RequestBody TbChat filter, @RequestHeader("Authorization") String token) { + ValidationResult result = ValidationUtils.validate(filter, Arrays.asList( + ValidationUtils.requiredString("agentId", "智能体id",10,50), + ValidationUtils.requiredString("chatId", "对话Id", 10, 50), + ValidationUtils.requiredString("userId", "用户Id") + )); + + filter.setUserType(false); + if(NonUtils.isNotEmpty(token)){ + LoginDomain loginDomain = LoginUtil.getCurrentLogin(); + if (NonUtils.isNotEmpty(loginDomain)) { + filter.setUserType(true); + } + } + log.info("获取消息列表: chatId={}", filter.getChatId()); + return chatService.getChatMessageList(filter); + } + + // ====================== 流式对话 ====================== + + /** + * @description 准备流式对话会话数据 + * @param chatPrepareData + * @author yslg + * @since 2025-12-17 + */ + @PostMapping("/stream/prepare") + public ResultDomain prepareStreamChat(@RequestBody ChatPrepareData chatPrepareData, @RequestHeader("Authorization") String token) { + ValidationResult result = ValidationUtils.validate(chatPrepareData, Arrays.asList( + ValidationUtils.requiredString("agentId", "智能体Id", 1, 50), + ValidationUtils.requiredString("chatId", "会话Id", 1, 50), + ValidationUtils.requiredString("query", "用户问题"), + ValidationUtils.requiredString("userId", "用户Id", 1, 100) + )); + if(!result.isValid()){ + return ResultDomain.failure(result.getAllErrors()); + } + // 设置用户类型 + chatPrepareData.setUserType(false); + if(NonUtils.isNotEmpty(token)){ + LoginDomain loginDomain = LoginUtil.getCurrentLogin(); + if (NonUtils.isNotEmpty(loginDomain)) { + chatPrepareData.setUserType(true); + } + } + log.info("准备流式对话: agentId={}, chatId={}, query={}", chatPrepareData.getAgentId(), chatPrepareData.getChatId(), chatPrepareData.getQuery()); + return chatService.prepareChatMessageSession(chatPrepareData); + } + + /** + * @description 进行流式对话 + * @param + * @author yslg + * @since 2025-12-17 + */ + @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter streamChat(@RequestParam("sessionId") String sessionId) { + if(NonUtils.isEmpty(sessionId)){ + SseEmitter emitter = new SseEmitter(300000L); + try { + emitter.send(SseEmitter.event().name("error").data("{\"message\":\"会话不存在\"}")); + } catch (IOException e) { + log.error("发送错误事件失败", e); + }finally { + emitter.complete(); + } + return emitter; + } + log.info("建立SSE连接: sessionId={}", sessionId); + return chatService.streamChatMessageWithSse(sessionId); + } + + /** + * @description 停止会话 + * @param + * @author yslg + * @since 2025-12-17 + */ + @PostMapping("/stop") + public ResultDomain stopChat(@RequestBody Map params, @RequestHeader("Authorization") String token) { + ValidationResult result = ValidationUtils.validateMap(params, Arrays.asList( + ValidationUtils.requiredString("taskId", "任务ID"), + ValidationUtils.requiredString("agentId", "智能体ID"), + ValidationUtils.requiredString("userId", "用户ID") + )); + if (!result.isValid()) { + return ResultDomain.failure(result.getAllErrors()); + } + TbChat filter = new TbChat(); + filter.setAgentId(params.get("agentId")); + filter.setUserId(params.get("userId")); + filter.setUserType(false); + if(NonUtils.isNotEmpty(token)){ + LoginDomain loginDomain = LoginUtil.getCurrentLogin(); + if (NonUtils.isNotEmpty(loginDomain)) { + filter.setUserType(true); + } + } + log.info("停止对话: taskId={}", params.get("taskId")); + return chatService.stopChatMessageByTaskId(filter, params.get("taskId")); + } + + /** + * @description 评价消息 + * @param + * @author yslg + * @since 2025-12-17 + */ + @PostMapping("/comment") + public ResultDomain commentMessage(@RequestBody Map params, @RequestHeader("Authorization") String token) { + ValidationResult result = ValidationUtils.validateMap(params, Arrays.asList( + ValidationUtils.requiredString("agentId", "智能体ID"), + ValidationUtils.requiredString("chatId", "对话ID"), + ValidationUtils.requiredString("messageId", "消息ID"), + ValidationUtils.requiredString("comment", "评价"), + ValidationUtils.requiredString("userId", "用户ID") + )); + if (!result.isValid()) { + return ResultDomain.failure(result.getAllErrors()); + } + TbChat filter = new TbChat(); + filter.setAgentId(params.get("agentId")); + filter.setChatId(params.get("chatId")); + filter.setUserId(params.get("userId")); + filter.setUserType(false); + if(NonUtils.isNotEmpty(token)){ + LoginDomain loginDomain = LoginUtil.getCurrentLogin(); + if (NonUtils.isNotEmpty(loginDomain)) { + filter.setUserType(true); + } + } + log.info("评价消息: messageId={}, comment={}", params.get("messageId"), params.get("comment")); + return chatService.commentChatMessage(filter, params.get("messageId"), params.get("comment")); + } + + // ====================== 文件上传 ====================== + + /** + * @description 上传文件用于对话(图文多模态) + * @param file 文件 + * @param agentId 智能体ID + * @author yslg + * @since 2025-12-17 + */ + @PostMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResultDomain> uploadFileForChat( + @RequestPart("file") @NotNull MultipartFile file, + @RequestPart("agentId") @NotNull String agentId) { + log.info("上传对话文件: agentId={}, fileName={}", agentId, file.getOriginalFilename()); + return fileUploadService.uploadFileForChat(file, agentId); + } +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/KnowledgeController.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/KnowledgeController.java new file mode 100644 index 00000000..e2639211 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/controller/KnowledgeController.java @@ -0,0 +1,350 @@ +package org.xyzh.ai.controller; + +import com.alibaba.fastjson2.JSONObject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.xyzh.ai.service.impl.DifyProxyServiceImpl; +import org.xyzh.ai.service.impl.KnowledgeServiceImpl; +import org.xyzh.api.ai.dto.TbKnowledge; +import org.xyzh.api.ai.dto.TbKnowledgeFile; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageRequest; +import org.xyzh.common.utils.validation.ValidationResult; +import org.xyzh.common.utils.validation.ValidationUtils; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +/** + * @description 知识库控制器 + * @filename KnowledgeController.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@RestController +@Validated +@RequestMapping("/ai/knowledge") +public class KnowledgeController { + private static final Logger logger = LoggerFactory.getLogger(KnowledgeController.class); + + @Autowired + private KnowledgeServiceImpl knowledgeService; + + @Autowired + private DifyProxyServiceImpl difyProxyService; + + // ====================== 知识库管理 ====================== + + /** + * @description 创建知识库基础信息,包含dify知识库各种参数的配置 + * @param knowledge + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:knowledge:create')") + @PostMapping + public ResultDomain createKnowledge(@RequestBody TbKnowledge knowledge) { + ValidationResult result = ValidationUtils.validate(knowledge, Arrays.asList( + ValidationUtils.requiredString("title", "知识库标题", 1, 50), + ValidationUtils.inSet("difyIndexingTechnique", "Dify索引方式(high_quality/economy)" , true, new HashSet<>(Arrays.asList("high_quality", "economy"))), + ValidationUtils.inSet("category", "所属分类 workcase 内部知识库、外部知识库", true, new HashSet<>(Arrays.asList("default", "内部知识库", "外部知识库"))) + )); + if(!result.isValid()){ + return ResultDomain.failure(result.getAllErrors()); + } + logger.info("创建知识库: title={}", knowledge.getTitle()); + return knowledgeService.createKnowledge(knowledge, "PUBLIC", null, null); + } + + /** + * @description 更新知识库,包含dify知识库各种参数的配置 + * @param knowledge + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:knowledge:update')") + @PutMapping + public ResultDomain updateKnowledge(@RequestBody @Valid TbKnowledge knowledge) { + ValidationResult result = ValidationUtils.validate(knowledge, Arrays.asList( + ValidationUtils.requiredString("title", "知识库标题", 1, 50), + ValidationUtils.inSet("difyIndexingTechnique", "Dify索引方式(high_quality/economy)" , true, new HashSet<>(Arrays.asList("high_quality", "economy"))), + ValidationUtils.inSet("category", "所属分类 workcase 内部知识库、外部知识库", true, new HashSet<>(Arrays.asList("default", "内部知识库", "外部知识库"))) + + )); + if(!result.isValid()){ + return ResultDomain.failure(result.getAllErrors()); + } + logger.info("更新知识库: knowledgeId={}", knowledge.getKnowledgeId()); + return knowledgeService.updateKnowledge(knowledge); + } + + /** + * @description 删除知识库,同时删除dify知识库 + * @param knowledgeId + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:knowledge:delete')") + @DeleteMapping("/{knowledgeId}") + public ResultDomain deleteKnowledge(@PathVariable("knowledgeId") @NotBlank String knowledgeId) { + logger.info("删除知识库: knowledgeId={}", knowledgeId); + return knowledgeService.deleteKnowledge(knowledgeId); + } + + /** + * @description 获取知识库详情,包含dify知识库各种参数的配置 + * @param knowledgeId + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:knowledge:view')") + @GetMapping("/{knowledgeId}") + public ResultDomain getKnowledge(@PathVariable("knowledgeId") @NotBlank String knowledgeId) { + logger.info("获取知识库: knowledgeId={}", knowledgeId); + return knowledgeService.getKnowledgeById(knowledgeId); + } + + /** + * @description 查询知识库列表 + * @param filter + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:knowledge:view')") + @PostMapping("/list") + public ResultDomain listKnowledges(@RequestBody(required = false) TbKnowledge filter) { + logger.info("查询知识库列表"); + return knowledgeService.listKnowledges(filter); + } + + /** + * @description 分页查询知识库 + * @param pageRequest + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:knowledge:view')") + @PostMapping("/page") + public ResultDomain pageKnowledges(@RequestBody @Valid PageRequest pageRequest) { + logger.info("分页查询知识库"); + return knowledgeService.pageKnowledges(pageRequest.getFilter(), pageRequest.getPageParam()); + } + + /** + * @description 获取知识库统计信息 + * @param knowledgeId + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:knowledge:view')") + @GetMapping("/{knowledgeId}/stats") + public ResultDomain getKnowledgeStats(@PathVariable("knowledgeId") @NotBlank String knowledgeId) { + logger.info("获取知识库统计: knowledgeId={}", knowledgeId); + return knowledgeService.getKnowledgeStats(knowledgeId); + } + + // ====================== 文件管理 ====================== + + /** + * @description 获取知识库文档列表 + * @param knowledgeId 知识库id + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:knowledge:file:view')") + @GetMapping("/{knowledgeId}/documents") + public ResultDomain> getDocumentList( + @PathVariable("knowledgeId") @NotBlank String knowledgeId, + @RequestParam(value = "page", defaultValue = "1") Integer page, + @RequestParam(value = "limit", defaultValue = "20") Integer limit) { + logger.info("获取文档列表: knowledgeId={}", knowledgeId); + return knowledgeService.getDocumentList(knowledgeId, page, limit); + } + + /** + * @description 上传文件到知识库(同步到Dify) + * @param file 上传文件 + * @param knowledgeId 知识库id + * @param indexingTechnique 索引方式 + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:knowledge:file:upload')") + @PostMapping(name = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResultDomain uploadToKnowledge( + @RequestParam("file") @NotNull MultipartFile file, + @RequestParam("knowledgeId") @NotBlank String knowledgeId, + @RequestParam(value = "indexingTechnique", required = false) String indexingTechnique) { + logger.info("上传知识库文件: knowledgeId={}, fileName={}", knowledgeId, file.getOriginalFilename()); + return knowledgeService.uploadKnowledgeFile(knowledgeId, file, indexingTechnique); + } + + /** + * @description 批量上传文件到知识库 + * @param files 文件数组 + * @param knowledgeId 知识库id + * @param indexingTechnique 索引方式 + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:knowledge:file:upload')") + @PostMapping(name = "/file/batch-upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResultDomain batchUploadToKnowledge( + @RequestParam("files") @NotEmpty List files, + @RequestParam("knowledgeId") @NotBlank String knowledgeId, + @RequestParam(value = "indexingTechnique", required = false) String indexingTechnique) { + logger.info("批量上传知识库文件: knowledgeId={}, fileCount={}", knowledgeId, files.size()); + return knowledgeService.batchUploadKnowledgeFile(knowledgeId, files, indexingTechnique); + } + + /** + * 上传新文件来更新知识库文件,注意fileRootId一致,生成新id + * @param file 上传文件 + * @param knowledgeId 知识库id + * @param fileRootId 文件Rootid,多version下一致 + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:knowledge:file:update')") + @PutMapping(value = "/file/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ResultDomain updateFile( + @RequestParam("file") @NotNull MultipartFile file, + @RequestParam("knowledgeId") @NotBlank String knowledgeId, + @RequestParam("fileRootId") @NotBlank String fileRootId) { + logger.info("更新知识库文件: knowledgeId={}, fileName={}", knowledgeId, file.getOriginalFilename()); + return knowledgeService.updateKnowledgeFileVersion(knowledgeId, file, fileRootId); + } + + /** + * 删除知识库文件(同时删除Dify文档),所有filtRootId的文件一起软删除 + * @param fileId 文件id + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:knowledge:file:delete')") + @DeleteMapping("/file/{fileId}") + public ResultDomain deleteFile(@PathVariable("fileId") @NotBlank String fileId) { + logger.info("删除知识库文件: fileId={}", fileId); + return knowledgeService.deleteKnowledgeFileById(fileId); + } + + /** + * 获取文件历史版本,获取fileRootId下所有version + * @param fileRootId 文件id + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:knowledge:file:view')") + @GetMapping("/file/{fileId}/history") + public ResultDomain getFileHistory(@PathVariable("fileId") @NotBlank String fileRootId) { + logger.info("获取文件历史: fileRootId={}", fileRootId); + return knowledgeService.getKnowledgeFileHistory(fileRootId); + } + + // ====================== 文档分段管理 ====================== + + /** + * @description 获取文档分段列表 + * @param datasetId Dify数据集ID + * @param documentId Dify文档ID + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:dify:segment:view')") + @GetMapping("/datasets/{datasetId}/documents/{documentId}/segments") + public ResultDomain getDocumentSegments( + @PathVariable("datasetId") @NotBlank String datasetId, + @PathVariable("documentId") @NotBlank String documentId) { + logger.info("获取文档分段: datasetId={}, documentId={}", datasetId, documentId); + return difyProxyService.getDocumentSegments(datasetId, documentId); + } + + /** + * @description 创建文档分段 + * @param datasetId Dify数据集ID + * @param documentId Dify文档ID + * @param requestBody 分段内容 + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:dify:segment:create')") + @PostMapping("/datasets/{datasetId}/documents/{documentId}/segments") + public ResultDomain createSegment( + @PathVariable("datasetId") @NotBlank String datasetId, + @PathVariable("documentId") @NotBlank String documentId, + @RequestBody Map requestBody) { + logger.info("创建文档分段: datasetId={}, documentId={}", datasetId, documentId); + return difyProxyService.createSegment(datasetId, documentId, requestBody); + } + + /** + * @description 更新文档分段 + * @param datasetId Dify数据集ID + * @param documentId Dify文档ID + * @param segmentId Dify分段ID + * @param requestBody 分段内容 + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:dify:segment:update')") + @PatchMapping("/datasets/{datasetId}/documents/{documentId}/segments/{segmentId}") + public ResultDomain updateSegment( + @PathVariable("datasetId") @NotBlank String datasetId, + @PathVariable("documentId") @NotBlank String documentId, + @PathVariable("segmentId") @NotBlank String segmentId, + @RequestBody Map requestBody) { + logger.info("更新文档分段: datasetId={}, documentId={}, segmentId={}", datasetId, documentId, segmentId); + return difyProxyService.updateSegment(datasetId, documentId, segmentId, requestBody); + } + + /** + * @description 删除文档分段 + * @param datasetId Dify数据集ID + * @param documentId Dify文档ID + * @param segmentId Dify分段ID + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:dify:segment:delete')") + @DeleteMapping("/datasets/{datasetId}/documents/{documentId}/segments/{segmentId}") + public ResultDomain deleteSegment( + @PathVariable("datasetId") @NotBlank String datasetId, + @PathVariable("documentId") @NotBlank String documentId, + @PathVariable("segmentId") @NotBlank String segmentId) { + logger.info("删除文档分段: datasetId={}, documentId={}, segmentId={}", datasetId, documentId, segmentId); + return difyProxyService.deleteSegment(datasetId, documentId, segmentId); + } + + // ====================== 文档状态管理 ====================== + + /** + * @description 更新文档状态(启用/禁用/归档) + * @param datasetId Dify数据集ID + * @param action 操作类型: enable/disable/archive/un_archive + * @param requestBody 请求体 + * @author yslg + * @since 2025-12-18 + */ + @PreAuthorize("@ss.hasPermission('ai:dify:document:status')") + @PatchMapping("/datasets/{datasetId}/documents/{action}/status") + public ResultDomain updateDocumentStatus( + @PathVariable("datasetId") @NotBlank String datasetId, + @PathVariable("action") @NotBlank String action, + @RequestBody Map requestBody) { + logger.info("更新文档状态: datasetId={}, action={}", datasetId, action); + return difyProxyService.updateDocumentStatus(datasetId, action, requestBody); + } +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/handler/PromptCardsTypeHandler.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/handler/PromptCardsTypeHandler.java new file mode 100644 index 00000000..0eb09ce4 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/handler/PromptCardsTypeHandler.java @@ -0,0 +1,64 @@ +package org.xyzh.ai.handler; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedTypes; +import org.xyzh.api.ai.dto.PromptCard; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * @description 提示卡片列表类型处理器(JSONB <-> List) + * @filename PromptCardsTypeHandler.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@MappedTypes({List.class}) +public class PromptCardsTypeHandler extends BaseTypeHandler> { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, List parameter, JdbcType jdbcType) throws SQLException { + if (parameter == null || parameter.isEmpty()) { + ps.setString(i, "[]"); + } else { + ps.setString(i, JSON.toJSONString(parameter)); + } + } + + @Override + public List getNullableResult(ResultSet rs, String columnName) throws SQLException { + String jsonString = rs.getString(columnName); + return parseToList(jsonString); + } + + @Override + public List getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String jsonString = rs.getString(columnIndex); + return parseToList(jsonString); + } + + @Override + public List getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String jsonString = cs.getString(columnIndex); + return parseToList(jsonString); + } + + private List parseToList(String jsonString) { + if (jsonString == null || jsonString.trim().isEmpty()) { + return new ArrayList<>(); + } + try { + return JSON.parseObject(jsonString, new TypeReference>() {}); + } catch (Exception e) { + return new ArrayList<>(); + } + } +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbAgentMapper.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbAgentMapper.java new file mode 100644 index 00000000..982b6973 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbAgentMapper.java @@ -0,0 +1,70 @@ +package org.xyzh.ai.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.xyzh.api.ai.dto.TbAgent; +import org.xyzh.common.core.page.PageParam; + +import java.util.List; + +/** + * @description 智能体数据访问层 + * @filename AgentMapper.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@Mapper +public interface TbAgentMapper { + + /** + * 插入智能体 + */ + int insertAgent(TbAgent agent); + + /** + * 更新智能体(只更新非null字段) + */ + int updateAgent(TbAgent agent); + + /** + * 逻辑删除智能体 + */ + int deleteAgent(TbAgent agent); + + /** + * 根据ID查询智能体 + */ + TbAgent selectAgentById(@Param("agentId") String agentId); + + /** + * 根据ApiKey查询智能体 + */ + TbAgent selectAgentByApiKey(@Param("apiKey") String apiKey); + + /** + * 查询智能体列表 + */ + List selectAgentList(@Param("filter") TbAgent filter); + + /** + * 分页查询智能体 + */ + List selectAgentPage( + @Param("filter") TbAgent filter, + @Param("pageParam") PageParam pageParam + ); + + /** + * 统计智能体数量 + */ + long countAgents(@Param("filter") TbAgent filter); + + /** + * 根据名称检查是否存在 + */ + int countByName( + @Param("name") String name, + @Param("excludeId") String excludeId + ); +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbChatMapper.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbChatMapper.java new file mode 100644 index 00000000..ce62d4e2 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbChatMapper.java @@ -0,0 +1,60 @@ +package org.xyzh.ai.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.xyzh.api.ai.dto.TbChat; +import org.xyzh.common.core.page.PageParam; + +import java.util.List; + +/** + * @description 对话数据访问层 + * @filename ChatMapper.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@Mapper +public interface TbChatMapper { + + /** + * 插入对话 + */ + int insertChat(TbChat chat); + + /** + * 更新对话 + */ + int updateChat(TbChat chat); + + /** + * 逻辑删除对话 + */ + int deleteChat(TbChat chat); + + /** + * 根据ID查询对话 + */ + TbChat selectChatById(@Param("chatId") String chatId); + + /** + * 根据智能体ID和用户ID查询对话列表 + */ + List selectChatList( + @Param("agentId") String agentId, + @Param("userId") String userId + ); + + /** + * 分页查询对话 + */ + List selectChatPage( + @Param("filter") TbChat filter, + @Param("pageParam") PageParam pageParam + ); + + /** + * 统计对话数量 + */ + long countChats(@Param("filter") TbChat filter); +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbChatMessageMapper.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbChatMessageMapper.java new file mode 100644 index 00000000..63e77ecf --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbChatMessageMapper.java @@ -0,0 +1,62 @@ +package org.xyzh.ai.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.xyzh.api.ai.dto.TbChatMessage; +import org.xyzh.common.core.page.PageParam; + +import java.util.List; + +/** + * @description 对话消息数据访问层 + * @filename ChatMessageMapper.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@Mapper +public interface TbChatMessageMapper { + + /** + * 插入消息 + */ + int insertChatMessage(TbChatMessage message); + + /** + * 更新消息 + */ + int updateChatMessage(TbChatMessage message); + + /** + * 逻辑删除消息 + */ + int deleteChatMessage(TbChatMessage message); + + /** + * 根据ID查询消息 + */ + TbChatMessage selectMessageById(@Param("messageId") String messageId); + + /** + * 根据对话ID查询消息列表 + */ + List selectMessagesByChatId(@Param("chatId") String chatId); + + /** + * 分页查询消息 + */ + List selectMessagePage( + @Param("chatId") String chatId, + @Param("pageParam") PageParam pageParam + ); + + /** + * 统计消息数量 + */ + long countMessages(@Param("chatId") String chatId); + + /** + * 批量删除消息(根据对话ID) + */ + int deleteMessagesByChatId(@Param("chatId") String chatId); +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbKnowledgeFileMapper.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbKnowledgeFileMapper.java new file mode 100644 index 00000000..9b1090de --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbKnowledgeFileMapper.java @@ -0,0 +1,78 @@ +package org.xyzh.ai.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.xyzh.api.ai.dto.TbKnowledgeFile; +import org.xyzh.common.core.page.PageParam; + +import java.util.List; + +/** + * @description 知识库文件数据访问层 + * @filename KnowledgeFileMapper.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@Mapper +public interface TbKnowledgeFileMapper { + + /** + * 插入知识库文件 + */ + int insertKnowledgeFile(TbKnowledgeFile file); + + /** + * 更新知识库文件 + */ + int updateKnowledgeFile(TbKnowledgeFile file); + + /** + * 逻辑删除知识库文件 + */ + int deleteKnowledgeFile(TbKnowledgeFile file); + + /** + * 根据知识库ID和文件ID查询 + */ + TbKnowledgeFile selectKnowledgeFile( + @Param("knowledgeId") String knowledgeId, + @Param("fileId") String fileId + ); + + /** + * 根据知识库ID查询文件列表 + */ + List selectFilesByKnowledgeId(@Param("knowledgeId") String knowledgeId); + + /** + * 根据文件根ID查询所有版本 + */ + List selectFileVersions(@Param("fileRootId") String fileRootId); + + /** + * 分页查询知识库文件 + */ + List selectFilePage( + @Param("knowledgeId") String knowledgeId, + @Param("pageParam") PageParam pageParam + ); + + /** + * 统计知识库文件数量 + */ + long countFiles(@Param("knowledgeId") String knowledgeId); + + /** + * 批量删除知识库文件(根据文件根ID) + */ + int deleteFilesByRootId(@Param("fileRootId") String fileRootId); + + /** + * 获取文件最新版本号 + */ + Integer selectLatestVersion( + @Param("knowledgeId") String knowledgeId, + @Param("fileRootId") String fileRootId + ); +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbKnowledgeMapper.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbKnowledgeMapper.java new file mode 100644 index 00000000..01592252 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/mapper/TbKnowledgeMapper.java @@ -0,0 +1,62 @@ +package org.xyzh.ai.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.xyzh.api.ai.dto.TbKnowledge; +import org.xyzh.common.core.page.PageParam; + +import java.util.List; + +/** + * @description 知识库数据访问层 + * @filename KnowledgeMapper.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@Mapper +public interface TbKnowledgeMapper { + + /** + * 插入知识库 + */ + int insertKnowledge(TbKnowledge knowledge); + + /** + * 更新知识库 + */ + int updateKnowledge(TbKnowledge knowledge); + + /** + * 逻辑删除知识库 + */ + int deleteKnowledge(TbKnowledge knowledge); + + /** + * 根据ID查询知识库 + */ + TbKnowledge selectKnowledgeById(@Param("knowledgeId") String knowledgeId); + + /** + * 根据DifyDatasetId查询知识库 + */ + TbKnowledge selectKnowledgeByDifyId(@Param("difyDatasetId") String difyDatasetId); + + /** + * 查询知识库列表 + */ + List selectKnowledgeList(@Param("filter") TbKnowledge filter); + + /** + * 分页查询知识库 + */ + List selectKnowledgePage( + @Param("filter") TbKnowledge filter, + @Param("pageParam") PageParam pageParam + ); + + /** + * 统计知识库数量 + */ + long countKnowledges(@Param("filter") TbKnowledge filter); +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AIFileUploadServiceImpl.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AIFileUploadServiceImpl.java new file mode 100644 index 00000000..e2c26434 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AIFileUploadServiceImpl.java @@ -0,0 +1,207 @@ +package org.xyzh.ai.service.impl; + +import org.apache.dubbo.config.annotation.DubboService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; +import org.xyzh.ai.client.DifyApiClient; +import org.xyzh.ai.client.dto.DifyFileInfo; +import org.xyzh.ai.client.dto.DocumentUploadRequest; +import org.xyzh.ai.client.dto.DocumentUploadResponse; +import org.xyzh.api.ai.dto.TbAgent; +import org.xyzh.api.ai.service.AIFileUploadService; +import org.xyzh.common.auth.utils.LoginUtil; +import org.xyzh.common.core.domain.ResultDomain; + +import java.io.File; +import java.util.List; +import java.util.Map; + +/** + * @description AI文件上传服务实现(只负责与Dify交互,不处理minio和数据库) + * @filename AIFileUploadServiceImpl.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@DubboService(version = "1.0.0", group = "ai", timeout = 30000, retries = 0) +public class AIFileUploadServiceImpl implements AIFileUploadService { + private static final Logger logger = LoggerFactory.getLogger(AIFileUploadServiceImpl.class); + + @Autowired + private DifyApiClient difyApiClient; + + @Autowired + private AgentServiceImpl agentService; + + // ============================ 对话文件管理 ============================ + + @Override + public ResultDomain> uploadFileForChat(MultipartFile file, String agentId) { + // 1. 参数校验 + if (file == null || file.isEmpty()) { + return ResultDomain.failure("文件不能为空"); + } + if (!StringUtils.hasText(agentId)) { + return ResultDomain.failure("智能体ID不能为空"); + } + + // 2. 获取智能体API Key + ResultDomain agentResult = agentService.selectAgentById(agentId); + if (!agentResult.getSuccess() || agentResult.getData() == null) { + return ResultDomain.failure("智能体不存在"); + } + TbAgent agent = agentResult.getData(); + + File tempFile = null; + try { + // 3. 将MultipartFile转换为临时File + tempFile = File.createTempFile("upload_", "_" + file.getOriginalFilename()); + file.transferTo(tempFile); + + // 4. 获取当前用户 + String userId = LoginUtil.getCurrentUserId(); + if (!StringUtils.hasText(userId)) { + userId = "anonymous"; + } + + // 5. 上传到Dify + DifyFileInfo difyFile = difyApiClient.uploadFileForChat(tempFile, file.getOriginalFilename(), userId, agent.getApiKey()); + if (difyFile != null && StringUtils.hasText(difyFile.getId())) { + logger.info("上传对话文件成功: agentId={}, fileId={}", agentId, difyFile.getId()); + Map result = new java.util.HashMap<>(); + result.put("id", difyFile.getId()); + result.put("name", difyFile.getName()); + result.put("size", difyFile.getSize()); + result.put("type", difyFile.getType()); + result.put("upload_file_id", difyFile.getUploadFileId()); + return ResultDomain.success("上传成功", result); + } + return ResultDomain.failure("上传文件失败"); + } catch (Exception e) { + logger.error("上传对话文件异常: {}", e.getMessage(), e); + return ResultDomain.failure("上传文件异常: " + e.getMessage()); + } finally { + if (tempFile != null && tempFile.exists()) { + tempFile.delete(); + } + } + } + + // ============================ 知识库Dify文档管理 ============================ + + /** + * @description 上传文件到Dify知识库(只负责Dify上传,不处理minio和数据库) + * @param difyDatasetId Dify知识库ID + * @param file 文件 + * @param fileName 文件名 + * @param indexingTechnique 索引方式 + * @return ResultDomain Dify文档ID + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain uploadFileToDify(String difyDatasetId, File file, String fileName, String indexingTechnique) { + if (!StringUtils.hasText(difyDatasetId)) { + return ResultDomain.failure("Dify知识库ID不能为空"); + } + if (file == null || !file.exists()) { + return ResultDomain.failure("文件不能为空"); + } + + try { + DocumentUploadRequest uploadRequest = new DocumentUploadRequest(); + uploadRequest.setName(fileName); + if (StringUtils.hasText(indexingTechnique)) { + uploadRequest.setIndexingTechnique(indexingTechnique); + } + + DocumentUploadResponse uploadResponse = difyApiClient.uploadDocumentByFile( + difyDatasetId, + file, + fileName, + uploadRequest + ); + + if (uploadResponse != null && uploadResponse.getDocument() != null) { + String difyDocumentId = uploadResponse.getDocument().getId(); + logger.info("上传文件到Dify成功: difyDatasetId={}, difyDocumentId={}", difyDatasetId, difyDocumentId); + return ResultDomain.success("上传成功", difyDocumentId); + } + return ResultDomain.failure("上传文件到Dify失败"); + } catch (Exception e) { + logger.error("上传文件到Dify异常: {}", e.getMessage(), e); + return ResultDomain.failure("上传文件到Dify异常: " + e.getMessage()); + } + } + + /** + * @description 从Dify知识库删除文档 + * @param difyDatasetId Dify知识库ID + * @param difyDocumentId Dify文档ID + * @return ResultDomain 删除结果 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain deleteFileFromDify(String difyDatasetId, String difyDocumentId) { + if (!StringUtils.hasText(difyDatasetId)) { + return ResultDomain.failure("Dify知识库ID不能为空"); + } + if (!StringUtils.hasText(difyDocumentId)) { + return ResultDomain.failure("Dify文档ID不能为空"); + } + + try { + difyApiClient.deleteDocument(difyDatasetId, difyDocumentId); + logger.info("从Dify删除文档成功: difyDatasetId={}, difyDocumentId={}", difyDatasetId, difyDocumentId); + return ResultDomain.success("删除成功", true); + } catch (Exception e) { + logger.error("从Dify删除文档异常: {}", e.getMessage(), e); + return ResultDomain.failure("从Dify删除文档异常: " + e.getMessage()); + } + } + + /** + * @description 批量从Dify知识库删除文档 + * @param difyDatasetId Dify知识库ID + * @param difyDocumentIds Dify文档ID列表 + * @return ResultDomain 删除结果 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain batchDeleteFilesFromDify(String difyDatasetId, List difyDocumentIds) { + if (!StringUtils.hasText(difyDatasetId)) { + return ResultDomain.failure("Dify知识库ID不能为空"); + } + if (difyDocumentIds == null || difyDocumentIds.isEmpty()) { + return ResultDomain.failure("Dify文档ID列表不能为空"); + } + + int successCount = 0; + int failCount = 0; + for (String difyDocumentId : difyDocumentIds) { + if (StringUtils.hasText(difyDocumentId)) { + try { + difyApiClient.deleteDocument(difyDatasetId, difyDocumentId); + successCount++; + } catch (Exception e) { + logger.warn("批量删除Dify文档失败: difyDocumentId={}, error={}", difyDocumentId, e.getMessage()); + failCount++; + } + } + } + + logger.info("批量从Dify删除文档完成: 成功={}, 失败={}", successCount, failCount); + if (failCount == 0) { + return ResultDomain.success("批量删除成功", true); + } else if (successCount == 0) { + return ResultDomain.failure("批量删除全部失败"); + } else { + return ResultDomain.success("部分删除成功", true); + } + } +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java new file mode 100644 index 00000000..1f5eb180 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentChatServiceImpl.java @@ -0,0 +1,476 @@ +package org.xyzh.ai.service.impl; + +import com.alibaba.fastjson2.JSONObject; + +import org.apache.dubbo.config.annotation.DubboReference; +import org.apache.dubbo.config.annotation.DubboService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import org.xyzh.ai.client.DifyApiClient; +import org.xyzh.ai.client.callback.StreamCallback; +import org.xyzh.ai.client.dto.ChatRequest; +import org.xyzh.ai.mapper.TbChatMapper; +import org.xyzh.ai.mapper.TbChatMessageMapper; +import org.xyzh.api.ai.dto.ChatPrepareData; +import org.xyzh.api.ai.dto.DifyFileInfo; +import org.xyzh.api.ai.dto.TbAgent; +import org.xyzh.api.ai.dto.TbChat; +import org.xyzh.api.ai.dto.TbChatMessage; +import org.xyzh.api.ai.service.AgentChatService; +import org.xyzh.api.ai.service.AgentService; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.redis.service.RedisService; +import org.xyzh.common.utils.id.IdUtil; +import org.xyzh.common.auth.utils.LoginUtil; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * @description 智能体对话服务实现 + * @filename AgentChatServiceImpl.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@DubboService( + version = "1.0.0", + group = "ai", + timeout = 3000, + retries = 0 +) +public class AgentChatServiceImpl implements AgentChatService { + private static final Logger logger = LoggerFactory.getLogger(AgentChatServiceImpl.class); + + private static final String CHAT_SESSION_PREFIX = "ai:chat:session:"; + private static final long SESSION_TTL = 5 * 60; + + @Autowired + private TbChatMapper chatMapper; + + @Autowired + private TbChatMessageMapper chatMessageMapper; + + @DubboReference + private AgentService agentService; + + @Autowired + private DifyApiClient difyApiClient; + + @Autowired + private RedisService redisService; + + /** + * @description 根据 userType 获取用户ID + * @param chat 会话信息(包含 userId 和 userType) + * @return 真实的系统用户ID + */ + private String getUserIdByType(TbChat chat) { + if (!chat.getUserType()) { + // 来客(userType=false):直接返回传入的 userId(已经是真正的系统 userId) + return chat.getUserId(); + } else { + // 员工(userType=true):从登录信息获取 userId + return LoginUtil.getCurrentUserId(); + } + } + + /** + * @description 判断智能体是否是outer + * @param agentId 智能体ID + * @return true-是outer,false-不是outer + */ + private Boolean isOuterAgent(String agentId){ + // 智能体必须是outer + ResultDomain agentResult = agentService.selectAgentById(agentId); + if(!agentResult.getSuccess()|| agentResult.getData() == null || !agentResult.getData().getOuter()){ + return false; + } + return true; + } + + // ====================== 智能体会话管理 ====================== + + @Override + @Transactional(rollbackFor = Exception.class) + public ResultDomain createChat(TbChat chat) { + // 如果是来客(userType=false),校验智能体是否是 outer + if(!chat.getUserType()){ + if(!isOuterAgent(chat.getAgentId())){ + return ResultDomain.failure("智能体不可用"); + } + } + // 设置chat(来客传入的 userId 已经是真正的系统 userId) + chat.setOptsn(IdUtil.getOptsn()); + chat.setChatId(IdUtil.generateID()); + chatMapper.insertChat(chat); + return ResultDomain.success("创建成功", chat); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ResultDomain updateChat(TbChat chat) { + // 1. 校验会话 + TbChat chat2 = chatMapper.selectChatById(chat.getChatId()); + if (chat2 == null) { + return ResultDomain.failure("会话不存在"); + } + // 判断agent是否是outer + if(!isOuterAgent(chat.getAgentId())){ + return ResultDomain.failure("智能体不可用"); + } + + // 2. 获取用户ID并校验权限 + String userId = getUserIdByType(chat); + if (userId == null) { + return ResultDomain.failure("用户信息获取失败"); + } + if (!chat2.getUserId().equals(userId)) { + return ResultDomain.failure("无权修改此会话"); + } + + // 3. 更新会话 + TbChat update = new TbChat(); + update.setChatId(chat.getChatId()); + update.setTitle(chat.getTitle()); + + int rows = chatMapper.updateChat(update); + if (rows > 0) { + return ResultDomain.success("更新会话成功", update); + } + + return ResultDomain.failure("更新会话失败"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ResultDomain deleteChat(TbChat filter) { + // 1. 校验会话 + TbChat chat = chatMapper.selectChatById(filter.getChatId()); + if (chat == null) { + return ResultDomain.failure("会话不存在"); + } + // 判断agent是否是outer + if(!isOuterAgent(chat.getAgentId())){ + return ResultDomain.failure("智能体不可用"); + } + // 2. 获取用户ID并校验权限 + String userId = getUserIdByType(filter); + if (userId == null) { + return ResultDomain.failure("用户信息获取失败"); + } + if (!chat.getUserId().equals(userId)) { + return ResultDomain.failure("无权删除此会话"); + } + + // 3. 删除会话消息 + chatMessageMapper.deleteMessagesByChatId(filter.getChatId()); + + // 4. 删除会话 + TbChat delete = new TbChat(); + delete.setChatId(filter.getChatId()); + int rows = chatMapper.deleteChat(delete); + if (rows > 0) { + logger.info("删除会话成功: chatId={}", filter.getChatId()); + return ResultDomain.success("删除会话成功", chat); + } + + return ResultDomain.failure("删除会话失败"); + } + + @Override + public ResultDomain getChatList(TbChat filter) { + // 判断agent是否是outer + if(!isOuterAgent(filter.getAgentId())){ + return ResultDomain.failure("智能体不可用"); + } + // 获取用户ID + String userId = getUserIdByType(filter); + if (userId == null) { + return ResultDomain.failure("用户信息获取失败"); + } + + List chatList = chatMapper.selectChatList(filter.getAgentId(), userId); + return ResultDomain.success("查询成功", chatList); + } + + // ====================== 智能体聊天管理 ====================== + + @Override + public ResultDomain getChatMessageList(TbChat filter) { + // 1. 校验会话 + TbChat chat = chatMapper.selectChatById(filter.getChatId()); + if (chat == null) { + return ResultDomain.failure("会话不存在"); + } + // 判断agent是否是outer + if(!isOuterAgent(chat.getAgentId())){ + return ResultDomain.failure("智能体不可用"); + } + // 2. 获取用户ID并校验权限 + String userId = getUserIdByType(filter); + if (userId == null) { + return ResultDomain.failure("用户信息获取失败"); + } + if (!chat.getUserId().equals(userId)) { + return ResultDomain.failure("无权查看此会话"); + } + + List messages = chatMessageMapper.selectMessagesByChatId(filter.getChatId()); + return ResultDomain.success("查询成功", messages); + } + + @Override + public ResultDomain prepareChatMessageSession(ChatPrepareData prepareData) { + String agentId = prepareData.getAgentId(); + String chatId = prepareData.getChatId(); + String query = prepareData.getQuery(); + + // 1. 校验智能体 + ResultDomain agentResult = agentService.selectAgentById(agentId); + if (!agentResult.getSuccess() || agentResult.getData() == null || !agentResult.getData().getOuter()) { + return ResultDomain.failure("智能体不存在或不可用"); + } + TbAgent agent = agentResult.getData(); + + // 2. 获取用户ID(根据userType处理来客/员工) + TbChat chatFilter = new TbChat(); + chatFilter.setAgentId(agentId); + chatFilter.setUserId(prepareData.getUserId()); + chatFilter.setUserType(prepareData.getUserType()); + + String userId = getUserIdByType(chatFilter); + if (userId == null) { + return ResultDomain.failure("用户信息获取失败"); + } + + // 3. 校验会话 + if (StringUtils.hasText(chatId)) { + TbChat chat = chatMapper.selectChatById(chatId); + if (chat == null) { + return ResultDomain.failure("会话不存在"); + } + if (!chat.getUserId().equals(userId)) { + return ResultDomain.failure("无权操作此会话"); + } + } + + // 4. 生成临时消息ID(sessionId) + String sessionId = IdUtil.getSnowflakeId(); + + // 5. 存储会话数据到Redis + Map sessionData = new HashMap<>(); + sessionData.put("agentId", agentId); + sessionData.put("chatId", chatId); + sessionData.put("query", query); + sessionData.put("userId", userId); + sessionData.put("filesData", prepareData.getFiles()); + sessionData.put("apiKey", agent.getApiKey()); + + String cacheKey = CHAT_SESSION_PREFIX + sessionId; + redisService.set(cacheKey, sessionData, SESSION_TTL, TimeUnit.SECONDS); + + logger.info("准备对话会话: sessionId={}, agentId={}", sessionId, agentId); + return ResultDomain.success("准备成功", sessionId); + } + + @Override + public SseEmitter streamChatMessageWithSse(String sessionId) { + SseEmitter emitter = new SseEmitter(300000L); + + // 1. 从Redis获取会话数据 + String cacheKey = CHAT_SESSION_PREFIX + sessionId; + @SuppressWarnings("unchecked") + Map sessionData = (Map) redisService.get(cacheKey); + + if (sessionData == null) { + try { + emitter.send(SseEmitter.event().name("error").data("{\"message\":\"会话已过期\"}")); + emitter.complete(); + } catch (IOException e) { + logger.error("发送错误事件失败", e); + } + return emitter; + } + + // 2. 解析会话数据 + String agentId = (String) sessionData.get("agentId"); + String chatId = (String) sessionData.get("chatId"); + String query = (String) sessionData.get("query"); + String userId = (String) sessionData.get("userId"); + String apiKey = (String) sessionData.get("apiKey"); + @SuppressWarnings("unchecked") + List filesData = (List) sessionData.get("filesData"); + + // 3. 删除已使用的会话数据 + redisService.delete(cacheKey); + + // 4. 保存用户消息 + String userMessageId = IdUtil.getSnowflakeId(); + TbChatMessage userMessage = new TbChatMessage(); + userMessage.setOptsn(IdUtil.getOptsn()); + userMessage.setMessageId(userMessageId); + userMessage.setChatId(chatId); + userMessage.setRole("user"); + userMessage.setContent(query); + chatMessageMapper.insertChatMessage(userMessage); + + // 5. 构建Dify请求 + ChatRequest chatRequest = new ChatRequest(); + chatRequest.setQuery(query); + chatRequest.setUser(userId); + chatRequest.setResponseMode("streaming"); + + if (filesData != null && !filesData.isEmpty()) { + chatRequest.setFiles(filesData); + } + + // 6. 准备AI消息记录 + String aiMessageId = IdUtil.getSnowflakeId(); + StringBuilder aiContent = new StringBuilder(); + + // 7. 发起流式请求 + difyApiClient.streamChat(chatRequest, apiKey, new StreamCallback() { + @Override + public void onEvent(String event, String data) { + try { + // 使用SseEmitter标准格式发送:event: xxx\ndata: xxx + emitter.send(SseEmitter.event().name(event).data(data)); + } catch (IOException e) { + logger.error("发送SSE事件失败: event={}", event, e); + } + } + + @Override + public void onMessage(String content) { + aiContent.append(content); + } + + @Override + public void onMessageEnd(String data) { + // 从message_end事件中提取difyMessageId + String difyMessageId = null; + try { + JSONObject json = JSONObject.parseObject(data); + if (json != null && json.containsKey("message_id")) { + difyMessageId = json.getString("message_id"); + } + } catch (Exception e) { + logger.warn("解析difyMessageId失败: {}", e.getMessage()); + } + + // 保存AI回复消息 + TbChatMessage aiMessage = new TbChatMessage(); + aiMessage.setOptsn(IdUtil.getOptsn()); + aiMessage.setMessageId(aiMessageId); + aiMessage.setDifyMessageId(difyMessageId); + aiMessage.setChatId(chatId); + aiMessage.setRole("ai"); + aiMessage.setContent(aiContent.toString()); + chatMessageMapper.insertChatMessage(aiMessage); + + logger.info("对话完成: chatId={}, aiMessageId={}, difyMessageId={}", chatId, aiMessageId, difyMessageId); + } + + @Override + public void onComplete() { + try { + emitter.complete(); + } catch (Exception e) { + logger.error("完成SSE失败", e); + } + } + + @Override + public void onError(Throwable throwable) { + logger.error("流式对话异常", throwable); + try { + JSONObject errorData = new JSONObject(); + errorData.put("message", throwable.getMessage()); + emitter.send(SseEmitter.event().name("error").data(errorData.toJSONString())); + emitter.complete(); + } catch (IOException e) { + logger.error("发送错误事件失败", e); + } + } + }); + + return emitter; + } + + @Override + public ResultDomain stopChatMessageByTaskId(TbChat filter, String taskId) { + // 1. 获取智能体 + ResultDomain agentResult = agentService.selectAgentById(filter.getAgentId()); + if (!agentResult.getSuccess() || agentResult.getData() == null || !agentResult.getData().getOuter()) { + return ResultDomain.failure("智能体不存在"); + } + TbAgent agent = agentResult.getData(); + + // 2. 获取用户ID + String userId = getUserIdByType(filter); + if (userId == null) { + return ResultDomain.failure("用户信息获取失败"); + } + + try { + // 3. 调用Dify停止接口 + difyApiClient.stopChatMessage(taskId, userId, agent.getApiKey()); + logger.info("停止对话成功: taskId={}", taskId); + return ResultDomain.success("停止成功", true); + } catch (Exception e) { + logger.error("停止对话失败: taskId={}", taskId, e); + return ResultDomain.failure("停止对话失败: " + e.getMessage()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ResultDomain commentChatMessage(TbChat filter, String messageId, String comment) { + // 1. 校验消息 + TbChatMessage message = chatMessageMapper.selectMessageById(messageId); + if (message == null) { + return ResultDomain.failure("消息不存在"); + } + + // 2. 获取用户ID + String userId = getUserIdByType(filter); + if (userId == null) { + return ResultDomain.failure("用户信息获取失败"); + } + + // 3. 更新评价 + TbChatMessage update = new TbChatMessage(); + update.setMessageId(messageId); + update.setComment(comment); + + int rows = chatMessageMapper.updateChatMessage(update); + if (rows > 0) { + // 4. 同步到Dify(转换评价格式,使用difyMessageId) + if (StringUtils.hasText(message.getDifyMessageId())) { + ResultDomain agentResult = agentService.selectAgentById(filter.getAgentId()); + if (agentResult.getSuccess() && agentResult.getData() != null) { + TbAgent agent = agentResult.getData(); + String rating = "like".equals(comment) ? "like" : ("dislike".equals(comment) ? "dislike" : null); + try { + difyApiClient.submitMessageFeedback(message.getDifyMessageId(), rating, userId, comment, agent.getApiKey()); + } catch (Exception e) { + logger.warn("同步评价到Dify失败: difyMessageId={}", message.getDifyMessageId(), e); + } + } + } + + logger.info("评价消息成功: messageId={}, comment={}", messageId, comment); + return ResultDomain.success("评价成功", true); + } + + return ResultDomain.failure("评价失败"); + } +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentServiceImpl.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentServiceImpl.java new file mode 100644 index 00000000..889494a3 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/AgentServiceImpl.java @@ -0,0 +1,253 @@ +package org.xyzh.ai.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.dubbo.config.annotation.DubboService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import org.xyzh.ai.mapper.TbAgentMapper; +import org.xyzh.api.ai.dto.TbAgent; +import org.xyzh.api.ai.service.AgentService; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageDomain; +import org.xyzh.common.core.page.PageParam; +import org.xyzh.common.core.page.PageRequest; +import org.xyzh.common.redis.service.RedisService; +import org.xyzh.common.utils.id.IdUtil; +import org.xyzh.common.auth.utils.LoginUtil; + +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @description 智能体服务实现 + * @filename AgentServiceImpl.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@DubboService(version = "1.0.0", group = "ai", timeout = 3000, retries = 0) +public class AgentServiceImpl implements AgentService { + private static final Logger logger = LoggerFactory.getLogger(AgentServiceImpl.class); + + private static final String AGENT_CACHE_PREFIX = "ai:agent:"; + private static final long AGENT_CACHE_TTL = 24 * 60 * 60; + + private final ReentrantLock agentLock = new ReentrantLock(); + + @Autowired + private TbAgentMapper agentMapper; + + @Autowired + private RedisService redisService; + + @Override + @Transactional(rollbackFor = Exception.class) + public ResultDomain addAgent(TbAgent tbAgent) { + // 1. 参数校验 + if (!StringUtils.hasText(tbAgent.getName())) { + return ResultDomain.failure("智能体名称不能为空"); + } + if (!StringUtils.hasText(tbAgent.getApiKey())) { + return ResultDomain.failure("智能体API Key不能为空"); + } + if (!StringUtils.hasText(tbAgent.getIntroduce())) { + return ResultDomain.failure("引导词不能为空"); + } + if (!StringUtils.hasText(tbAgent.getCategory())) { + return ResultDomain.failure("分类不能为空"); + } + + // 2. 检查名称是否重复 + int count = agentMapper.countByName(tbAgent.getName(), null); + if (count > 0) { + return ResultDomain.failure("智能体名称已存在"); + } + + // 3. 检查ApiKey是否重复 + TbAgent existAgent = agentMapper.selectAgentByApiKey(tbAgent.getApiKey()); + if (existAgent != null) { + return ResultDomain.failure("该API Key已被使用"); + } + + // 4. 生成ID和流水号 + tbAgent.setOptsn(IdUtil.getOptsn()); + tbAgent.setAgentId(IdUtil.getSnowflakeId()); + + // 5. 设置创建者 + String userId = LoginUtil.getCurrentUserId(); + if (StringUtils.hasText(userId)) { + tbAgent.setCreator(userId); + } + + // 6. 插入数据库 + int rows = agentMapper.insertAgent(tbAgent); + if (rows > 0) { + logger.info("创建智能体成功: agentId={}, name={}", tbAgent.getAgentId(), tbAgent.getName()); + return ResultDomain.success("创建智能体成功", tbAgent); + } + + return ResultDomain.failure("创建智能体失败"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ResultDomain updateAgent(TbAgent tbAgent) { + // 1. 参数校验 + if (!StringUtils.hasText(tbAgent.getAgentId())) { + return ResultDomain.failure("智能体ID不能为空"); + } + + // 2. 检查是否存在 + TbAgent existAgent = agentMapper.selectAgentById(tbAgent.getAgentId()); + if (existAgent == null) { + return ResultDomain.failure("智能体不存在"); + } + + // 3. 检查名称是否重复(排除自身) + if (StringUtils.hasText(tbAgent.getName())) { + int count = agentMapper.countByName(tbAgent.getName(), tbAgent.getAgentId()); + if (count > 0) { + return ResultDomain.failure("智能体名称已存在"); + } + } + + // 4. 加锁更新(避免并发问题) + agentLock.lock(); + try { + // 5. 设置更新者 + String userId = LoginUtil.getCurrentUserId(); + if (StringUtils.hasText(userId)) { + tbAgent.setUpdater(userId); + } + + // 6. 更新数据库 + int rows = agentMapper.updateAgent(tbAgent); + if (rows > 0) { + // 7. 返回最新数据 + TbAgent updated = agentMapper.selectAgentById(tbAgent.getAgentId()); + // 8. 更新缓存 + String cacheKey = AGENT_CACHE_PREFIX + tbAgent.getAgentId(); + logger.info("更新智能体成功: agentId={}", tbAgent.getAgentId()); + redisService.set(cacheKey, updated); + return ResultDomain.success("更新智能体成功", updated); + } + } finally { + agentLock.unlock(); + } + + return ResultDomain.failure("更新智能体失败"); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ResultDomain deleteAgent(TbAgent tbAgent) { + // 1. 参数校验 + if (!StringUtils.hasText(tbAgent.getAgentId())) { + return ResultDomain.failure("智能体ID不能为空"); + } + + // 2. 检查是否存在 + TbAgent existAgent = agentMapper.selectAgentById(tbAgent.getAgentId()); + if (existAgent == null) { + return ResultDomain.failure("智能体不存在"); + } + + // 3. 设置更新者 + String userId = LoginUtil.getCurrentUserId(); + if (StringUtils.hasText(userId)) { + tbAgent.setUpdater(userId); + } + + // 4. 逻辑删除 + int rows = agentMapper.deleteAgent(tbAgent); + if (rows > 0) { + // 5. 删除缓存 + String cacheKey = AGENT_CACHE_PREFIX + tbAgent.getAgentId(); + redisService.delete(cacheKey); + + logger.info("删除智能体成功: agentId={}", tbAgent.getAgentId()); + return ResultDomain.success("删除智能体成功", existAgent); + } + + return ResultDomain.failure("删除智能体失败"); + } + + /** + * 根据ID获取智能体(优先从缓存获取,双检加锁) + */ + public ResultDomain selectAgentById(String agentId) { + if (!StringUtils.hasText(agentId)) { + return ResultDomain.failure("智能体ID不能为空"); + } + + String cacheKey = AGENT_CACHE_PREFIX + agentId; + + // 1. 先从缓存获取 + TbAgent agent = (TbAgent) redisService.get(cacheKey); + if (agent != null) { + return ResultDomain.success("查询成功", agent); + } + + // 2. 双检加锁 + agentLock.lock(); + try { + // 再次检查缓存 + agent = (TbAgent) redisService.get(cacheKey); + if (agent != null) { + return ResultDomain.success("查询成功", agent); + } + + // 3. 从数据库获取 + agent = agentMapper.selectAgentById(agentId); + if (agent != null) { + // 4. 写入缓存 + redisService.set(cacheKey, agent); + return ResultDomain.success("查询成功", agent); + } + + return ResultDomain.failure("智能体不存在"); + } finally { + agentLock.unlock(); + } + } + + /** + * 根据ApiKey获取智能体 + */ + public TbAgent getAgentByApiKey(String apiKey) { + if (!StringUtils.hasText(apiKey)) { + return null; + } + return agentMapper.selectAgentByApiKey(apiKey); + } + + @Override + public ResultDomain getAgentPage(PageRequest pageRequest) { + TbAgent filter = pageRequest.getFilter(); + PageParam pageParam = pageRequest.getPageParam(); + + // 查询总数 + long total = agentMapper.countAgents(filter); + pageParam.setTotal((int) total); + + // 查询分页数据 + List list = agentMapper.selectAgentPage(filter, pageParam); + + PageDomain pageDomain = new PageDomain<>(pageParam, list); + return ResultDomain.success("查询成功", pageDomain); + } + + @Override + public ResultDomain getAgentList(TbAgent filter) { + // 查询列表 + List list = agentMapper.selectAgentList(filter); + + ResultDomain result = ResultDomain.success("查询成功"); + result.setDataList(list); + return result; + } +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/DifyProxyServiceImpl.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/DifyProxyServiceImpl.java new file mode 100644 index 00000000..c5f1bd50 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/DifyProxyServiceImpl.java @@ -0,0 +1,165 @@ +package org.xyzh.ai.service.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import lombok.extern.slf4j.Slf4j; +import org.apache.dubbo.config.annotation.DubboService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.xyzh.ai.client.DifyApiClient; +import org.xyzh.api.ai.service.DifyProxyService; +import org.xyzh.common.core.domain.ResultDomain; + +import java.util.Map; + +/** + * @description Dify代理服务实现(直接转发请求到Dify API) + * @filename DifyProxyServiceImpl.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@DubboService(version="1.0.0", group="ai", timeout=3000, retries=0) +public class DifyProxyServiceImpl implements DifyProxyService { + private static final Logger logger = LoggerFactory.getLogger(DifyProxyServiceImpl.class); + + @Autowired + private DifyApiClient difyApiClient; + + @Override + public ResultDomain getDocumentSegments(String datasetId, String documentId) { + // 1. 参数校验 + if (!StringUtils.hasText(datasetId)) { + return ResultDomain.failure("数据集ID不能为空"); + } + if (!StringUtils.hasText(documentId)) { + return ResultDomain.failure("文档ID不能为空"); + } + + try { + // 2. 调用Dify API获取分段 + String url = String.format("/datasets/%s/documents/%s/segments", datasetId, documentId); + String response = difyApiClient.get(url, null); + + if (StringUtils.hasText(response)) { + JSONObject result = JSON.parseObject(response); + logger.info("获取文档分段成功: datasetId={}, documentId={}", datasetId, documentId); + return ResultDomain.success("查询成功", result); + } + + return ResultDomain.failure("获取文档分段失败"); + } catch (Exception e) { + logger.error("获取文档分段异常: datasetId={}, documentId={}, error={}", datasetId, documentId, e.getMessage(), e); + return ResultDomain.failure("获取文档分段异常: " + e.getMessage()); + } + } + + @Override + public ResultDomain updateSegment(String datasetId, String documentId, String segmentId, Map requestBody) { + // 1. 参数校验 + if (!StringUtils.hasText(datasetId)) { + return ResultDomain.failure("数据集ID不能为空"); + } + if (!StringUtils.hasText(documentId)) { + return ResultDomain.failure("文档ID不能为空"); + } + if (!StringUtils.hasText(segmentId)) { + return ResultDomain.failure("分段ID不能为空"); + } + + try { + // 2. 调用Dify API更新分段 + String url = String.format("/datasets/%s/documents/%s/segments/%s", datasetId, documentId, segmentId); + String response = difyApiClient.patch(url, requestBody, null); + + logger.info("更新分段成功: datasetId={}, documentId={}, segmentId={}", datasetId, documentId, segmentId); + return ResultDomain.success("更新成功", response); + } catch (Exception e) { + logger.error("更新分段异常: datasetId={}, documentId={}, segmentId={}, error={}", + datasetId, documentId, segmentId, e.getMessage(), e); + return ResultDomain.failure("更新分段异常: " + e.getMessage()); + } + } + + @Override + public ResultDomain createSegment(String datasetId, String documentId, Map requestBody) { + // 1. 参数校验 + if (!StringUtils.hasText(datasetId)) { + return ResultDomain.failure("数据集ID不能为空"); + } + if (!StringUtils.hasText(documentId)) { + return ResultDomain.failure("文档ID不能为空"); + } + + try { + // 2. 调用Dify API创建分段 + String url = String.format("/datasets/%s/documents/%s/segments", datasetId, documentId); + String response = difyApiClient.post(url, requestBody, null); + + logger.info("创建分段成功: datasetId={}, documentId={}", datasetId, documentId); + return ResultDomain.success("创建成功", response); + } catch (Exception e) { + logger.error("创建分段异常: datasetId={}, documentId={}, error={}", datasetId, documentId, e.getMessage(), e); + return ResultDomain.failure("创建分段异常: " + e.getMessage()); + } + } + + @Override + public ResultDomain deleteSegment(String datasetId, String documentId, String segmentId) { + // 1. 参数校验 + if (!StringUtils.hasText(datasetId)) { + return ResultDomain.failure("数据集ID不能为空"); + } + if (!StringUtils.hasText(documentId)) { + return ResultDomain.failure("文档ID不能为空"); + } + if (!StringUtils.hasText(segmentId)) { + return ResultDomain.failure("分段ID不能为空"); + } + + try { + // 2. 调用Dify API删除分段 + String url = String.format("/datasets/%s/documents/%s/segments/%s", datasetId, documentId, segmentId); + String response = difyApiClient.delete(url, null); + + logger.info("删除分段成功: datasetId={}, documentId={}, segmentId={}", datasetId, documentId, segmentId); + return ResultDomain.success("删除成功", response); + } catch (Exception e) { + logger.error("删除分段异常: datasetId={}, documentId={}, segmentId={}, error={}", + datasetId, documentId, segmentId, e.getMessage(), e); + return ResultDomain.failure("删除分段异常: " + e.getMessage()); + } + } + + @Override + public ResultDomain updateDocumentStatus(String datasetId, String action, Map requestBody) { + // 1. 参数校验 + if (!StringUtils.hasText(datasetId)) { + return ResultDomain.failure("数据集ID不能为空"); + } + if (!StringUtils.hasText(action)) { + return ResultDomain.failure("操作类型不能为空"); + } + + // 校验action合法性 + if (!action.equals("enable") && !action.equals("disable") && + !action.equals("archive") && !action.equals("un_archive")) { + return ResultDomain.failure("操作类型不合法,支持:enable/disable/archive/un_archive"); + } + + try { + // 2. 调用Dify API更新文档状态 + String url = String.format("/datasets/%s/documents/%s/status", datasetId, action); + String response = difyApiClient.patch(url, requestBody, null); + + logger.info("更新文档状态成功: datasetId={}, action={}", datasetId, action); + return ResultDomain.success("更新成功", response); + } catch (Exception e) { + logger.error("更新文档状态异常: datasetId={}, action={}, error={}", datasetId, action, e.getMessage(), e); + return ResultDomain.failure("更新文档状态异常: " + e.getMessage()); + } + } +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/KnowledgeServiceImpl.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/KnowledgeServiceImpl.java new file mode 100644 index 00000000..71aa6a5a --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/service/impl/KnowledgeServiceImpl.java @@ -0,0 +1,661 @@ +package org.xyzh.ai.service.impl; + +import org.apache.dubbo.config.annotation.DubboReference; +import org.apache.dubbo.config.annotation.DubboService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; +import org.xyzh.ai.client.DifyApiClient; +import org.xyzh.ai.client.dto.DatasetCreateRequest; +import org.xyzh.ai.client.dto.DatasetCreateResponse; +import org.xyzh.ai.client.dto.DatasetDetailResponse; +import org.xyzh.ai.client.dto.DocumentListResponse; +import org.xyzh.ai.mapper.TbKnowledgeFileMapper; +import org.xyzh.ai.mapper.TbKnowledgeMapper; +import org.xyzh.api.ai.dto.TbKnowledge; +import org.xyzh.api.ai.dto.TbKnowledgeFile; +import org.xyzh.api.ai.service.AIFileUploadService; +import org.xyzh.api.ai.service.KnowledgeService; +import org.xyzh.api.file.dto.TbSysFileDTO; +import org.xyzh.api.file.service.FileService; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageParam; +import org.xyzh.common.utils.id.IdUtil; +import org.xyzh.common.auth.utils.LoginUtil; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @description 知识库服务实现 + * @filename KnowledgeServiceImpl.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@DubboService(version="1.0.0", group="ai", timeout=3000, retries=0) +public class KnowledgeServiceImpl implements KnowledgeService { + private static final Logger logger = LoggerFactory.getLogger(KnowledgeServiceImpl.class); + + @Autowired + private TbKnowledgeMapper knowledgeMapper; + + @Autowired + private TbKnowledgeFileMapper knowledgeFileMapper; + + @Autowired + private DifyApiClient difyApiClient; + + @DubboReference(version = "1.0.0", group = "file", timeout = 30000) + private FileService fileService; + + @DubboReference(version = "1.0.0", group = "ai", timeout = 30000) + private AIFileUploadService aiFileUploadService; + + // ================================= 知识库管理 ================================= + + /** + * @description 创建知识库基础信息,包含dify知识库各种参数的配置 + * 注意:涉及外部API调用,不使用@Transactional,采用手动补偿机制 + * @param knowledge 知识库信息 + * @param permissionType 权限类型:PUBLIC-公开,DEPARTMENT-部门,ROLE-角色,PRIVATE-私有 + * @param deptIds 部门ID列表(DEPARTMENT类型需要) + * @param roleIds 角色ID列表(ROLE/PRIVATE类型需要) + * @return ResultDomain 创建结果 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain createKnowledge(TbKnowledge knowledge, String permissionType, List deptIds, List roleIds) { + // 1. 参数校验 + if (!StringUtils.hasText(knowledge.getTitle())) { + return ResultDomain.failure("知识库标题不能为空"); + } + + // 2. 获取当前用户 + String userId = LoginUtil.getCurrentUserId(); + if (!StringUtils.hasText(userId)) { + return ResultDomain.failure("用户未登录"); + } + + // 3. 生成ID + knowledge.setOptsn(IdUtil.getOptsn()); + knowledge.setKnowledgeId(IdUtil.getSnowflakeId()); + knowledge.setCreator(userId); + + // 4. 创建Dify知识库 + String difyDatasetId = null; + try { + DatasetCreateRequest createRequest = new DatasetCreateRequest(); + createRequest.setName(knowledge.getTitle()); + createRequest.setDescription(knowledge.getDescription()); + if (StringUtils.hasText(knowledge.getDifyIndexingTechnique())) { + createRequest.setIndexingTechnique(knowledge.getDifyIndexingTechnique()); + } + + DatasetCreateResponse difyResponse = difyApiClient.createDataset(createRequest); + if (difyResponse != null && StringUtils.hasText(difyResponse.getId())) { + difyDatasetId = difyResponse.getId(); + knowledge.setDifyDatasetId(difyDatasetId); + } else { + return ResultDomain.failure("创建Dify知识库失败"); + } + } catch (Exception e) { + logger.error("创建Dify知识库异常: {}", e.getMessage(), e); + return ResultDomain.failure("创建Dify知识库异常: " + e.getMessage()); + } + + // 5. 插入数据库 + int rows = knowledgeMapper.insertKnowledge(knowledge); + if (rows > 0) { + logger.info("创建知识库成功: knowledgeId={}, title={}", knowledge.getKnowledgeId(), knowledge.getTitle()); + return ResultDomain.success("创建知识库成功", knowledge); + } + + // 数据库保存失败,补偿删除Dify知识库 + if (StringUtils.hasText(difyDatasetId)) { + try { + difyApiClient.deleteDataset(difyDatasetId); + logger.info("补偿删除Dify知识库成功: difyDatasetId={}", difyDatasetId); + } catch (Exception e) { + logger.warn("补偿删除Dify知识库失败: {}", e.getMessage()); + } + } + return ResultDomain.failure("创建知识库失败"); + } + + /** + * @description 更新知识库,包含dify知识库各种参数的配置 + * @param knowledge 知识库信息 + * @return ResultDomain 更新结果 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain updateKnowledge(TbKnowledge knowledge) { + // 1. 参数校验 + if (!StringUtils.hasText(knowledge.getKnowledgeId())) { + return ResultDomain.failure("知识库ID不能为空"); + } + + // 2. 检查是否存在 + TbKnowledge existing = knowledgeMapper.selectKnowledgeById(knowledge.getKnowledgeId()); + if (existing == null) { + return ResultDomain.failure("知识库不存在"); + } + + // 3. 设置更新者 + String userId = LoginUtil.getCurrentUserId(); + if (StringUtils.hasText(userId)) { + knowledge.setUpdater(userId); + } + + // 4. 更新数据库 + int rows = knowledgeMapper.updateKnowledge(knowledge); + if (rows > 0) { + logger.info("更新知识库成功: knowledgeId={}", knowledge.getKnowledgeId()); + TbKnowledge updated = knowledgeMapper.selectKnowledgeById(knowledge.getKnowledgeId()); + return ResultDomain.success("更新知识库成功", updated); + } + + return ResultDomain.failure("更新知识库失败"); + } + + /** + * @description 删除知识库,同时删除dify知识库 + * 注意:Dify删除失败不影响本地删除 + * @param knowledgeId 知识库ID + * @return ResultDomain 删除结果 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain deleteKnowledge(String knowledgeId) { + // 1. 参数校验 + if (!StringUtils.hasText(knowledgeId)) { + return ResultDomain.failure("知识库ID不能为空"); + } + + // 2. 检查是否存在 + TbKnowledge existing = knowledgeMapper.selectKnowledgeById(knowledgeId); + if (existing == null) { + return ResultDomain.failure("知识库不存在"); + } + + // 3. 删除Dify知识库 + if (StringUtils.hasText(existing.getDifyDatasetId())) { + try { + difyApiClient.deleteDataset(existing.getDifyDatasetId()); + } catch (Exception e) { + logger.warn("删除Dify知识库失败: {}", e.getMessage()); + } + } + + // 4. 设置更新者并软删除 + TbKnowledge delete = new TbKnowledge(); + delete.setKnowledgeId(knowledgeId); + String userId = LoginUtil.getCurrentUserId(); + if (StringUtils.hasText(userId)) { + delete.setUpdater(userId); + } + + int rows = knowledgeMapper.deleteKnowledge(delete); + if (rows > 0) { + logger.info("删除知识库成功: knowledgeId={}", knowledgeId); + return ResultDomain.success("删除知识库成功", true); + } + + return ResultDomain.failure("删除知识库失败"); + } + + /** + * @description 获取知识库详情,包含dify知识库各种参数的配置 + * @param knowledgeId 知识库ID + * @return ResultDomain 知识库信息 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain getKnowledgeById(String knowledgeId) { + if (!StringUtils.hasText(knowledgeId)) { + return ResultDomain.failure("知识库ID不能为空"); + } + + TbKnowledge knowledge = knowledgeMapper.selectKnowledgeById(knowledgeId); + if (knowledge == null) { + return ResultDomain.failure("知识库不存在"); + } + + return ResultDomain.success("查询成功", knowledge); + } + + /** + * @description 查询知识库列表 + * @param filter 过滤条件 + * @return ResultDomain 知识库列表 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain listKnowledges(TbKnowledge filter) { + List list = knowledgeMapper.selectKnowledgeList(filter); + return ResultDomain.success("查询成功", list); + } + + /** + * @description 分页查询知识库 + * @param filter 过滤条件 + * @param pageParam 分页参数 + * @return ResultDomain 分页结果 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain pageKnowledges(TbKnowledge filter, PageParam pageParam) { + List list = knowledgeMapper.selectKnowledgePage(filter, pageParam); + long total = knowledgeMapper.countKnowledges(filter); + pageParam.setTotal((int) total); + return ResultDomain.success("查询成功", list); + } + + /** + * @description 获取知识库统计信息(从Dify同步文档数量和分段数量) + * @param knowledgeId 知识库ID + * @return ResultDomain 统计信息 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain getKnowledgeStats(String knowledgeId) { + TbKnowledge knowledge = knowledgeMapper.selectKnowledgeById(knowledgeId); + if (knowledge == null) { + return ResultDomain.failure("知识库不存在"); + } + + // 从Dify同步统计信息 + if (StringUtils.hasText(knowledge.getDifyDatasetId())) { + try { + DatasetDetailResponse detail = difyApiClient.getDatasetDetail(knowledge.getDifyDatasetId()); + if (detail != null) { + TbKnowledge update = new TbKnowledge(); + update.setKnowledgeId(knowledgeId); + update.setDocumentCount(detail.getDocumentCount()); + update.setTotalChunks(detail.getWordCount()); + knowledgeMapper.updateKnowledge(update); + + knowledge.setDocumentCount(detail.getDocumentCount()); + knowledge.setTotalChunks(detail.getWordCount()); + } + } catch (Exception e) { + logger.warn("同步Dify统计信息失败: {}", e.getMessage()); + } + } + + return ResultDomain.success("查询成功", knowledge); + } + + /** + * @description 获取可用的嵌入模型列表 + * @return ResultDomain> 嵌入模型列表 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain> getAvailableEmbeddingModels() { + try { + var response = difyApiClient.getAvailableEmbeddingModels(); + Map result = new HashMap<>(); + result.put("data", response.getData()); + return ResultDomain.success("查询成功", result); + } catch (Exception e) { + logger.error("获取嵌入模型列表失败: {}", e.getMessage(), e); + return ResultDomain.failure("获取嵌入模型列表失败: " + e.getMessage()); + } + } + + /** + * @description 获取可用的Rerank模型列表 + * @return ResultDomain> Rerank模型列表 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain> getAvailableRerankModels() { + try { + var response = difyApiClient.getAvailableRerankModels(); + Map result = new HashMap<>(); + result.put("data", response.getData()); + return ResultDomain.success("查询成功", result); + } catch (Exception e) { + logger.error("获取Rerank模型列表失败: {}", e.getMessage(), e); + return ResultDomain.failure("获取Rerank模型列表失败: " + e.getMessage()); + } + } + + /** + * @description 获取知识库文档列表(从Dify获取) + * @param knowledgeId 知识库ID + * @param page 页码(从1开始) + * @param limit 每页数量 + * @return ResultDomain> 文档列表 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain> getDocumentList(String knowledgeId, Integer page, Integer limit) { + TbKnowledge knowledge = knowledgeMapper.selectKnowledgeById(knowledgeId); + if (knowledge == null || !StringUtils.hasText(knowledge.getDifyDatasetId())) { + return ResultDomain.failure("知识库不存在或未关联Dify"); + } + + try { + DocumentListResponse response = difyApiClient.listDocuments(knowledge.getDifyDatasetId(), page, limit); + Map result = new HashMap<>(); + result.put("data", response.getData()); + result.put("total", response.getTotal()); + result.put("page", response.getPage()); + result.put("limit", response.getLimit()); + return ResultDomain.success("查询成功", result); + } catch (Exception e) { + logger.error("获取Dify文档列表失败: {}", e.getMessage(), e); + return ResultDomain.failure("获取文档列表失败: " + e.getMessage()); + } + } + + // ================================= 文件管理 ================================= + + /** + * @description 上传文件到知识库(完整流程:minio + Dify + 数据库) + * @param knowledgeId 知识库ID + * @param file 上传的文件 + * @param indexingTechnique 索引方式 + * @return ResultDomain 上传结果 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain uploadKnowledgeFile(String knowledgeId, MultipartFile file, String indexingTechnique) { + // 1. 参数校验 + if (!StringUtils.hasText(knowledgeId)) { + return ResultDomain.failure("知识库ID不能为空"); + } + if (file == null || file.isEmpty()) { + return ResultDomain.failure("文件不能为空"); + } + + // 2. 获取知识库 + TbKnowledge knowledge = knowledgeMapper.selectKnowledgeById(knowledgeId); + if (knowledge == null) { + return ResultDomain.failure("知识库不存在"); + } + if (!StringUtils.hasText(knowledge.getDifyDatasetId())) { + return ResultDomain.failure("知识库未关联Dify"); + } + + // 3. 调用FileService上传到minio + ResultDomain fileResult = fileService.uploadFile(file, "knowledge", knowledgeId); + if (!fileResult.getSuccess() || fileResult.getData() == null) { + return ResultDomain.failure("上传文件到存储服务失败: " + fileResult.getMessage()); + } + TbSysFileDTO sysFile = fileResult.getData(); + String fileId = sysFile.getFileId(); + logger.info("上传文件到minio成功: fileId={}, path={}", fileId, sysFile.getPath()); + + // 4. 上传到Dify知识库 + String difyFileId = null; + File tempFile = null; + try { + tempFile = File.createTempFile("knowledge_", "_" + file.getOriginalFilename()); + file.transferTo(tempFile); + + String technique = StringUtils.hasText(indexingTechnique) ? indexingTechnique : knowledge.getDifyIndexingTechnique(); + ResultDomain difyResult = aiFileUploadService.uploadFileToDify( + knowledge.getDifyDatasetId(), tempFile, file.getOriginalFilename(), technique); + + if (difyResult.getSuccess() && StringUtils.hasText(difyResult.getData())) { + difyFileId = difyResult.getData(); + logger.info("上传文件到Dify成功: difyFileId={}", difyFileId); + } else { + logger.error("上传文件到Dify失败: {}", difyResult.getMessage()); + fileService.deleteFile(fileId); + return ResultDomain.failure("上传文件到Dify失败: " + difyResult.getMessage()); + } + } catch (Exception e) { + logger.error("上传文件到Dify异常: {}", e.getMessage(), e); + fileService.deleteFile(fileId); + return ResultDomain.failure("上传文件到Dify异常: " + e.getMessage()); + } finally { + if (tempFile != null && tempFile.exists()) { + tempFile.delete(); + } + } + + // 5. 保存tb_knowledge_file + TbKnowledgeFile knowledgeFile = new TbKnowledgeFile(); + knowledgeFile.setOptsn(IdUtil.getOptsn()); + knowledgeFile.setKnowledgeId(knowledgeId); + knowledgeFile.setFileRootId(fileId); + knowledgeFile.setFileId(fileId); + knowledgeFile.setDifyFileId(difyFileId); + knowledgeFile.setVersion(1); + + int rows = knowledgeFileMapper.insertKnowledgeFile(knowledgeFile); + if (rows > 0) { + logger.info("保存知识库文件记录成功: knowledgeId={}, fileId={}, difyFileId={}", knowledgeId, fileId, difyFileId); + return ResultDomain.success("上传成功", knowledgeFile); + } + + // 补偿删除 + fileService.deleteFile(fileId); + aiFileUploadService.deleteFileFromDify(knowledge.getDifyDatasetId(), difyFileId); + return ResultDomain.failure("保存知识库文件记录失败"); + } + + /** + * @description 批量上传文件到知识库 + * @param knowledgeId 知识库ID + * @param files 文件列表 + * @param indexingTechnique 索引方式 + * @return ResultDomain 上传结果 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain batchUploadKnowledgeFile(String knowledgeId, List files, String indexingTechnique) { + if (!StringUtils.hasText(knowledgeId)) { + return ResultDomain.failure("知识库ID不能为空"); + } + if (files == null || files.isEmpty()) { + return ResultDomain.failure("文件列表不能为空"); + } + + List successList = new ArrayList<>(); + List failedFiles = new ArrayList<>(); + + for (MultipartFile file : files) { + ResultDomain result = uploadKnowledgeFile(knowledgeId, file, indexingTechnique); + if (result.getSuccess() && result.getData() != null) { + successList.add(result.getData()); + } else { + failedFiles.add(file.getOriginalFilename() + "(" + result.getMessage() + ")"); + } + } + + if (failedFiles.isEmpty()) { + return ResultDomain.success("批量上传成功", successList); + } else if (successList.isEmpty()) { + return ResultDomain.failure("批量上传全部失败: " + String.join(", ", failedFiles)); + } else { + return ResultDomain.success("部分上传成功,失败: " + String.join(", ", failedFiles), successList); + } + } + + /** + * @description 上传新版本文件(fileRootId一致,version递增) + * @param knowledgeId 知识库ID + * @param file 新文件 + * @param fileRootId 文件根ID + * @return ResultDomain 新版本文件信息 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain updateKnowledgeFileVersion(String knowledgeId, MultipartFile file, String fileRootId) { + // 1. 参数校验 + if (!StringUtils.hasText(knowledgeId)) { + return ResultDomain.failure("知识库ID不能为空"); + } + if (file == null || file.isEmpty()) { + return ResultDomain.failure("文件不能为空"); + } + if (!StringUtils.hasText(fileRootId)) { + return ResultDomain.failure("文件根ID不能为空"); + } + + // 2. 获取知识库 + TbKnowledge knowledge = knowledgeMapper.selectKnowledgeById(knowledgeId); + if (knowledge == null) { + return ResultDomain.failure("知识库不存在"); + } + if (!StringUtils.hasText(knowledge.getDifyDatasetId())) { + return ResultDomain.failure("知识库未关联Dify"); + } + + // 3. 获取旧版本 + List oldVersions = knowledgeFileMapper.selectFileVersions(fileRootId); + if (oldVersions == null || oldVersions.isEmpty()) { + return ResultDomain.failure("原文件不存在"); + } + + // 4. 上传新版本到minio + ResultDomain fileResult = fileService.uploadFileVersion(file, "knowledge", knowledgeId, fileRootId); + if (!fileResult.getSuccess() || fileResult.getData() == null) { + return ResultDomain.failure("上传新版本文件失败: " + fileResult.getMessage()); + } + TbSysFileDTO sysFile = fileResult.getData(); + String newFileId = sysFile.getFileId(); + int newVersion = sysFile.getVersion(); + logger.info("上传新版本到minio成功: fileId={}, version={}", newFileId, newVersion); + + // 5. 删除Dify旧文档 + for (TbKnowledgeFile oldFile : oldVersions) { + if (StringUtils.hasText(oldFile.getDifyFileId())) { + aiFileUploadService.deleteFileFromDify(knowledge.getDifyDatasetId(), oldFile.getDifyFileId()); + } + } + + // 6. 上传新文件到Dify + String newDifyFileId = null; + File tempFile = null; + try { + tempFile = File.createTempFile("knowledge_update_", "_" + file.getOriginalFilename()); + file.transferTo(tempFile); + + ResultDomain difyResult = aiFileUploadService.uploadFileToDify( + knowledge.getDifyDatasetId(), tempFile, file.getOriginalFilename(), knowledge.getDifyIndexingTechnique()); + + if (difyResult.getSuccess() && StringUtils.hasText(difyResult.getData())) { + newDifyFileId = difyResult.getData(); + } else { + fileService.deleteFile(newFileId); + return ResultDomain.failure("上传新文件到Dify失败: " + difyResult.getMessage()); + } + } catch (Exception e) { + logger.error("上传新文件到Dify异常: {}", e.getMessage(), e); + fileService.deleteFile(newFileId); + return ResultDomain.failure("上传新文件到Dify异常: " + e.getMessage()); + } finally { + if (tempFile != null && tempFile.exists()) { + tempFile.delete(); + } + } + + // 7. 保存tb_knowledge_file + TbKnowledgeFile newKnowledgeFile = new TbKnowledgeFile(); + newKnowledgeFile.setOptsn(IdUtil.getOptsn()); + newKnowledgeFile.setKnowledgeId(knowledgeId); + newKnowledgeFile.setFileRootId(fileRootId); + newKnowledgeFile.setFileId(newFileId); + newKnowledgeFile.setDifyFileId(newDifyFileId); + newKnowledgeFile.setVersion(newVersion); + + int rows = knowledgeFileMapper.insertKnowledgeFile(newKnowledgeFile); + if (rows > 0) { + logger.info("保存新版本记录成功: knowledgeId={}, fileRootId={}, newVersion={}", knowledgeId, fileRootId, newVersion); + return ResultDomain.success("更新成功", newKnowledgeFile); + } + + // 补偿 + fileService.deleteFile(newFileId); + aiFileUploadService.deleteFileFromDify(knowledge.getDifyDatasetId(), newDifyFileId); + return ResultDomain.failure("保存新版本记录失败"); + } + + /** + * @description 删除知识库文件(根据fileRootId删除所有版本) + * @param fileRootId 文件根ID + * @return ResultDomain 删除结果 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain deleteKnowledgeFileById(String fileRootId) { + if (!StringUtils.hasText(fileRootId)) { + return ResultDomain.failure("文件根ID不能为空"); + } + + // 1. 获取所有版本 + List versions = knowledgeFileMapper.selectFileVersions(fileRootId); + if (versions == null || versions.isEmpty()) { + return ResultDomain.failure("文件不存在"); + } + + // 2. 删除Dify中的文档 + TbKnowledge knowledge = knowledgeMapper.selectKnowledgeById(versions.get(0).getKnowledgeId()); + if (knowledge != null && StringUtils.hasText(knowledge.getDifyDatasetId())) { + List difyDocIds = new ArrayList<>(); + for (TbKnowledgeFile file : versions) { + if (StringUtils.hasText(file.getDifyFileId())) { + difyDocIds.add(file.getDifyFileId()); + } + } + if (!difyDocIds.isEmpty()) { + aiFileUploadService.batchDeleteFilesFromDify(knowledge.getDifyDatasetId(), difyDocIds); + } + } + + // 3. 软删除本地记录和minio文件 + int rows = knowledgeFileMapper.deleteFilesByRootId(fileRootId); + if (rows > 0) { + logger.info("删除知识库文件成功: fileRootId={}", fileRootId); + for (TbKnowledgeFile file : versions) { + fileService.deleteFile(file.getFileId()); + } + return ResultDomain.success("删除成功", true); + } + + return ResultDomain.failure("删除文件失败"); + } + + /** + * @description 获取文件历史版本 + * @param fileRootId 文件根ID + * @return ResultDomain 文件历史版本列表 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain getKnowledgeFileHistory(String fileRootId) { + if (!StringUtils.hasText(fileRootId)) { + return ResultDomain.failure("文件根ID不能为空"); + } + + List versions = knowledgeFileMapper.selectFileVersions(fileRootId); + return ResultDomain.success("查询成功", versions); + } +} diff --git a/urbanLifelineServ/ai/src/main/resources/application.yml b/urbanLifelineServ/ai/src/main/resources/application.yml index 8de48c16..212e8d8c 100644 --- a/urbanLifelineServ/ai/src/main/resources/application.yml +++ b/urbanLifelineServ/ai/src/main/resources/application.yml @@ -5,18 +5,19 @@ server: # context-path: /urban-lifeline/agent # 微服务架构下,context-path由Gateway管理 # ================== Auth ==================== -urban-lifeline: - auth: - enabled: true - whitelist: - - /swagger-ui/** - - /swagger-ui.html - - /v3/api-docs/** - - /webjars/** - - /favicon.ico - - /error - - /actuator/health - - /actuator/info +auth: + enabled: true + gateway-mode: true + whitelist: + - /swagger-ui/** + - /swagger-ui.html + - /v3/api-docs/** + - /webjars/** + - /favicon.ico + - /error + - /actuator/health + - /actuator/info + - /ai/chat/* # AI对话,有非系统用户对话的接口,无登录状态 security: aes: diff --git a/urbanLifelineServ/ai/src/main/resources/mapper/TbAgentMapper.xml b/urbanLifelineServ/ai/src/main/resources/mapper/TbAgentMapper.xml new file mode 100644 index 00000000..a79a1af3 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/resources/mapper/TbAgentMapper.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + optsn, agent_id, name, description, link, api_key, outer, introduce, prompt_cards, + category, creator, updater, create_time, update_time, delete_time, deleted + + + + INSERT INTO ai.tb_agent ( + optsn, agent_id, name, api_key, introduce, category + , outer + , description + , link + , prompt_cards + , creator + ) VALUES ( + #{optsn}, #{agentId}, #{name}, #{apiKey}, #{introduce}, #{category} + , #{outer} + , #{description} + , #{link} + , #{promptCards, typeHandler=org.xyzh.ai.handler.PromptCardsTypeHandler} + , #{creator} + ) + + + + UPDATE ai.tb_agent + + name = #{name}, + description = #{description}, + link = #{link}, + api_key = #{apiKey}, + outer = #{outer}, + introduce = #{introduce}, + prompt_cards = #{promptCards, typeHandler=org.xyzh.ai.handler.PromptCardsTypeHandler}, + category = #{category}, + updater = #{updater}, + update_time = now() + + WHERE agent_id = #{agentId} AND deleted = false + + + + UPDATE ai.tb_agent + SET deleted = true, + delete_time = now(), + updater = #{updater} + WHERE agent_id = #{agentId} AND deleted = false + + + + + + + + + + + + + + diff --git a/urbanLifelineServ/ai/src/main/resources/mapper/TbChatMapper.xml b/urbanLifelineServ/ai/src/main/resources/mapper/TbChatMapper.xml new file mode 100644 index 00000000..f643544f --- /dev/null +++ b/urbanLifelineServ/ai/src/main/resources/mapper/TbChatMapper.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + optsn, chat_id, agent_id, user_id, user_type, title, + create_time, update_time, delete_time, deleted + + + + INSERT INTO ai.tb_chat ( + optsn, chat_id, agent_id, user_id, title + , user_type + ) VALUES ( + #{optsn}, #{chatId}, #{agentId}, #{userId}, #{title} + , #{userType} + ) + + + + UPDATE ai.tb_chat + + title = #{title}, + update_time = now() + + WHERE chat_id = #{chatId} AND deleted = false + + + + UPDATE ai.tb_chat + SET deleted = true, + delete_time = now() + WHERE chat_id = #{chatId} AND deleted = false + + + + + + + + + + diff --git a/urbanLifelineServ/ai/src/main/resources/mapper/TbChatMessageMapper.xml b/urbanLifelineServ/ai/src/main/resources/mapper/TbChatMessageMapper.xml new file mode 100644 index 00000000..1ffdadaf --- /dev/null +++ b/urbanLifelineServ/ai/src/main/resources/mapper/TbChatMessageMapper.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + optsn, message_id, dify_message_id, chat_id, role, content, files, comment, + create_time, update_time, delete_time, deleted + + + + INSERT INTO ai.tb_chat_message ( + optsn, message_id, chat_id, role, content + , dify_message_id + , files + , comment + ) VALUES ( + #{optsn}, #{messageId}, #{chatId}, #{role}, #{content} + , #{difyMessageId} + , #{files, typeHandler=org.xyzh.common.utils.json.StringArrayTypeHandler} + , #{comment} + ) + + + + UPDATE ai.tb_chat_message + + content = #{content}, + comment = #{comment}, + update_time = now() + + WHERE message_id = #{messageId} AND deleted = false + + + + UPDATE ai.tb_chat_message + SET deleted = true, + delete_time = now() + WHERE message_id = #{messageId} AND deleted = false + + + + + + + + + + + + UPDATE ai.tb_chat_message + SET deleted = true, + delete_time = now() + WHERE chat_id = #{chatId} AND deleted = false + + diff --git a/urbanLifelineServ/ai/src/main/resources/mapper/TbKnowledgeFileMapper.xml b/urbanLifelineServ/ai/src/main/resources/mapper/TbKnowledgeFileMapper.xml new file mode 100644 index 00000000..1588226c --- /dev/null +++ b/urbanLifelineServ/ai/src/main/resources/mapper/TbKnowledgeFileMapper.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + optsn, knowledge_id, file_root_id, file_id, dify_file_id, version, + create_time, update_time, delete_time, deleted + + + + INSERT INTO ai.tb_knowledge_file ( + optsn, knowledge_id, file_root_id, file_id, dify_file_id + , version + ) VALUES ( + #{optsn}, #{knowledgeId}, #{fileRootId}, #{fileId}, #{difyFileId} + , #{version} + ) + + + + UPDATE ai.tb_knowledge_file + + dify_file_id = #{difyFileId}, + version = #{version}, + update_time = now() + + WHERE knowledge_id = #{knowledgeId} AND file_id = #{fileId} AND deleted = false + + + + UPDATE ai.tb_knowledge_file + SET deleted = true, + delete_time = now() + WHERE knowledge_id = #{knowledgeId} AND file_id = #{fileId} AND deleted = false + + + + + + + + + + + + + + UPDATE ai.tb_knowledge_file + SET deleted = true, + delete_time = now() + WHERE file_root_id = #{fileRootId} AND deleted = false + + + + diff --git a/urbanLifelineServ/ai/src/main/resources/mapper/TbKnowledgeMapper.xml b/urbanLifelineServ/ai/src/main/resources/mapper/TbKnowledgeMapper.xml new file mode 100644 index 00000000..4cd67bfc --- /dev/null +++ b/urbanLifelineServ/ai/src/main/resources/mapper/TbKnowledgeMapper.xml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + optsn, knowledge_id, title, avatar, description, dify_dataset_id, + dify_indexing_technique, embedding_model, embedding_model_provider, + rerank_model, rerank_model_provider, reranking_enable, + retrieval_top_k, retrieval_score_threshold, document_count, total_chunks, + service, project_id, category, creator, dept_path, updater, + create_time, update_time, delete_time, deleted + + + + INSERT INTO ai.tb_knowledge ( + optsn, knowledge_id, title, creator + , avatar + , description + , dify_dataset_id + , dify_indexing_technique + , embedding_model + , embedding_model_provider + , rerank_model + , rerank_model_provider + , reranking_enable + , retrieval_top_k + , retrieval_score_threshold + , service + , project_id + , category + , dept_path + ) VALUES ( + #{optsn}, #{knowledgeId}, #{title}, #{creator} + , #{avatar} + , #{description} + , #{difyDatasetId} + , #{difyIndexingTechnique} + , #{embeddingModel} + , #{embeddingModelProvider} + , #{rerankModel} + , #{rerankModelProvider} + , #{rerankingEnable} + , #{retrievalTopK} + , #{retrievalScoreThreshold} + , #{service} + , #{projectId} + , #{category} + , #{deptPath} + ) + + + + UPDATE ai.tb_knowledge + + title = #{title}, + avatar = #{avatar}, + description = #{description}, + dify_dataset_id = #{difyDatasetId}, + dify_indexing_technique = #{difyIndexingTechnique}, + embedding_model = #{embeddingModel}, + embedding_model_provider = #{embeddingModelProvider}, + rerank_model = #{rerankModel}, + rerank_model_provider = #{rerankModelProvider}, + reranking_enable = #{rerankingEnable}, + retrieval_top_k = #{retrievalTopK}, + retrieval_score_threshold = #{retrievalScoreThreshold}, + document_count = #{documentCount}, + total_chunks = #{totalChunks}, + service = #{service}, + project_id = #{projectId}, + category = #{category}, + updater = #{updater}, + update_time = now() + + WHERE knowledge_id = #{knowledgeId} AND deleted = false + + + + UPDATE ai.tb_knowledge + SET deleted = true, + delete_time = now(), + updater = #{updater} + WHERE knowledge_id = #{knowledgeId} AND deleted = false + + + + + + + + + + + + diff --git a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/ChatPrepareData.java b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/ChatPrepareData.java new file mode 100644 index 00000000..b085c729 --- /dev/null +++ b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/ChatPrepareData.java @@ -0,0 +1,32 @@ +package org.xyzh.api.ai.dto; + +import java.io.Serializable; +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "对话消息数据预处理对象") +public class ChatPrepareData implements Serializable { + private final static long serialVersionUID = 1L; + + @Schema(description = "智能体Id") + private String agentId; + + @Schema(description = "对话Id") + private String chatId; + + @Schema(description = "用户问题") + private String query; + + @Schema(description = "本次对话携带的dify文件对象") + private List files; + + @Schema(description = "用户ID(来客传wechatId,员工传userId)") + private String userId; + + @Schema(description = "用户类型(false=来客,true=员工)") + private Boolean userType; + +} diff --git a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbAgent.java b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbAgent.java index b19e52f4..4afd97e8 100644 --- a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbAgent.java +++ b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbAgent.java @@ -29,6 +29,9 @@ public class TbAgent extends BaseDTO{ @Schema(description = "智能体APIKEY") private String apiKey; + @Schema(description = "是否是对外智能体,未登录可用") + private Boolean outer; + @Schema(description = "引导词") private String introduce; diff --git a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbChatMessage.java b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbChatMessage.java index 86f29c69..e8f84710 100644 --- a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbChatMessage.java +++ b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbChatMessage.java @@ -15,6 +15,9 @@ public class TbChatMessage extends BaseDTO{ @Schema(description = "消息ID") private String messageId; + @Schema(description = "Dify消息ID") + private String difyMessageId; + @Schema(description = "对话ID") private String chatId; diff --git a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbKnowledge.java b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbKnowledge.java index 0c668059..0c042fd9 100644 --- a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbKnowledge.java +++ b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbKnowledge.java @@ -61,7 +61,7 @@ public class TbKnowledge extends BaseDTO{ @Schema(description = "bidding所属项目ID") private String projectId; - @Schema(description = "所属分类 workcase 内部知识库、外部知识库") + @Schema(description = "所属分类 workcase的内部知识库、外部知识库,其他服务default") private String category; diff --git a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbKnowledgeFile.java b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbKnowledgeFile.java index 836251b9..43846089 100644 --- a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbKnowledgeFile.java +++ b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/dto/TbKnowledgeFile.java @@ -23,6 +23,6 @@ public class TbKnowledgeFile extends BaseDTO{ private String difyFileId; @Schema(description = "文件版本") - private String version; + private Integer version; } diff --git a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AIFileUploadService.java b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AIFileUploadService.java index 07561f4e..1f7addac 100644 --- a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AIFileUploadService.java +++ b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AIFileUploadService.java @@ -1,64 +1,62 @@ package org.xyzh.api.ai.service; +import java.io.File; import java.util.List; import java.util.Map; import org.springframework.web.multipart.MultipartFile; -import org.xyzh.api.ai.dto.TbKnowledgeFile; import org.xyzh.common.core.domain.ResultDomain; -import org.xyzh.common.core.page.PageDomain; -import org.xyzh.common.core.page.PageParam; +/** + * @description AI文件上传服务(只负责与Dify交互,不处理minio和数据库) + * @author yslg + * @since 2025-12-18 + */ public interface AIFileUploadService { // ============================ 对话文件管理 ============================ - /** - * 上传文件用于对话(图文多模态) - * @param file 上传的文件 - * @param agentId 智能体ID - * @return Dify文件信息(包含id、name、size等) - */ - ResultDomain> uploadFileForChat( - MultipartFile file, - String agentId - ); - + /** + * @description 上传文件用于对话(图文多模态) + * @param file 上传的文件 + * @param agentId 智能体ID + * @return ResultDomain> Dify文件信息 + * @author yslg + * @since 2025-12-18 + */ + ResultDomain> uploadFileForChat(MultipartFile file, String agentId); - // ============================ 知识库文件管理 ============================ + // ============================ 知识库Dify文档管理 ============================ /** - * 上传文件到知识库(同步到Dify) - * @param knowledgeId 知识库ID - * @param file 上传的文件 - * @param indexingTechnique 索引方式(可选) - * @return 上传结果 + * @description 上传文件到Dify知识库(只负责Dify上传,不处理minio和数据库) + * @param difyDatasetId Dify知识库ID + * @param file 文件 + * @param fileName 文件名 + * @param indexingTechnique 索引方式 + * @return ResultDomain Dify文档ID + * @author yslg + * @since 2025-12-18 */ - ResultDomain uploadToKnowledge( - String knowledgeId, - MultipartFile file, - String indexingTechnique - ); + ResultDomain uploadFileToDify(String difyDatasetId, File file, String fileName, String indexingTechnique); /** - * 批量上传文件到知识库 - * @param knowledgeId 知识库ID - * @param files 上传的文件列表 - * @param indexingTechnique 索引方式(可选) - * @return 上传结果列表 + * @description 从Dify知识库删除文档 + * @param difyDatasetId Dify知识库ID + * @param difyDocumentId Dify文档ID + * @return ResultDomain 删除结果 + * @author yslg + * @since 2025-12-18 */ - ResultDomain batchUploadToKnowledge( - String knowledgeId, - List files, - String indexingTechnique - ); + ResultDomain deleteFileFromDify(String difyDatasetId, String difyDocumentId); /** - * 同时删除Dify中的文档 - * @param fileId dify的文件ID - * @return 删除结果 + * @description 批量从Dify知识库删除文档 + * @param difyDatasetId Dify知识库ID + * @param difyDocumentIds Dify文档ID列表 + * @return ResultDomain 删除结果 + * @author yslg + * @since 2025-12-18 */ - ResultDomain deleteKnowledgeFile(String fileId); - - + ResultDomain batchDeleteFilesFromDify(String difyDatasetId, List difyDocumentIds); } diff --git a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AgentChatService.java b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AgentChatService.java index 721c6cac..454cc272 100644 --- a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AgentChatService.java +++ b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AgentChatService.java @@ -1,9 +1,7 @@ package org.xyzh.api.ai.service; -import java.util.List; - import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import org.xyzh.api.ai.dto.DifyFileInfo; +import org.xyzh.api.ai.dto.ChatPrepareData; import org.xyzh.api.ai.dto.TbChat; import org.xyzh.api.ai.dto.TbChatMessage; import org.xyzh.common.core.domain.ResultDomain; @@ -17,7 +15,7 @@ public interface AgentChatService { * @param title 会话标题(可选) * @return 会话信息 */ - ResultDomain createChat(String agentId, String title); + ResultDomain createChat(TbChat chat); /** * 更新会话名称 @@ -26,7 +24,7 @@ public interface AgentChatService { * @param title 会话标题(可选) * @return 会话信息 */ - ResultDomain updateChat(String agentId, String chatId, String title); + ResultDomain updateChat(TbChat chat); /** * 删除会话 @@ -34,40 +32,31 @@ public interface AgentChatService { * @param chatId 会话ID * @return 会话信息 */ - ResultDomain deleteChat(String agentId, String chatId); + ResultDomain deleteChat(TbChat chat); /** * 获取会话列表 * @param agentId 智能体ID * @return 会话列表 */ - ResultDomain getChatList(String agentId); + ResultDomain getChatList(TbChat filter); // ====================== 智能体聊天管理 ====================== /** * 获取会话消息列表 - * @param agentId 智能体ID - * @param chatId 会话ID + * @param filter 会话过滤条件(包含agentId, chatId, userId, userType) * @return 会话消息列表 */ - ResultDomain getChatMessageList(String agentId, String chatId); + ResultDomain getChatMessageList(TbChat filter); /** * 准备聊天数据(POST传递复杂参数) - * @param agentId 智能体ID - * @param chatId 会话ID(可选,为空则创建新会话) - * @param query 用户问题 - * @param filesData 上传的文件列表(Dify文件信息) + * @param prepareData 对话准备数据(包含agentId, chatId, query, files, userId, userType) * @return ResultDomain 返回sessionId */ - ResultDomain prepareChatMessageSession( - String agentId, - String chatId, - String query, - List filesData - ); + ResultDomain prepareChatMessageSession(ChatPrepareData prepareData); /** * 流式对话(SSE)- 使用sessionId建立SSE连接 产生chatMessage @@ -78,21 +67,20 @@ public interface AgentChatService { /** * 停止对话生成(通过Dify TaskID) + * @param filter 会话过滤条件(包含agentId, userId, userType) * @param taskId Dify任务ID - * @param agentId 智能体ID * @return 停止结果 */ - ResultDomain stopChatMessageByTaskId(String taskId, String agentId); + ResultDomain stopChatMessageByTaskId(TbChat filter, String taskId); /** * 评价 - * @param agentId 智能体ID - * @param chatId 会话ID + * @param filter 会话过滤条件(包含agentId, chatId, userId, userType) * @param messageId 消息ID * @param comment 评价 * @return 评价结果 */ - ResultDomain commentChatMessage(String agentId, String chatId, String messageId, String comment); + ResultDomain commentChatMessage(TbChat filter, String messageId, String comment); } diff --git a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AgentService.java b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AgentService.java index 052ec5c6..17d2ecb2 100644 --- a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AgentService.java +++ b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/AgentService.java @@ -2,6 +2,7 @@ package org.xyzh.api.ai.service; import org.xyzh.api.ai.dto.TbAgent; import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageRequest; /** * Agent服务接口 @@ -12,6 +13,8 @@ public interface AgentService { /** * 添加Agent, 用户自己在dify搭建发布后,在本服务进行接入 * @param tbAgent + * @author yslg + * @since 2025-12-17 * @return */ ResultDomain addAgent(TbAgent tbAgent); @@ -19,6 +22,8 @@ public interface AgentService { /** * 更新Agent * @param tbAgent + * @author yslg + * @since 2025-12-17 * @return */ ResultDomain updateAgent(TbAgent tbAgent); @@ -26,7 +31,27 @@ public interface AgentService { /** * 删除Agent * @param tbAgent + * @author yslg + * @since 2025-12-17 * @return */ ResultDomain deleteAgent(TbAgent tbAgent); + + ResultDomain selectAgentById(String agentId); + + /** + * @description 分页获取当前用户可用的agent智能体 + * @param pageRequest + * @author yslg + * @since 2025-12-17 + */ + ResultDomain getAgentPage(PageRequest pageRequest); + + /** + * @description 获取当前用户所有可用智能体列表 + * @param tbAgent + * @author yslg + * @since 2025-12-17 + */ + ResultDomain getAgentList(TbAgent tbAgent); } diff --git a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/KnowledgeService.java b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/KnowledgeService.java index 0fc7f98c..f9b10663 100644 --- a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/KnowledgeService.java +++ b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/service/KnowledgeService.java @@ -3,6 +3,7 @@ package org.xyzh.api.ai.service; import java.util.List; import java.util.Map; +import org.springframework.web.multipart.MultipartFile; import org.xyzh.api.ai.dto.TbKnowledge; import org.xyzh.api.ai.dto.TbKnowledgeFile; import org.xyzh.common.core.domain.ResultDomain; @@ -11,13 +12,16 @@ import org.xyzh.common.core.page.PageParam; public interface KnowledgeService { // ================================= 知识库管理 ================================= + /** - * 创建知识库(同步到Dify) - * @param knowledge 知识库信息 - * @param permissionType 权限类型:PUBLIC-公开,DEPARTMENT-部门,ROLE-角色,PRIVATE-私有 - * @param deptIds 部门ID列表(DEPARTMENT类型需要) - * @param roleIds 角色ID列表(ROLE/PRIVATE类型需要) - * @return 创建结果 + * @description 创建知识库基础信息,包含dify知识库各种参数的配置 + * @param knowledge 知识库信息 + * @param permissionType 权限类型:PUBLIC-公开,DEPARTMENT-部门,ROLE-角色,PRIVATE-私有 + * @param deptIds 部门ID列表(DEPARTMENT类型需要) + * @param roleIds 角色ID列表(ROLE/PRIVATE类型需要) + * @return ResultDomain 创建结果 + * @author yslg + * @since 2025-12-18 */ ResultDomain createKnowledge( TbKnowledge knowledge, @@ -27,98 +31,138 @@ public interface KnowledgeService { ); /** - * 更新知识库 - * @param knowledge 知识库信息 - * @return 更新结果 + * @description 更新知识库,包含dify知识库各种参数的配置 + * @param knowledge 知识库信息 + * @return ResultDomain 更新结果 + * @author yslg + * @since 2025-12-18 */ ResultDomain updateKnowledge(TbKnowledge knowledge); /** - * 删除知识库(同时删除Dify中的知识库) - * @param knowledgeId 知识库ID - * @return 删除结果 + * @description 删除知识库,同时删除dify知识库 + * @param knowledgeId 知识库ID + * @return ResultDomain 删除结果 + * @author yslg + * @since 2025-12-18 */ ResultDomain deleteKnowledge(String knowledgeId); /** - * 根据ID查询知识库(带权限校验) - * @param knowledgeId 知识库ID - * @return 知识库信息 + * @description 获取知识库详情,包含dify知识库各种参数的配置 + * @param knowledgeId 知识库ID + * @return ResultDomain 知识库信息 + * @author yslg + * @since 2025-12-18 */ ResultDomain getKnowledgeById(String knowledgeId); /** - * 查询用户有权限的知识库列表 - * @param filter 过滤条件 - * @return 知识库列表 + * @description 查询知识库列表 + * @param filter 过滤条件 + * @return ResultDomain 知识库列表 + * @author yslg + * @since 2025-12-18 */ ResultDomain listKnowledges(TbKnowledge filter); /** - * 分页查询知识库 - * @param filter 过滤条件 - * @param pageParam 分页参数 - * @return 分页结果 + * @description 分页查询知识库 + * @param filter 过滤条件 + * @param pageParam 分页参数 + * @return ResultDomain 分页结果 + * @author yslg + * @since 2025-12-18 */ ResultDomain pageKnowledges(TbKnowledge filter, PageParam pageParam); /** - * 查询知识库的文档数量和分段数量(从Dify同步) - * @param knowledgeId 知识库ID - * @return 统计信息 + * @description 获取知识库统计信息(从Dify同步文档数量和分段数量) + * @param knowledgeId 知识库ID + * @return ResultDomain 统计信息 + * @author yslg + * @since 2025-12-18 */ ResultDomain getKnowledgeStats(String knowledgeId); /** - * 获取可用的嵌入模型列表 - * @return 嵌入模型列表 + * @description 获取可用的嵌入模型列表 + * @return ResultDomain> 嵌入模型列表 + * @author yslg + * @since 2025-12-18 */ ResultDomain> getAvailableEmbeddingModels(); /** - * 获取可用的Rerank模型列表 - * @return Rerank模型列表 + * @description 获取可用的Rerank模型列表 + * @return ResultDomain> Rerank模型列表 + * @author yslg + * @since 2025-12-18 */ ResultDomain> getAvailableRerankModels(); + // ================================= 文件管理 ================================= + /** - * 获取Dify知识库文档列表 - * @param knowledgeId 知识库ID - * @param page 页码(从1开始) - * @param limit 每页数量 - * @return 文档列表 + * @description 获取知识库文档列表(从Dify获取) + * @param knowledgeId 知识库ID + * @param page 页码(从1开始) + * @param limit 每页数量 + * @return ResultDomain> 文档列表 + * @author yslg + * @since 2025-12-18 */ ResultDomain> getDocumentList(String knowledgeId, Integer page, Integer limit); - // ================================= 文件管理 ================================= /** - * 添加知识库文件 - * @param knowledgeFile 文件信息 - * @return 添加结果 - */ - ResultDomain addKnowledgeFile(TbKnowledgeFile knowledgeFile); - - /** - * 更新知识库文件, 产生一个新version, 删除dify中旧版本的文件的document,并上传新文件重新索引 - * @param knowledgeFile 文件信息 - * @return 更新结果 - */ - ResultDomain updateKnowledgeFile(TbKnowledgeFile knowledgeFile); - - /** - * 删除知识库文件 - * @param knowledgeFile 文件信息 - * @return 删除结果 - */ - ResultDomain deleteKnowledgeFile(TbKnowledgeFile knowledgeFile); - - /** - * @description 获取一个文件的所有历史版本 - * @param fileId 文件ID + * @description 上传文件到知识库(完整流程:minio + Dify + 数据库) + * @param knowledgeId 知识库ID + * @param file 上传的文件 + * @param indexingTechnique 索引方式 + * @return ResultDomain 上传结果 * @author yslg - * @since 2025-12-15 + * @since 2025-12-18 */ - ResultDomain getKnowledgeFileHistory(String fileId); + ResultDomain uploadKnowledgeFile(String knowledgeId, MultipartFile file, String indexingTechnique); + /** + * @description 批量上传文件到知识库 + * @param knowledgeId 知识库ID + * @param files 文件列表 + * @param indexingTechnique 索引方式 + * @return ResultDomain 上传结果 + * @author yslg + * @since 2025-12-18 + */ + ResultDomain batchUploadKnowledgeFile(String knowledgeId, List files, String indexingTechnique); + + /** + * @description 上传新版本文件(fileRootId一致,version递增) + * @param knowledgeId 知识库ID + * @param file 新文件 + * @param fileRootId 文件根ID + * @return ResultDomain 新版本文件信息 + * @author yslg + * @since 2025-12-18 + */ + ResultDomain updateKnowledgeFileVersion(String knowledgeId, MultipartFile file, String fileRootId); + + /** + * @description 删除知识库文件(根据fileRootId删除所有版本) + * @param fileRootId 文件根ID + * @return ResultDomain 删除结果 + * @author yslg + * @since 2025-12-18 + */ + ResultDomain deleteKnowledgeFileById(String fileRootId); + + /** + * @description 获取文件历史版本 + * @param fileRootId 文件根ID + * @return ResultDomain 文件历史版本列表 + * @author yslg + * @since 2025-12-18 + */ + ResultDomain getKnowledgeFileHistory(String fileRootId); } diff --git a/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/vo/AgentVO.java b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/vo/AgentVO.java new file mode 100644 index 00000000..55c226be --- /dev/null +++ b/urbanLifelineServ/apis/api-ai/src/main/java/org/xyzh/api/ai/vo/AgentVO.java @@ -0,0 +1,50 @@ +package org.xyzh.api.ai.vo; + +import java.util.List; + +import org.xyzh.api.ai.dto.PromptCard; +import org.xyzh.common.vo.BaseVO; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @description Agent视图 + * @filename AgentVO.java + * @author yslg + * @copyright yslg + * @since 2025-11-05 + */ +@Data +@Schema(description = "Agent视图") +public class AgentVO extends BaseVO{ + private static final long serialVersionUID = 1L; + + @Schema(description = "智能体ID") + private String agentId; + + @Schema(description = "智能体名称") + private String name; + + @Schema(description = "智能体描述") + private String description; + + @Schema(description = "智能体url") + private String link; + + @Schema(description = "智能体APIKEY") + private String apiKey; + + @Schema(description = "是否是对外智能体,未登录可用") + private Boolean outer; + + @Schema(description = "引导词") + private String introduce; + + @Schema(description = "提示卡片数组") + private List promptCards; + + @Schema(description = "分类") + private String category; + +} diff --git a/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/dto/BidResponseDTO.java b/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/dto/BidResponseDTO.java index ebcfef0b..f288d35a 100644 --- a/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/dto/BidResponseDTO.java +++ b/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/dto/BidResponseDTO.java @@ -46,7 +46,7 @@ public class BidResponseDTO extends BaseDTO { private String generationStatus; @Schema(description = "版本号") - private String version; + private Integer version; @Schema(description = "父版本ID") private String parentVersionId; diff --git a/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/dto/BiddingDocumentDTO.java b/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/dto/BiddingDocumentDTO.java index cc41c845..73636874 100644 --- a/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/dto/BiddingDocumentDTO.java +++ b/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/dto/BiddingDocumentDTO.java @@ -40,7 +40,7 @@ public class BiddingDocumentDTO extends BaseDTO { private String mimeType; @Schema(description = "版本号") - private String version; + private Integer version; @Schema(description = "语言") private String language; diff --git a/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/vo/BidResponseVO.java b/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/vo/BidResponseVO.java index d111a9c9..c6332f0b 100644 --- a/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/vo/BidResponseVO.java +++ b/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/vo/BidResponseVO.java @@ -78,7 +78,7 @@ public class BidResponseVO extends BaseVO { private String fileUrl; @Schema(description = "版本号") - private String version; + private Integer version; @Schema(description = "父版本ID") private String parentVersionId; diff --git a/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/vo/BiddingDocumentVO.java b/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/vo/BiddingDocumentVO.java index 658d8fb0..047860d1 100644 --- a/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/vo/BiddingDocumentVO.java +++ b/urbanLifelineServ/apis/api-bidding/src/main/java/org/xyzh/api/bidding/vo/BiddingDocumentVO.java @@ -58,7 +58,7 @@ public class BiddingDocumentVO extends BaseVO { private String fileIcon; @Schema(description = "版本号") - private String version; + private Integer version; @Schema(description = "语言") private String language; diff --git a/urbanLifelineServ/apis/api-file/src/main/java/org/xyzh/api/file/dto/TbSysFileDTO.java b/urbanLifelineServ/apis/api-file/src/main/java/org/xyzh/api/file/dto/TbSysFileDTO.java index 8481bdaa..e726e2ce 100644 --- a/urbanLifelineServ/apis/api-file/src/main/java/org/xyzh/api/file/dto/TbSysFileDTO.java +++ b/urbanLifelineServ/apis/api-file/src/main/java/org/xyzh/api/file/dto/TbSysFileDTO.java @@ -26,7 +26,7 @@ public class TbSysFileDTO extends BaseDTO { private String fileRootId; @Schema(description = "文件版本") - private String version; + private Integer version; @Schema(description = "文件名") private String name; diff --git a/urbanLifelineServ/apis/api-file/src/main/java/org/xyzh/api/file/service/FileService.java b/urbanLifelineServ/apis/api-file/src/main/java/org/xyzh/api/file/service/FileService.java index e806bd67..d5a9783f 100644 --- a/urbanLifelineServ/apis/api-file/src/main/java/org/xyzh/api/file/service/FileService.java +++ b/urbanLifelineServ/apis/api-file/src/main/java/org/xyzh/api/file/service/FileService.java @@ -83,4 +83,16 @@ public interface FileService { */ ResultDomain saveTempFile(MultipartFile file, String module, String businessId); + /** + * @description 上传新版本文件(用于文件更新,fileRootId保持一致,version递增) + * @param file 文件对象 + * @param module 所属模块 + * @param businessId 业务ID + * @param fileRootId 文件根ID(多版本一致) + * @return ResultDomain 上传结果,包含新版本文件信息 + * @author yslg + * @since 2025-12-18 + */ + ResultDomain uploadFileVersion(MultipartFile file, String module, String businessId, String fileRootId); + } diff --git a/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/GuestService.java b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/GuestService.java new file mode 100644 index 00000000..c5fbf206 --- /dev/null +++ b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/GuestService.java @@ -0,0 +1,63 @@ +package org.xyzh.api.system.service; + +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageRequest; +import org.xyzh.common.dto.sys.TbGuestDTO; + +/** + * @description 来客服务接口 + * @filename GuestService.java + * @author yslg + * @copyright xyzh + * @since 2025-12-18 + */ +public interface GuestService { + + /** + * @description 创建来客 + * @param guest 来客信息 + * @author yslg + * @since 2025-12-18 + */ + ResultDomain createGuest(TbGuestDTO guest); + + /** + * @description 更新来客 + * @param guest 来客信息 + * @author yslg + * @since 2025-12-18 + */ + ResultDomain updateGuest(TbGuestDTO guest); + + /** + * @description 删除来客 + * @param userId 来客ID + * @author yslg + * @since 2025-12-18 + */ + ResultDomain deleteGuest(String userId); + + /** + * @description 查询单个来客 + * @param guest 来客信息 + * @author yslg + * @since 2025-12-18 + */ + ResultDomain selectGuestOne(TbGuestDTO guest); + + /** + * @description 查询来客列表 + * @param guest 来客信息 + * @author yslg + * @since 2025-12-18 + */ + ResultDomain selectGuestList(TbGuestDTO guest); + + /** + * @description 查询来客分页列表 + * @param pageRequest 分页请求 + * @author yslg + * @since 2025-12-18 + */ + ResultDomain selectGuestPage(PageRequest pageRequest); +} diff --git a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/service/impl/AuthServiceImpl.java b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/service/impl/AuthServiceImpl.java index ac1981c8..6bb07e55 100644 --- a/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/service/impl/AuthServiceImpl.java +++ b/urbanLifelineServ/auth/src/main/java/org/xyzh/auth/service/impl/AuthServiceImpl.java @@ -23,7 +23,7 @@ import org.xyzh.common.dto.sys.TbSysDeptDTO; import org.xyzh.common.dto.sys.TbSysPermissionDTO; import org.xyzh.common.dto.sys.TbSysViewDTO; import org.xyzh.common.redis.service.RedisService; -import org.xyzh.common.utils.IDUtils; +import org.xyzh.common.utils.id.IdUtil; import org.xyzh.common.utils.crypto.AesEncryptUtil; import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboService; @@ -79,7 +79,7 @@ public class AuthServiceImpl implements AuthService{ switch (captchaType) { case "image": // 生成图片验证码 - String captchaId = IDUtils.generateID(); + String captchaId = IdUtil.generateID(); String captchaCode = CapcatUtils.generateVerificationCode(); // 存储验证码到Redis,有效期5分钟 @@ -97,7 +97,7 @@ public class AuthServiceImpl implements AuthService{ return ResultDomain.failure("手机号不能为空"); } - String smsSessionId = IDUtils.generateID(); + String smsSessionId = IdUtil.generateID(); String smsCode = CapcatUtils.generateVerificationCode(); // 存储短信验证码到Redis,有效期5分钟 @@ -119,7 +119,7 @@ public class AuthServiceImpl implements AuthService{ return ResultDomain.failure("邮箱不能为空"); } - String emailSessionId = IDUtils.generateID(); + String emailSessionId = IdUtil.generateID(); String emailCode = CapcatUtils.generateVerificationCode(); // 存储邮箱验证码到Redis,有效期5分钟 @@ -210,7 +210,7 @@ public class AuthServiceImpl implements AuthService{ @Override public ResultDomain login(LoginParam loginParam, HttpServletRequest request) { String loginType = loginParam.getLoginType(); - String loginAttempt = IDUtils.generateID(); // 生成登录尝试ID + String loginAttempt = IdUtil.generateID(); // 生成登录尝试ID try { logger.info("用户登录请求:loginType={}, username={}, attempt={}", loginType, loginParam.getUsername(), loginAttempt); diff --git a/urbanLifelineServ/common/common-all/pom.xml b/urbanLifelineServ/common/common-all/pom.xml index dcba5743..1b1138d0 100644 --- a/urbanLifelineServ/common/common-all/pom.xml +++ b/urbanLifelineServ/common/common-all/pom.xml @@ -24,6 +24,10 @@ org.xyzh.common common-auth + + org.xyzh.common + common-exception + org.xyzh.common common-core diff --git a/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/utils/JwtTokenUtil.java b/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/utils/JwtTokenUtil.java index 526fe454..ac7e5b35 100644 --- a/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/utils/JwtTokenUtil.java +++ b/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/utils/JwtTokenUtil.java @@ -7,7 +7,7 @@ import org.springframework.stereotype.Component; import org.xyzh.common.core.domain.LoginDomain; import org.xyzh.common.dto.sys.TbSysUserDTO; import org.xyzh.common.dto.sys.TbSysUserInfoDTO; -import org.xyzh.common.utils.IDUtils; +import org.xyzh.common.utils.id.IdUtil; import javax.crypto.SecretKey; import java.util.Date; @@ -55,7 +55,7 @@ public class JwtTokenUtil { return Jwts.builder() .setClaims(claims) .setSubject(user.getUserId()) - .setId(IDUtils.generateID()) // 使用IDUtils生成JWT ID + .setId(IdUtil.generateID()) // 使用IdUtil生成JWT ID .setIssuedAt(new Date()) .setExpiration(generateExpirationDate()) .signWith(getSigningKey()) diff --git a/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/utils/LoginUtil.java b/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/utils/LoginUtil.java new file mode 100644 index 00000000..65a219d7 --- /dev/null +++ b/urbanLifelineServ/common/common-auth/src/main/java/org/xyzh/common/auth/utils/LoginUtil.java @@ -0,0 +1,116 @@ +package org.xyzh.common.auth.utils; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.xyzh.common.core.domain.LoginDomain; +import org.xyzh.common.redis.service.RedisService; + +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; + +/** + * @description 登录用户工具类,从Redis获取当前登录用户信息 + * @filename LoginUtil.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +@Component +public class LoginUtil { + + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String BEARER_PREFIX = "Bearer "; + private static final String LOGIN_TOKEN_PREFIX = "login:token:"; + + @Autowired + private RedisService redisService; + + private static LoginUtil instance; + + @PostConstruct + public void init() { + instance = this; + } + + /** + * 获取当前登录用户信息 + */ + public static LoginDomain getCurrentLogin() { + String token = getToken(); + if (!StringUtils.hasText(token)) { + return null; + } + + try { + String cacheKey = LOGIN_TOKEN_PREFIX + token; + Object obj = instance.redisService.get(cacheKey); + if (obj instanceof LoginDomain) { + return (LoginDomain) obj; + } + } catch (Exception e) { + // 忽略异常 + } + + return null; + } + + /** + * 获取当前用户ID + */ + public static String getCurrentUserId() { + LoginDomain login = getCurrentLogin(); + if (login != null && login.getUser() != null) { + return login.getUser().getUserId(); + } + return null; + } + + /** + * 获取当前用户名 + */ + public static String getCurrentUserName() { + LoginDomain login = getCurrentLogin(); + if (login != null && login.getUserInfo() != null) { + return login.getUserInfo().getUsername(); + } + return null; + } + + /** + * 判断用户是否已登录 + */ + public static boolean isLoggedIn() { + return getCurrentLogin() != null; + } + + /** + * 从请求头获取Token + */ + public static String getToken() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + + String authHeader = request.getHeader(AUTHORIZATION_HEADER); + if (StringUtils.hasText(authHeader) && authHeader.startsWith(BEARER_PREFIX)) { + return authHeader.substring(BEARER_PREFIX.length()); + } + + return null; + } + + /** + * 获取当前请求 + */ + private static HttpServletRequest getRequest() { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (attributes != null) { + return attributes.getRequest(); + } + return null; + } +} diff --git a/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/page/PageDomain.java b/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/page/PageDomain.java index 348ca07b..1c9b5aa2 100644 --- a/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/page/PageDomain.java +++ b/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/page/PageDomain.java @@ -22,6 +22,9 @@ public class PageDomain implements Serializable { * @since 2025-11-02 */ private List dataList; + + public PageDomain() { + } public PageDomain(PageParam pageParam, List dataList) { if (pageParam == null) { diff --git a/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/page/PageParam.java b/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/page/PageParam.java index 004c8012..a41bb258 100644 --- a/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/page/PageParam.java +++ b/urbanLifelineServ/common/common-core/src/main/java/org/xyzh/common/core/page/PageParam.java @@ -73,4 +73,11 @@ public class PageParam implements Serializable { } this.offset = (this.page - 1) * this.pageSize; } + + public void setTotal(int total){ + this.total = total; + if (this.pageSize > 0) { + this.totalPages = (int) Math.ceil((double) total / pageSize); + } + } } diff --git a/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/dto/sys/TbGuestDTO.java b/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/dto/sys/TbGuestDTO.java new file mode 100644 index 00000000..75fd0201 --- /dev/null +++ b/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/dto/sys/TbGuestDTO.java @@ -0,0 +1,27 @@ +package org.xyzh.common.dto.sys; + +import org.xyzh.common.dto.BaseDTO; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@Schema(description = "来客信息") +public class TbGuestDTO extends BaseDTO{ + private static final long serialVersionUID = 1L; + + @Schema(description = "来客ID") + private String userId; + + @Schema(description = "姓名") + private String name; + + @Schema(description = "电话") + private String phone; + + @Schema(description = "邮箱") + private String email; + + @Schema(description = "微信ID") + private String wechatId; +} diff --git a/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/vo/BaseVO.java b/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/vo/BaseVO.java index 73e1a4b7..5c41d88a 100644 --- a/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/vo/BaseVO.java +++ b/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/vo/BaseVO.java @@ -24,9 +24,15 @@ public class BaseVO implements Serializable { @Schema(description = "创建人") private String creator; + + @Schema(description = "创建人名称") + private String creatorName; @Schema(description = "更新人") private String updater; + + @Schema(description = "更新人名称") + private String updaterName; @Schema(description = "部门路径") private String deptPath; diff --git a/urbanLifelineServ/common/common-exception/pom.xml b/urbanLifelineServ/common/common-exception/pom.xml new file mode 100644 index 00000000..1d2f28e9 --- /dev/null +++ b/urbanLifelineServ/common/common-exception/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + org.xyzh + common + 1.0.0 + + + org.xyzh.common + common-exception + 1.0.0 + jar + + + 21 + 21 + + + + + org.xyzh.common + common-core + ${urban-lifeline.version} + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.projectlombok + lombok + provided + + + org.slf4j + slf4j-api + + + diff --git a/urbanLifelineServ/common/common-exception/src/main/java/org/xyzh/common/exception/BusinessException.java b/urbanLifelineServ/common/common-exception/src/main/java/org/xyzh/common/exception/BusinessException.java new file mode 100644 index 00000000..0153618d --- /dev/null +++ b/urbanLifelineServ/common/common-exception/src/main/java/org/xyzh/common/exception/BusinessException.java @@ -0,0 +1,36 @@ +package org.xyzh.common.exception; + +import lombok.Getter; + +/** + * @description 业务异常 + * @filename BusinessException.java + * @author yslg + * @copyright yslg + * @since 2025-12-17 + */ +@Getter +public class BusinessException extends RuntimeException { + private static final long serialVersionUID = 1L; + + private final Integer code; + private final String message; + + public BusinessException(String message) { + super(message); + this.code = 500; + this.message = message; + } + + public BusinessException(Integer code, String message) { + super(message); + this.code = code; + this.message = message; + } + + public BusinessException(String message, Throwable cause) { + super(message, cause); + this.code = 500; + this.message = message; + } +} diff --git a/urbanLifelineServ/common/common-exception/src/main/java/org/xyzh/common/exception/handler/GlobalExceptionHandler.java b/urbanLifelineServ/common/common-exception/src/main/java/org/xyzh/common/exception/handler/GlobalExceptionHandler.java new file mode 100644 index 00000000..60d4b1b6 --- /dev/null +++ b/urbanLifelineServ/common/common-exception/src/main/java/org/xyzh/common/exception/handler/GlobalExceptionHandler.java @@ -0,0 +1,124 @@ +package org.xyzh.common.exception.handler; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.multipart.MaxUploadSizeExceededException; +import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.exception.BusinessException; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @description 全局异常处理器 + * @filename GlobalExceptionHandler.java + * @author yslg + * @copyright yslg + * @since 2025-12-17 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 业务异常 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.OK) + public ResultDomain handleBusinessException(BusinessException e) { + log.warn("业务异常: {}", e.getMessage()); + return ResultDomain.failure(e.getCode(), e.getMessage()); + } + + /** + * 参数校验异常 - @RequestBody 参数校验失败 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.OK) + public ResultDomain handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining("; ")); + log.warn("参数校验失败: {}", message); + return ResultDomain.failure(HttpStatus.BAD_REQUEST.value(), message); + } + + /** + * 参数校验异常 - @Validated 校验失败(方法参数直接校验) + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.OK) + public ResultDomain handleConstraintViolationException(ConstraintViolationException e) { + Set> violations = e.getConstraintViolations(); + String message = violations.stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.joining("; ")); + log.warn("参数校验失败: {}", message); + return ResultDomain.failure(HttpStatus.BAD_REQUEST.value(), message); + } + + /** + * 参数绑定异常 + */ + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.OK) + public ResultDomain handleBindException(BindException e) { + String message = e.getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining("; ")); + log.warn("参数绑定失败: {}", message); + return ResultDomain.failure(HttpStatus.BAD_REQUEST.value(), message); + } + + /** + * 缺少请求参数 + */ + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseStatus(HttpStatus.OK) + public ResultDomain handleMissingServletRequestParameterException(MissingServletRequestParameterException e) { + String message = "缺少必要参数: " + e.getParameterName(); + log.warn(message); + return ResultDomain.failure(HttpStatus.BAD_REQUEST.value(), message); + } + + /** + * 缺少请求Part(multipart/form-data) + */ + @ExceptionHandler(MissingServletRequestPartException.class) + @ResponseStatus(HttpStatus.OK) + public ResultDomain handleMissingServletRequestPartException(MissingServletRequestPartException e) { + String message = "缺少必要参数: " + e.getRequestPartName(); + log.warn(message); + return ResultDomain.failure(HttpStatus.BAD_REQUEST.value(), message); + } + + /** + * 文件上传大小超限 + */ + @ExceptionHandler(MaxUploadSizeExceededException.class) + @ResponseStatus(HttpStatus.OK) + public ResultDomain handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) { + log.warn("文件上传大小超限: {}", e.getMessage()); + return ResultDomain.failure(HttpStatus.BAD_REQUEST.value(), "上传文件大小超过限制"); + } + + /** + * 其他未捕获异常 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.OK) + public ResultDomain handleException(Exception e) { + log.error("系统异常: ", e); + return ResultDomain.failure(HttpStatus.INTERNAL_SERVER_ERROR.value(), "系统异常,请联系管理员"); + } +} diff --git a/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/IDUtils.java b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/IDUtils.java deleted file mode 100644 index 7d24a1b1..00000000 --- a/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/IDUtils.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.xyzh.common.utils; - -import java.util.UUID; - -/** - * @description IDUtils.java文件描述 - * @filename IDUtils.java - * @author yslg - * @copyright yslg - * @since 2025-11-02 - */ -public class IDUtils { - - /** - * @description 生成UUID - * @return UUID - * @author yslg - * @since 2025-11-02 - */ - public static String generateID() { - return UUID.randomUUID().toString().replaceAll("-", ""); - } -} diff --git a/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/id/IdUtil.java b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/id/IdUtil.java new file mode 100644 index 00000000..573d88ea --- /dev/null +++ b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/id/IdUtil.java @@ -0,0 +1,46 @@ +package org.xyzh.common.utils.id; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @description ID生成工具类 + * @filename IdUtil.java + * @author yslg + * @copyright xyzh + * @since 2025-12-17 + */ +public class IdUtil { + + private static final AtomicLong SEQUENCE = new AtomicLong(0); + + /** + * 生成流水号(UUID格式,无横线) + */ + public static String getOptsn() { + return "optsn"+UUID.randomUUID().toString().replaceAll("-", ""); + } + + /** + * 生成雪花ID(简化实现,使用时间戳+序列号) + */ + public static String getSnowflakeId() { + long timestamp = System.currentTimeMillis(); + long seq = SEQUENCE.incrementAndGet() % 10000; + return String.valueOf(timestamp) + String.format("%04d", seq); + } + + /** + * 生成UUID + */ + public static String generateUUID() { + return UUID.randomUUID().toString().replaceAll("-", ""); + } + + /** + * 生成ID + */ + public static String generateID() { + return UUID.randomUUID().toString().replaceAll("-", ""); + } +} diff --git a/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/ValidationUtils.java b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/ValidationUtils.java index c4678403..4d666f17 100644 --- a/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/ValidationUtils.java +++ b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/ValidationUtils.java @@ -1,8 +1,13 @@ package org.xyzh.common.utils.validation; +import org.xyzh.common.utils.validation.method.InSetValidateMethod; +import org.xyzh.common.utils.validation.method.MinFieldsValidateMethod; +import org.xyzh.common.utils.validation.method.ObjectValidateMethod; import org.xyzh.common.utils.validation.method.ValidateMethod; import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -37,10 +42,10 @@ public class ValidationUtils { for (ValidationParam param : validationParams) { try { Object fieldValue = getFieldValue(obj, param.getFieldName()); - validateField(param, fieldValue, result); + validateField(param, fieldValue, obj, result); } catch (Exception e) { result.addError(param.getFieldLabel() + "字段获取失败: " + e.getMessage()); - } + } } return result; @@ -52,7 +57,7 @@ public class ValidationUtils { * @param validationParams 校验参数列表 * @return ValidationResult 校验结果 */ - public static ValidationResult validateMap(Map map, List validationParams) { + public static ValidationResult validateMap(Map map, List validationParams) { ValidationResult result = new ValidationResult(); if (map == null) { @@ -66,7 +71,7 @@ public class ValidationUtils { for (ValidationParam param : validationParams) { Object fieldValue = map.get(param.getFieldName()); - validateField(param, fieldValue, result); + validateField(param, fieldValue, map, result); } return result; @@ -76,9 +81,10 @@ public class ValidationUtils { * @description 校验单个字段 * @param param 校验参数 * @param fieldValue 字段值 + * @param obj 原始对象 * @param result 校验结果 */ - private static void validateField(ValidationParam param, Object fieldValue, ValidationResult result) { + private static void validateField(ValidationParam param, Object fieldValue, Object obj, ValidationResult result) { String fieldLabel = param.getFieldLabel(); // 1. 必填校验 @@ -176,10 +182,11 @@ public class ValidationUtils { } } - // 8. 使用ValidateMethod校验(直接传入实例,保留兼容性) + // 8. 使用ValidateMethod校验(直接传入实例) if (param.getValidateMethod() != null) { try { - if (!param.getValidateMethod().validate(fieldValue)) { + Object validateTarget = (param.getValidateMethod() instanceof ObjectValidateMethod) ? obj : fieldValue; + if (!param.getValidateMethod().validate(validateTarget)) { String errorMsg = param.getValidateMethod().getErrorMessage(); if (errorMsg != null && !errorMsg.isEmpty()) { result.addError(errorMsg); @@ -194,13 +201,27 @@ public class ValidationUtils { } /** - * @description 获取对象字段值(支持getter方法和直接访问) + * @description 获取对象字段值(支持getter方法、直接访问和嵌套路径) * @param obj 对象 - * @param fieldName 字段名 + * @param fieldName 字段名,支持嵌套路径如"pageParam.page" * @return 字段值 * @throws Exception 异常 */ private static Object getFieldValue(Object obj, String fieldName) throws Exception { + if (obj == null) { + return null; + } + + // 支持嵌套路径,如"pageParam.page" + if (fieldName.contains(".")) { + String[] parts = fieldName.split("\\.", 2); + Object nestedObj = getFieldValue(obj, parts[0]); + if (nestedObj == null) { + return null; + } + return getFieldValue(nestedObj, parts[1]); + } + if (obj instanceof Map) { return ((Map) obj).get(fieldName); } @@ -317,5 +338,46 @@ public class ValidationUtils { .patternDesc("请输入有效的手机号码") .build(); } -} + /** + * @description 创建"至少填一个"校验参数 + * @param fieldNames 字段名列表 + * @param fieldLabels 字段标签列表 + * @return ValidationParam + */ + public static ValidationParam atLeastOne(List fieldNames, List fieldLabels) { + return minFields(fieldNames, fieldLabels, 1); + } + + /** + * @description 创建"至少填几个"校验参数 + * @param fieldNames 字段名列表 + * @param fieldLabels 字段标签列表 + * @param minCount 至少填写的数量 + * @return ValidationParam + */ + public static ValidationParam minFields(List fieldNames, List fieldLabels, int minCount) { + return ValidationParam.builder() + .fieldName("_minFields") + .fieldLabel(String.join("、", fieldLabels)) + .validateMethod(new MinFieldsValidateMethod(fieldNames, fieldLabels, minCount)) + .build(); + } + + /** + * @description 创建N选1校验参数(值必须是集合中的一个) + * @param fieldName 字段名 + * @param fieldLabel 字段标签 + * @param required 是否必填 + * @param allowedValues 允许的值集合 + * @return ValidationParam + */ + public static ValidationParam inSet(String fieldName, String fieldLabel, Boolean required, Collection allowedValues) { + return ValidationParam.builder() + .fieldName(fieldName) + .fieldLabel(fieldLabel) + .required(required) + .validateMethod(new InSetValidateMethod(fieldLabel, allowedValues)) + .build(); + } +} diff --git a/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/AtLeastOneValidateMethod.java b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/AtLeastOneValidateMethod.java new file mode 100644 index 00000000..e69de29b diff --git a/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/InSetValidateMethod.java b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/InSetValidateMethod.java new file mode 100644 index 00000000..883c52d3 --- /dev/null +++ b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/InSetValidateMethod.java @@ -0,0 +1,68 @@ +package org.xyzh.common.utils.validation.method; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @description N选1校验方法,值必须是指定集合中的一个 + * @filename InSetValidateMethod.java + * @author yslg + * @copyright xyzh + * @since 2025-12-18 + */ +public class InSetValidateMethod implements ValidateMethod { + + private final Set allowedValues; + private final String errorMessage; + + /** + * @description 构造函数 + * @param fieldLabel 字段标签 + * @param allowedValues 允许的值集合 + */ + public InSetValidateMethod(String fieldLabel, Collection allowedValues) { + this.allowedValues = allowedValues.stream().map(v -> (Object) v).collect(Collectors.toSet()); + this.errorMessage = fieldLabel + "必须是以下值之一: " + formatValues(allowedValues); + } + + /** + * @description 构造函数(自定义错误信息) + * @param fieldLabel 字段标签 + * @param allowedValues 允许的值集合 + * @param customErrorMessage 自定义错误信息 + */ + public InSetValidateMethod(String fieldLabel, Collection allowedValues, String customErrorMessage) { + this.allowedValues = allowedValues.stream().map(v -> (Object) v).collect(Collectors.toSet()); + this.errorMessage = customErrorMessage; + } + + @Override + public Boolean validate(Object value) { + if (value == null) { + return false; + } + return allowedValues.contains(value); + } + + @Override + public String getErrorMessage() { + return errorMessage; + } + + @Override + public String getName() { + return "N选1校验"; + } + + /** + * @description 格式化允许值列表 + * @param values 值集合 + * @return 格式化后的字符串 + */ + private String formatValues(Collection values) { + return values.stream() + .map(v -> v instanceof String ? "\"" + v + "\"" : String.valueOf(v)) + .collect(Collectors.joining(", ", "[", "]")); + } +} diff --git a/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/MinFieldsValidateMethod.java b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/MinFieldsValidateMethod.java new file mode 100644 index 00000000..3b1483ad --- /dev/null +++ b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/MinFieldsValidateMethod.java @@ -0,0 +1,98 @@ +package org.xyzh.common.utils.validation.method; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +/** + * @description 至少填几个字段校验方法 + * @filename MinFieldsValidateMethod.java + * @author yslg + * @copyright yslg + * @since 2025-12-18 + */ +public class MinFieldsValidateMethod implements ObjectValidateMethod { + + + private final List fieldNames; + private final int minCount; + private final String errorMessage; + + public MinFieldsValidateMethod(List fieldNames, List fieldLabels) { + this(fieldNames, fieldLabels, 1); + } + + public MinFieldsValidateMethod(List fieldNames, List fieldLabels, int minCount) { + this.fieldNames = fieldNames; + this.minCount = minCount; + this.errorMessage = String.join("、", fieldLabels) + "至少填写" + minCount + "项"; + } + + @Override + public Boolean validate(Object obj) { + if (obj == null || fieldNames == null || fieldNames.isEmpty()) { + return false; + } + + int count = 0; + for (String fieldName : fieldNames) { + try { + Object fieldValue = getFieldValue(obj, fieldName); + if (fieldValue != null) { + if (fieldValue instanceof String) { + if (!((String) fieldValue).trim().isEmpty()) { + count++; + } + } else { + count++; + } + } + } catch (Exception e) { + // 字段不存在,忽略 + } + } + return count >= minCount; + } + + private Object getFieldValue(Object obj, String fieldName) throws Exception { + if (obj instanceof Map) { + return ((Map) obj).get(fieldName); + } + + Class clazz = obj.getClass(); + + // 尝试getter方法 + try { + String getterName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); + Method getter = clazz.getMethod(getterName); + return getter.invoke(obj); + } catch (NoSuchMethodException e) { + // 尝试直接访问字段 + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(obj); + } catch (NoSuchFieldException ex) { + // 尝试父类 + Class superClass = clazz.getSuperclass(); + if (superClass != null) { + Field field = superClass.getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(obj); + } + throw ex; + } + } + } + + @Override + public String getErrorMessage() { + return errorMessage; + } + + @Override + public String getName() { + return "至少填" + minCount + "个校验"; + } +} diff --git a/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/ObjectValidateMethod.java b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/ObjectValidateMethod.java new file mode 100644 index 00000000..695d5785 --- /dev/null +++ b/urbanLifelineServ/common/common-utils/src/main/java/org/xyzh/common/utils/validation/method/ObjectValidateMethod.java @@ -0,0 +1,11 @@ +package org.xyzh.common.utils.validation.method; + +/** + * @description 对象级校验方法接口,表示对整个对象进行校验 + * @filename ObjectValidateMethod.java + * @author yslg + * @copyright yslg + * @since 2025-12-18 + */ +public interface ObjectValidateMethod extends ValidateMethod { +} diff --git a/urbanLifelineServ/common/pom.xml b/urbanLifelineServ/common/pom.xml index 26512329..6561405e 100644 --- a/urbanLifelineServ/common/pom.xml +++ b/urbanLifelineServ/common/pom.xml @@ -20,6 +20,7 @@ common-redis common-utils common-all + common-exception @@ -59,6 +60,11 @@ common-utils ${urban-lifeline.version} + + org.xyzh.common + common-exception + ${urban-lifeline.version} + \ No newline at end of file diff --git a/urbanLifelineServ/file/src/main/java/org/xyzh/file/mapper/FileMapper.java b/urbanLifelineServ/file/src/main/java/org/xyzh/file/mapper/FileMapper.java index 0ec52c83..3922188e 100644 --- a/urbanLifelineServ/file/src/main/java/org/xyzh/file/mapper/FileMapper.java +++ b/urbanLifelineServ/file/src/main/java/org/xyzh/file/mapper/FileMapper.java @@ -84,4 +84,14 @@ public interface FileMapper extends BaseMapper { */ @Select("SELECT * FROM file.tb_sys_file WHERE bucket_name = #{bucketName} AND object_name = #{objectName} AND deleted = 0") TbSysFileDTO selectByMinioObject(@Param("bucketName") String bucketName, @Param("objectName") String objectName); + + /** + * @description 根据文件根ID查询最大版本号 + * @param fileRootId 文件根ID + * @return Integer 最大版本号 + * @author yslg + * @since 2025-12-18 + */ + @Select("SELECT MAX(version) FROM file.tb_sys_file WHERE file_root_id = #{fileRootId} AND deleted = 0") + Integer selectMaxVersionByFileRootId(@Param("fileRootId") String fileRootId); } diff --git a/urbanLifelineServ/file/src/main/java/org/xyzh/file/service/impl/FileServiceImpl.java b/urbanLifelineServ/file/src/main/java/org/xyzh/file/service/impl/FileServiceImpl.java index d2f6f259..6f8a1a10 100644 --- a/urbanLifelineServ/file/src/main/java/org/xyzh/file/service/impl/FileServiceImpl.java +++ b/urbanLifelineServ/file/src/main/java/org/xyzh/file/service/impl/FileServiceImpl.java @@ -157,6 +157,13 @@ public class FileServiceImpl implements FileService { } } + /** + * @description 软删除文件(只标记数据库记录为已删除,不删除minio文件) + * @param fileId 文件ID + * @return ResultDomain 删除结果 + * @author yslg + * @since 2025-12-18 + */ @Override public ResultDomain deleteFile(String fileId) { try { @@ -167,13 +174,13 @@ public class FileServiceImpl implements FileService { } // 删除MinIO中的文件 - minioUtil.deleteFile(sysFile.getBucketName(), sysFile.getObjectName()); + // minioUtil.deleteFile(sysFile.getBucketName(), sysFile.getObjectName()); // 逻辑删除数据库记录 int result = fileMapper.deleteByFileId(fileId, "system"); if (result > 0) { - logger.info("文件删除成功: {}", fileId); + logger.info("文件软删除成功: {}", fileId); return ResultDomain.success("文件删除成功", true); } else { return ResultDomain.failure("文件删除失败"); @@ -276,6 +283,97 @@ public class FileServiceImpl implements FileService { return ResultDomain.failure("临时文件保存失败: " + e.getMessage()); } } + + /** + * @description 上传新版本文件(用于文件更新,fileRootId保持一致,version递增) + * @param file 文件对象 + * @param module 所属模块 + * @param businessId 业务ID + * @param fileRootId 文件根ID(多版本一致) + * @return ResultDomain 上传结果,包含新版本文件信息 + * @author yslg + * @since 2025-12-18 + */ + @Override + public ResultDomain uploadFileVersion(MultipartFile file, String module, String businessId, String fileRootId) { + try { + if (file == null || file.isEmpty()) { + return ResultDomain.failure("文件不能为空"); + } + if (fileRootId == null || fileRootId.isEmpty()) { + return ResultDomain.failure("文件根ID不能为空"); + } + + // 1. 获取当前最大版本号 + Integer maxVersion = fileMapper.selectMaxVersionByFileRootId(fileRootId); + int newVersion = (maxVersion != null ? maxVersion : 0) + 1; + + // 2. 生成文件信息 + String originalFilename = file.getOriginalFilename(); + String extension = getFileExtension(originalFilename); + String contentType = file.getContentType(); + long size = file.getSize(); + + // 3. 生成唯一的对象名称 + String objectName = generateObjectName(originalFilename, module); + + // 4. 计算文件MD5 + String md5Hash = calculateMD5(file.getBytes()); + + // 5. 上传到MinIO + String bucketName = minioConfig.getBucketName(); + boolean uploadSuccess = minioUtil.uploadFile( + bucketName, + objectName, + file.getInputStream(), + size, + contentType + ); + + if (!uploadSuccess) { + return ResultDomain.failure("文件上传到MinIO失败"); + } + + // 6. 构建文件访问URL + String fileUrl = minioConfig.buildFileUrl(objectName); + + // 7. 保存到数据库(新版本记录) + TbSysFileDTO fileDTO = new TbSysFileDTO(); + fileDTO.setOptsn(UUID.randomUUID().toString()); + fileDTO.setFileId(UUID.randomUUID().toString()); + fileDTO.setFileRootId(fileRootId); + fileDTO.setVersion(newVersion); + fileDTO.setName(originalFilename); + fileDTO.setPath(objectName); + fileDTO.setSize(size); + fileDTO.setType(extension); + fileDTO.setStorageType("MINIO"); + fileDTO.setMimeType(contentType); + fileDTO.setUrl(fileUrl); + fileDTO.setStatus("NORMAL"); + fileDTO.setModule(module); + fileDTO.setBusinessId(businessId); + fileDTO.setObjectName(objectName); + fileDTO.setBucketName(bucketName); + fileDTO.setMd5Hash(md5Hash); + fileDTO.setExtension(extension); + fileDTO.setCreateTime(new java.util.Date()); + + int result = fileMapper.insertFile(fileDTO); + if (result <= 0) { + // 如果数据库保存失败,删除MinIO中的文件 + minioUtil.deleteFile(bucketName, objectName); + return ResultDomain.failure("文件信息保存失败"); + } + + logger.info("新版本文件上传成功: {}, version: {}, fileRootId: {}", originalFilename, newVersion, fileRootId); + return ResultDomain.success("文件上传成功", fileDTO); + + } catch (Exception e) { + logger.error("新版本文件上传失败", e); + return ResultDomain.failure("文件上传失败: " + e.getMessage()); + } + } /** * 生成唯一的对象名称 diff --git a/urbanLifelineServ/pom.xml b/urbanLifelineServ/pom.xml index 711b6bd8..1facc96d 100644 --- a/urbanLifelineServ/pom.xml +++ b/urbanLifelineServ/pom.xml @@ -274,6 +274,11 @@ common-utils ${urban-lifeline.version} + + org.xyzh.common + common-exception + ${urban-lifeline.version} + org.xyzh diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java new file mode 100644 index 00000000..17f791d9 --- /dev/null +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/GuestController.java @@ -0,0 +1,78 @@ +package org.xyzh.system.controller; + +import java.util.Arrays; + +import org.apache.dubbo.config.annotation.DubboReference; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.xyzh.api.system.service.GuestService; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageRequest; +import org.xyzh.common.dto.sys.TbGuestDTO; +import org.xyzh.common.utils.validation.ValidationUtils; + +import jakarta.validation.constraints.NotNull; + +/** + * @description 来客控制器 + * @filename GuestController.java + * @author yslg + * @copyright xyzh + * @since 2025-12-18 + */ +@Validated +@RestController +@RequestMapping("/system/guest") +public class GuestController { + + @DubboReference(version = "1.0.0", group = "system", timeout = 3000, retries = 0) + private GuestService guestService; + + + @PostMapping + public ResultDomain createGuest(TbGuestDTO guest) { + ValidationUtils.validate(guest, Arrays.asList( + ValidationUtils.requiredString("name", "姓名"), + ValidationUtils.atLeastOne( + Arrays.asList("phone", "email", "wechatId"), + Arrays.asList("电话", "邮箱", "微信ID") + ) + )); + return guestService.createGuest(guest); + } + + @PutMapping + public ResultDomain updateGuest(TbGuestDTO guest) { + ValidationUtils.validate(guest, Arrays.asList( + ValidationUtils.requiredString("name", "姓名"), + ValidationUtils.atLeastOne( + Arrays.asList("phone", "email", "wechatId"), + Arrays.asList("电话", "邮箱", "微信ID") + ) + )); + return guestService.updateGuest(guest); + } + + @DeleteMapping + public ResultDomain deleteGuest(@NotNull String userId) { + return guestService.deleteGuest(userId); + } + + + @GetMapping("/list") + public ResultDomain listGuest(TbGuestDTO filter) { + return guestService.selectGuestList(filter); + } + + @PostMapping("/page") + public ResultDomain pageGuest(PageRequest pageRequest) { + return guestService.selectGuestPage(pageRequest); + } + + +} diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/user/TbGuestMapper.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/user/TbGuestMapper.java new file mode 100644 index 00000000..ae979b84 --- /dev/null +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/user/TbGuestMapper.java @@ -0,0 +1,71 @@ +package org.xyzh.system.mapper.user; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.xyzh.common.core.page.PageParam; +import org.xyzh.common.dto.sys.TbGuestDTO; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @description 来客Mapper + * @filename TbGuestMapper.java + * @author yslg + * @copyright xyzh + * @since 2025-12-18 + */ +@Mapper +public interface TbGuestMapper extends BaseMapper{ + + /** + * @description 插入来客 + * @param guest 来客信息 + * @author yslg + * @since 2025-12-18 + */ + int insertGuest(TbGuestDTO guest); + + /** + * @description 更新来客 + * @param guest 来客信息 + * @author yslg + * @since 2025-12-18 + */ + int updateGuest(TbGuestDTO guest); + + /** + * @description 删除来客 + * @param userId 来客ID + * @author yslg + * @since 2025-12-18 + */ + int deleteGuest(String userId); + + /** + * @description 查询单个来客 + * @param guest 来客信息 + * @author yslg + * @since 2025-12-18 + */ + TbGuestDTO selectGuestOne(TbGuestDTO guest); + + /** + * @description 查询来客列表 + * @param guest 来客信息 + * @author yslg + * @since 2025-12-18 + */ + List selectGuestList(TbGuestDTO guest); + + /** + * @description 查询来客分页列表 + * @param guest 来客信息 + * @param pageParam 分页参数 + * @author yslg + * @since 2025-12-18 + */ + List selectGuestPage(TbGuestDTO guest, PageParam pageParam); + + int countGuest(TbGuestDTO guest); +} diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/AclServiceImpl.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/AclServiceImpl.java index 936f24cb..c7a7a7c9 100644 --- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/AclServiceImpl.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/AclServiceImpl.java @@ -14,7 +14,7 @@ import org.xyzh.common.core.page.PageParam; import org.xyzh.common.core.page.PageRequest; import org.xyzh.common.dto.sys.TbSysAclDTO; import org.xyzh.common.dto.sys.TbSysAclPolicyDTO; -import org.xyzh.common.utils.IDUtils; +import org.xyzh.common.utils.id.IdUtil; import org.xyzh.common.utils.StringUtils; import org.xyzh.system.mapper.acl.TbSysAclMapper; import org.xyzh.system.mapper.acl.TbSysAclPolicyMapper; @@ -54,7 +54,7 @@ public class AclServiceImpl implements AclService { return ResultDomain.failure(MSG_ACL_PARAM_REQUIRED); } if (StringUtils.isBlank(aclDTO.getAclId())) { - aclDTO.setAclId(IDUtils.generateID()); + aclDTO.setAclId(IdUtil.generateID()); } if (aclDTO.getCreateTime() == null) { aclDTO.setCreateTime(new Date()); @@ -178,7 +178,7 @@ public class AclServiceImpl implements AclService { return ResultDomain.failure(MSG_POLICY_PARAM_REQUIRED); } if (StringUtils.isBlank(aclPolicyDTO.getPolicyId())) { - aclPolicyDTO.setPolicyId(IDUtils.generateID()); + aclPolicyDTO.setPolicyId(IdUtil.generateID()); } if (aclPolicyDTO.getCreateTime() == null) { aclPolicyDTO.setCreateTime(new Date()); diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/DeptRoleServiceImpl.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/DeptRoleServiceImpl.java index 88f8c261..a572f195 100644 --- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/DeptRoleServiceImpl.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/DeptRoleServiceImpl.java @@ -21,7 +21,7 @@ import org.xyzh.common.dto.sys.TbSysDeptRoleDTO; import org.xyzh.common.dto.sys.TbSysRoleDTO; import org.xyzh.common.dto.sys.TbSysRolePermissionDTO; import org.xyzh.common.dto.sys.TbSysUserRoleDTO; -import org.xyzh.common.utils.IDUtils; +import org.xyzh.common.utils.id.IdUtil; import org.xyzh.common.utils.StringUtils; import org.xyzh.system.mapper.dept.TbSysDeptMapper; import org.xyzh.system.mapper.role.TbSysRoleMapper; @@ -67,7 +67,7 @@ public class DeptRoleServiceImpl implements DeptRoleService { return ResultDomain.failure(MSG_DEPT_PARAM_REQUIRED); } if (StringUtils.isBlank(deptDTO.getDeptId())) { - deptDTO.setDeptId(IDUtils.generateID()); + deptDTO.setDeptId(IdUtil.generateID()); } if (deptDTO.getCreateTime() == null) { deptDTO.setCreateTime(new Date()); @@ -166,7 +166,7 @@ public class DeptRoleServiceImpl implements DeptRoleService { return ResultDomain.failure(MSG_ROLE_PARAM_REQUIRED); } if (StringUtils.isBlank(roleDTO.getRoleId())) { - roleDTO.setRoleId(IDUtils.generateID()); + roleDTO.setRoleId(IdUtil.generateID()); } if (roleDTO.getCreateTime() == null) { roleDTO.setCreateTime(new Date()); diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/GuestServiceImpl.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/GuestServiceImpl.java new file mode 100644 index 00000000..d62b503e --- /dev/null +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/GuestServiceImpl.java @@ -0,0 +1,81 @@ +package org.xyzh.system.service.impl; + +import java.util.List; + +import org.apache.dubbo.config.annotation.DubboService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; +import org.xyzh.api.system.service.GuestService; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageDomain; +import org.xyzh.common.core.page.PageRequest; +import org.xyzh.common.dto.sys.TbGuestDTO; +import org.xyzh.system.mapper.user.TbGuestMapper; + + +/** + * @description 来客服务接口实现 + * @filename GuestServiceImpl.java + * @author yslg + * @copyright xyzh + * @since 2025-12-18 + */ +@DubboService( + version = "1.0.0", + group = "system", + timeout = 3000, + retries = 0 +) +public class GuestServiceImpl implements GuestService{ + private static final Logger logger = LoggerFactory.getLogger(GuestServiceImpl.class); + + @Autowired + private TbGuestMapper guestMapper; + + @Override + @Transactional + public ResultDomain createGuest(TbGuestDTO guest) { + guestMapper.insertGuest(guest); + return ResultDomain.success("创建成功",guest); + } + + @Override + @Transactional + public ResultDomain updateGuest(TbGuestDTO guest) { + guestMapper.updateGuest(guest); + return ResultDomain.success("更新成功",guest); + } + + @Override + public ResultDomain deleteGuest(String userId) { + guestMapper.deleteGuest(userId); + return ResultDomain.success("删除成功"); + } + + @Override + public ResultDomain selectGuestList(TbGuestDTO guest) { + guestMapper.selectGuestList(guest); + return ResultDomain.success("查询成功",guest); + } + + @Override + public ResultDomain selectGuestOne(TbGuestDTO guest) { + guestMapper.selectGuestOne(guest); + return ResultDomain.success("查询成功",guest); + } + + @Override + public ResultDomain selectGuestPage(PageRequest pageRequest) { + List guestList = guestMapper.selectGuestPage(pageRequest.getFilter(),pageRequest.getPageParam()); + int total = guestMapper.countGuest(pageRequest.getFilter()); + PageDomain pageDomain = new PageDomain<>(); + pageDomain.setPageParam(pageRequest.getPageParam()); + pageDomain.getPageParam().setTotal(total); + + pageDomain.setDataList(guestList); + return ResultDomain.success("查询成功",pageDomain); + } + +} diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/ModulePermissionServiceImpl.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/ModulePermissionServiceImpl.java index 6ed8dede..4ef2dc19 100644 --- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/ModulePermissionServiceImpl.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/ModulePermissionServiceImpl.java @@ -15,7 +15,7 @@ import java.util.Collections; import java.util.List; import org.xyzh.common.core.page.PageParam; import org.xyzh.common.core.page.PageDomain; -import org.xyzh.common.utils.IDUtils; +import org.xyzh.common.utils.id.IdUtil; import org.xyzh.common.utils.StringUtils; import org.xyzh.system.mapper.module.TbSysModuleMapper; import org.xyzh.system.mapper.permission.TbSysPermissionMapper; @@ -61,7 +61,7 @@ public class ModulePermissionServiceImpl implements ModulePermissionService { return ResultDomain.failure(MSG_MODULE_PARAM_REQUIRED); } if (StringUtils.isBlank(moduleDTO.getModuleId())) { - moduleDTO.setModuleId(IDUtils.generateID()); + moduleDTO.setModuleId(IdUtil.generateID()); } if (moduleDTO.getCreateTime() == null) { moduleDTO.setCreateTime(new Date()); @@ -138,7 +138,7 @@ public class ModulePermissionServiceImpl implements ModulePermissionService { return ResultDomain.failure(MSG_PERMISSION_PARAM_REQUIRED); } if (StringUtils.isBlank(permissionDTO.getPermissionId())) { - permissionDTO.setPermissionId(IDUtils.generateID()); + permissionDTO.setPermissionId(IdUtil.generateID()); } if (permissionDTO.getCreateTime() == null) { permissionDTO.setCreateTime(new Date()); diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysConfigServiceImpl.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysConfigServiceImpl.java index 5ace9c32..57c8031e 100644 --- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysConfigServiceImpl.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysConfigServiceImpl.java @@ -15,7 +15,7 @@ import java.util.List; import org.xyzh.common.core.page.PageParam; import org.xyzh.common.core.page.PageDomain; -import org.xyzh.common.utils.IDUtils; +import org.xyzh.common.utils.id.IdUtil; import org.xyzh.common.utils.StringUtils; import org.xyzh.system.mapper.config.TbSysConfigMapper; @@ -231,7 +231,7 @@ public class SysConfigServiceImpl implements SysConfigService { return ResultDomain.failure(MSG_CONFIG_PARAM_REQUIRED); } if (StringUtils.isBlank(configDTO.getConfigId())) { - configDTO.setConfigId(IDUtils.generateID()); + configDTO.setConfigId(IdUtil.generateID()); } if (configDTO.getCreateTime() == null) { configDTO.setCreateTime(new Date()); diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysUserServiceImpl.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysUserServiceImpl.java index 0158fbd0..51f0ce90 100644 --- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysUserServiceImpl.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/SysUserServiceImpl.java @@ -18,7 +18,7 @@ import java.util.List; import org.xyzh.common.core.page.PageParam; import org.xyzh.common.core.page.PageDomain; -import org.xyzh.common.utils.IDUtils; +import org.xyzh.common.utils.id.IdUtil; import org.xyzh.common.utils.NonUtils; import org.xyzh.common.utils.StringUtils; import org.xyzh.common.utils.crypto.AesEncryptUtil; @@ -80,7 +80,7 @@ public class SysUserServiceImpl implements SysUserService { // 设置用户基本信息 Date now = new Date(); if (StringUtils.isBlank(dto.getUserId())) { - dto.setUserId(IDUtils.generateID()); + dto.setUserId(IdUtil.generateID()); } dto.setPhone(userVO.getPhone()); dto.setCreateTime(now); @@ -299,7 +299,7 @@ public class SysUserServiceImpl implements SysUserService { if (StringUtils.isBlank(userId)) { return ResultDomain.failure(MSG_USER_ID_REQUIRED); } - String newPwd = IDUtils.generateID(); + String newPwd = IdUtil.generateID(); if (newPwd.length() > 12) { newPwd = newPwd.substring(0, 12); } diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/ViewServiceImpl.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/ViewServiceImpl.java index 30f962cc..e137b653 100644 --- a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/ViewServiceImpl.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/ViewServiceImpl.java @@ -16,7 +16,7 @@ import org.xyzh.common.core.page.PageParam; import org.xyzh.common.core.page.PageRequest; import org.xyzh.common.dto.sys.TbSysViewDTO; import org.xyzh.common.dto.sys.TbSysViewPermissionDTO; -import org.xyzh.common.utils.IDUtils; +import org.xyzh.common.utils.id.IdUtil; import org.xyzh.common.utils.StringUtils; import org.xyzh.system.mapper.view.TbSysViewMapper; import org.xyzh.system.mapper.view.TbSysViewPermissionMapper; @@ -50,7 +50,7 @@ public class ViewServiceImpl implements ViewService { return ResultDomain.failure("视图参数不能为空"); } if (StringUtils.isBlank(viewDTO.getViewId())) { - viewDTO.setViewId(IDUtils.generateID()); + viewDTO.setViewId(IdUtil.generateID()); } if (viewDTO.getCreateTime() == null) { viewDTO.setCreateTime(new Date()); diff --git a/urbanLifelineServ/system/src/main/resources/mapper/user/TbGuestMapper.xml b/urbanLifelineServ/system/src/main/resources/mapper/user/TbGuestMapper.xml new file mode 100644 index 00000000..035602b2 --- /dev/null +++ b/urbanLifelineServ/system/src/main/resources/mapper/user/TbGuestMapper.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + optsn, user_id, name, phone, email, wechat_id, create_time, update_time, delete_time, deleted + + + + INSERT INTO sys.tb_guest ( + optsn, user_id, name + , phone + , email + , wechat_id + , create_time + , deleted + ) VALUES ( + #{optsn}, #{userId}, #{name} + , #{phone} + , #{email} + , #{wechatId} + , #{createTime} + , #{deleted} + ) + + + + UPDATE sys.tb_guest + + name = #{name}, + phone = #{phone}, + email = #{email}, + wechat_id = #{wechatId}, + update_time = now() + + WHERE user_id = #{userId} AND deleted = false + + + + UPDATE sys.tb_guest + SET deleted = true, + delete_time = now() + WHERE user_id = #{userId} AND deleted = false + + + + + + + + + + + + \ No newline at end of file