From a07daa715a972df1eda73b454b30157d545a73e5 Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Thu, 1 Jan 2026 12:16:24 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../database/postgres/sql/initDataConfig.sql | 4 ++ .../java/org/xyzh/workcase/config/AiInit.java | 39 ++++++++++++++----- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql b/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql index d16074d1..0b3677ed 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql @@ -84,6 +84,10 @@ INSERT INTO config.tb_sys_config ( ('CFG-0475', 'cfg_dify_rerank_provider', 'dify.knowledge.rerank.model.provider','Rerank模型供应商', 'langgenius/siliconflow/siliconflow', 'String', 'input', 'Rerank模型供应商', NULL, NULL, 'dify', 'mod_agent', 150, 1, 'Rerank模型的供应商标识', 'system', NULL, NULL, now(), NULL, NULL, false), ('CFG-0476', 'cfg_dify_retrieval_topk', 'dify.knowledge.retrieval.top.k','检索TopK', '5', 'INTEGER', 'input', '检索返回的最大文档数', NULL, NULL, 'dify', 'mod_agent', 160, 1, '知识库检索时返回的最相关文档数量(1-20)', 'system', NULL, NULL, now(), NULL, NULL, false), ('CFG-0477', 'cfg_dify_retrieval_threshold', 'dify.knowledge.retrieval.score.threshold','相似度阈值', '0.5', 'DOUBLE', 'input', '检索相似度阈值', NULL, NULL, 'dify', 'mod_agent', 170, 1, '低于此阈值的文档将被过滤(0.0-1.0)', 'system', NULL, NULL, now(), NULL, NULL, false), +-- Dify workcase相关智能体配置 +('CFG-0478', 'cfg_dify_workcase_chat', 'dify.workcase.agent.chat','泰豪小电AgentApiKey', 'app-CDKy0wYkPnl6dA6G7eu113Vw', 'String', 'input', '泰豪小电AgentApiKey', NULL, NULL, 'dify', 'mod_agent', 160, 1, '泰豪小电AgentApiKey', 'system', NULL, NULL, now(), NULL, NULL, false), +('CFG-0479', 'cfg_dify_workcase_summary', 'dify.workcase.agent.summary','工单总结AgentApikey', 'app-YMlj2B0m21KpYZPv3YdObi7r', 'String', 'input', '工单总结AgentApikey', NULL, NULL, 'dify', 'mod_agent', 170, 1, '工单总结AgentApikey', 'system', NULL, NULL, now(), NULL, NULL, false), + -- 日志与审计 ('CFG-0501', 'cfg_log_level', 'log.level', '日志级别', 'INFO', 'String', 'select', '系统日志级别', NULL, '["DEBUG", "INFO", "WARN", "ERROR"]'::json, 'log', 'mod_system', 10, 0, 'DEBUG/INFO/WARN/ERROR', 'system', NULL, NULL, now(), NULL, NULL, false), diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java index 19cc7d5b..da407be1 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java @@ -66,6 +66,7 @@ public class AiInit { @Bean public CommandLineRunner agentInitRunner(){ return args -> { + String chatAgentApiKey = sysConfigService.getStringConfig("dify.workcase.agent.chat"); logger.info("开始初始化客服系统智能体..."); TbAgent agent = new TbAgent(); agent.setIsOuter(true); @@ -73,17 +74,35 @@ public class AiInit { ResultDomain listDomain = agentService.getAgentList(agent); if (listDomain.getSuccess()&&!listDomain.getDataList().isEmpty()) { logger.info("泰豪小电智能体已经存在"); - return; - } - agent.setApiKey("app-CDKy0wYkPnl6dA6G7eu113Vw"); - agent.setIntroduce("您好,我是泰豪小电智能客服。请描述您的问题,我会尽力协助。"); - agent.setCategory("客服智能体"); - agent.setCategory("user_admin"); - ResultDomain resultDomain = agentService.addAgent(agent); - if(resultDomain.getSuccess()){ - logger.info("泰豪小电智能体初始化成功"); }else{ - logger.error("泰豪小电智能体初始化失败"+resultDomain.getMessage()); + agent.setApiKey(chatAgentApiKey); + agent.setIntroduce("您好,我是泰豪小电智能客服。请描述您的问题,我会尽力协助。"); + agent.setCategory("客服智能体"); + ResultDomain resultDomain = agentService.addAgent(agent); + if(resultDomain.getSuccess()){ + logger.info("泰豪小电智能体初始化成功"); + }else{ + logger.error("泰豪小电智能体初始化失败"+resultDomain.getMessage()); + } + } + + String summaryAgentApiKey = sysConfigService.getStringConfig("dify.workcase.agent.summary"); + TbAgent summaryAgent = new TbAgent(); + summaryAgent.setIsOuter(true); + summaryAgent.setName("工单总结"); + ResultDomain listDomain2 = agentService.getAgentList(summaryAgent); + if (listDomain2.getSuccess()&&!listDomain2.getDataList().isEmpty()) { + logger.info("工单总结智能体已经存在"); + }else { + summaryAgent.setApiKey(summaryAgentApiKey); + summaryAgent.setIntroduce("工单总结工作流"); + summaryAgent.setCategory("工作流"); + ResultDomain resultDomain2 = agentService.addAgent(summaryAgent); + if(resultDomain2.getSuccess()){ + logger.info("泰豪小电智能体初始化成功"); + }else{ + logger.error("泰豪小电智能体初始化失败"+resultDomain2.getMessage()); + } } }; } From 4e373e6d2c683fd215ef270d02374cf0b21ab159 Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Thu, 1 Jan 2026 13:12:42 +0800 Subject: [PATCH 2/6] =?UTF-8?q?ai=E8=81=8A=E5=A4=A9input=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai/service/impl/AgentChatServiceImpl.java | 61 +++++++++++-------- .../org/xyzh/api/ai/dto/ChatPrepareData.java | 4 ++ 2 files changed, 41 insertions(+), 24 deletions(-) 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 index 2e0e3880..70ca191d 100644 --- 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 @@ -256,7 +256,7 @@ public class AgentChatServiceImpl implements AgentChatService { 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().getIsOuter()) { @@ -269,7 +269,7 @@ public class AgentChatServiceImpl implements AgentChatService { chatFilter.setAgentId(agentId); chatFilter.setUserId(prepareData.getUserId()); chatFilter.setUserType(prepareData.getUserType()); - + LoginDomain loginDomain = LoginUtil.getCurrentLogin(); String userId = loginDomain.getUser().getUserId(); if (userId == null) { @@ -290,7 +290,30 @@ public class AgentChatServiceImpl implements AgentChatService { // 4. 生成临时消息ID(sessionId) String sessionId = IdUtil.getSnowflakeId(); - // 5. 存储会话数据到Redis + // 5. 准备 inputs 参数 + Map inputsMap = prepareData.getInputsMap(); + if (inputsMap == null) { + inputsMap = new HashMap<>(); + } + + // 处理动态知识库 + Boolean isGuest = "guest".equals(loginDomain.getUser().getStatus()); + if (agent.getIsOuter() && NonUtils.isNotEmpty(prepareData.getService())) { + TbKnowledge filter = new TbKnowledge(); + filter.setService(prepareData.getService()); + filter.setCategory(isGuest ? "external" : "internal"); + ResultDomain knowledgeRD = knowledgeService.listKnowledges(filter); + List datasets = new ArrayList<>(); + if (knowledgeRD.getSuccess()) { + datasets = knowledgeRD.getDataList().stream() + .map(TbKnowledge::getDifyDatasetId) + .toList(); + } + inputsMap.put("datasets", JSON.toJSONString(datasets)); + inputsMap.put("dataset_apikey", difyConfig.getKnowledgeApiKey()); + } + + // 6. 存储会话数据到Redis Map sessionData = new HashMap<>(); sessionData.put("agentId", agentId); sessionData.put("chatId", chatId); @@ -300,7 +323,8 @@ public class AgentChatServiceImpl implements AgentChatService { sessionData.put("apiKey", agent.getApiKey()); sessionData.put("outer", agent.getIsOuter()); sessionData.put("service", prepareData.getService()); - sessionData.put("isGuest", "guest".equals(loginDomain.getUser().getStatus())); + sessionData.put("isGuest", isGuest); + sessionData.put("inputsMap", inputsMap); // 存储处理好的 inputs String cacheKey = CHAT_SESSION_PREFIX + sessionId; redisService.set(cacheKey, sessionData, SESSION_TTL, TimeUnit.SECONDS); @@ -334,9 +358,9 @@ public class AgentChatServiceImpl implements AgentChatService { String query = (String) sessionData.get("query"); String userId = (String) sessionData.get("userId"); String apiKey = (String) sessionData.get("apiKey"); - String service = (String) sessionData.get("service"); - Boolean outer = (Boolean) sessionData.get("outer"); - Boolean isGuest = (Boolean) sessionData.get("isGuest"); + + @SuppressWarnings("unchecked") + Map inputsMap = (Map) sessionData.get("inputsMap"); @SuppressWarnings("unchecked") List filesData = (List) sessionData.get("filesData"); @@ -352,7 +376,7 @@ public class AgentChatServiceImpl implements AgentChatService { userMessage.setChatId(chatId); userMessage.setRole("user"); userMessage.setContent(query); - + // 提取系统文件ID列表保存到消息中 if (filesData != null && !filesData.isEmpty()) { List sysFileIds = filesData.stream() @@ -363,7 +387,7 @@ public class AgentChatServiceImpl implements AgentChatService { userMessage.setFiles(sysFileIds); } } - + chatMessageMapper.insertChatMessage(userMessage); // 5. 构建Dify请求 @@ -371,23 +395,12 @@ public class AgentChatServiceImpl implements AgentChatService { chatRequest.setQuery(query); chatRequest.setUser(userId); chatRequest.setResponseMode("streaming"); - Map inputsMap = new HashMap<>(); - chatRequest.setInputs(inputsMap); // Dify API 要求 inputs 必传 - // 处理动态知识库的问题 - if(outer && NonUtils.isNotEmpty(service)){ - TbKnowledge filter = new TbKnowledge(); - filter.setService(service); - filter.setCategory(isGuest?"external":"internal"); - ResultDomain knowledgeRD = knowledgeService.listKnowledges(filter); - List datasets = new ArrayList<>(); - if(knowledgeRD.getSuccess()){ - datasets = knowledgeRD.getDataList().stream().map(TbKnowledge::getDifyDatasetId).toList(); - } - inputsMap.put("datasets", JSON.toJSONString(datasets)); - inputsMap.put("dataset_apikey", difyConfig.getKnowledgeApiKey()); + // 使用从Redis获取的inputsMap,如果为空则创建新的 + if (inputsMap == null) { + inputsMap = new HashMap<>(); } - + chatRequest.setInputs(inputsMap); // Dify API 要求 inputs 必传 if (filesData != null && !filesData.isEmpty()) { chatRequest.setFiles(filesData); 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 index 3fa26272..08c2562d 100644 --- 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 @@ -2,6 +2,7 @@ package org.xyzh.api.ai.dto; import java.io.Serializable; import java.util.List; +import java.util.Map; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -31,4 +32,7 @@ public class ChatPrepareData implements Serializable { @Schema(description = "服务名称") private String service; + + @Schema(description = "智能体输入参数,不同智能体可能需要不同的输入参数") + private Map inputsMap; } From eb15706ccc46571a7f8f2033a4279ebd3fb5c8ab Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Thu, 1 Jan 2026 15:12:29 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E6=80=BB=E7=BB=93?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E6=8E=A5=E5=85=A5=E3=80=81=E5=89=8D?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../postgres/sql/createTableWorkcase.sql | 24 ++ .../database/postgres/sql/initDataConfig.sql | 2 +- .../org/xyzh/ai/client/DifyApiClient.java | 39 ++ .../ai/client/dto/WorkflowRunRequest.java | 34 ++ .../ai/client/dto/WorkflowRunResponse.java | 86 +++++ .../ai/service/impl/AgentChatServiceImpl.java | 227 ++++++++++- .../org/xyzh/api/ai/dto/ChatPrepareData.java | 3 + .../xyzh/api/ai/service/AgentChatService.java | 25 ++ .../workcase/dto/ChatRoomSummaryRequest.java | 27 ++ .../workcase/dto/ChatRoomSummaryResponse.java | 40 ++ .../workcase/dto/TbChatRoomSummaryDTO.java | 46 +++ .../api/workcase/service/AgentService.java | 33 ++ urbanLifelineServ/dify/会话总结.yml | 268 +++++++++++++ .../java/org/xyzh/workcase/config/AiInit.java | 2 +- .../controller/WorkcaseChatController.java | 31 ++ .../mapper/TbChatRoomSummaryMapper.java | 60 +++ .../workcase/service/AgentServiceImpl.java | 355 ++++++++++++++++++ .../mapper/TbChatRoomSummaryMapper.xml | 135 +++++++ .../workcase/src/api/workcase/workcaseChat.ts | 28 +- .../workcase/src/types/workcase/chatRoom.ts | 22 ++ .../ChatRoom/ChatMessage/ChatMessage.scss | 154 +++++++- .../ChatRoom/ChatMessage/ChatMessage.vue | 140 +++++-- 22 files changed, 1738 insertions(+), 43 deletions(-) create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunRequest.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunResponse.java create mode 100644 urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryRequest.java create mode 100644 urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryResponse.java create mode 100644 urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/TbChatRoomSummaryDTO.java create mode 100644 urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/AgentService.java create mode 100644 urbanLifelineServ/dify/会话总结.yml create mode 100644 urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbChatRoomSummaryMapper.java create mode 100644 urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/AgentServiceImpl.java create mode 100644 urbanLifelineServ/workcase/src/main/resources/mapper/TbChatRoomSummaryMapper.xml diff --git a/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql b/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql index 825f42ba..cc2d3d57 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/createTableWorkcase.sql @@ -113,6 +113,30 @@ CREATE INDEX idx_chat_msg_sender ON workcase.tb_chat_room_message(sender_id, sen CREATE INDEX idx_chat_msg_ai ON workcase.tb_chat_room_message(ai_message_id) WHERE ai_message_id IS NOT NULL; COMMENT ON TABLE workcase.tb_chat_room_message IS 'IM聊天消息表,包含AI对话和人工客服消息'; +DROP TABLE IF EXISTS workcase.tb_chat_room_summary CASCADE; +CREATE TABLE workcase.tb_chat_room_summary ( + optsn VARCHAR(50) NOT NULL, -- 流水号 + summary_id VARCHAR(50) NOT NULL, -- 总结ID + room_id VARCHAR(50) NOT NULL, -- 聊天室ID + question TEXT DEFAULT NULL, -- 核心问题 + needs VARCHAR(500)[] DEFAULT '{}', -- 核心诉求数组 + answer TEXT DEFAULT NULL, -- 解决方案 + workcloud VARCHAR(500)[] DEFAULT '{}', -- 词云关键词数组 + message_count INTEGER DEFAULT 0, -- 参与总结的消息数量 + summary_time TIMESTAMPTZ DEFAULT NULL, -- 总结生成时间 + creator VARCHAR(50) NOT 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 (summary_id), + UNIQUE (optsn) +); +CREATE INDEX idx_chat_room_summary_room ON workcase.tb_chat_room_summary(room_id, summary_time DESC); +CREATE INDEX idx_chat_room_summary_time ON workcase.tb_chat_room_summary(summary_time DESC); +COMMENT ON TABLE workcase.tb_chat_room_summary IS '聊天室总结表,保存AI生成的聊天总结分析'; + + -- 4. 视频会议表(Jitsi Meet) -- 记录聊天室内创建的视频会议 DROP TABLE IF EXISTS workcase.tb_video_meeting CASCADE; diff --git a/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql b/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql index 0b3677ed..b011712b 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/initDataConfig.sql @@ -86,7 +86,7 @@ INSERT INTO config.tb_sys_config ( ('CFG-0477', 'cfg_dify_retrieval_threshold', 'dify.knowledge.retrieval.score.threshold','相似度阈值', '0.5', 'DOUBLE', 'input', '检索相似度阈值', NULL, NULL, 'dify', 'mod_agent', 170, 1, '低于此阈值的文档将被过滤(0.0-1.0)', 'system', NULL, NULL, now(), NULL, NULL, false), -- Dify workcase相关智能体配置 ('CFG-0478', 'cfg_dify_workcase_chat', 'dify.workcase.agent.chat','泰豪小电AgentApiKey', 'app-CDKy0wYkPnl6dA6G7eu113Vw', 'String', 'input', '泰豪小电AgentApiKey', NULL, NULL, 'dify', 'mod_agent', 160, 1, '泰豪小电AgentApiKey', 'system', NULL, NULL, now(), NULL, NULL, false), -('CFG-0479', 'cfg_dify_workcase_summary', 'dify.workcase.agent.summary','工单总结AgentApikey', 'app-YMlj2B0m21KpYZPv3YdObi7r', 'String', 'input', '工单总结AgentApikey', NULL, NULL, 'dify', 'mod_agent', 170, 1, '工单总结AgentApikey', 'system', NULL, NULL, now(), NULL, NULL, false), +('CFG-0479', 'cfg_dify_workcase_summary', 'dify.workcase.workflow.summary','工单总结AgentApikey', 'app-YMlj2B0m21KpYZPv3YdObi7r', 'String', 'input', '工单总结AgentApikey', NULL, NULL, 'dify', 'mod_agent', 170, 1, '工单总结AgentApikey', 'system', NULL, NULL, now(), NULL, NULL, false), -- 日志与审计 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 53c84a69..8c7c5780 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 @@ -572,6 +572,45 @@ public class DifyApiClient { } } + // ===================== 工作流 API ===================== + + /** + * 执行工作流(阻塞模式) + */ + public WorkflowRunResponse runWorkflowBlocking(WorkflowRunRequest request, String apiKey) { + String url = difyConfig.getFullApiUrl("/workflows/run"); + + try { + // 设置为阻塞模式 + request.setResponseMode("blocking"); + + String jsonBody = JSON.toJSONString(request); + logger.info("调用Dify工作流接口: url={}, request={}", url, jsonBody); + + Request httpRequest = new Request.Builder() + .url(url) + .header("Authorization", "Bearer " + getApiKey(apiKey)) + .header("Content-Type", "application/json") + .post(RequestBody.create(jsonBody, MediaType.parse("application/json"))) + .build(); + + try (Response response = httpClient.newCall(httpRequest).execute()) { + String responseBody = response.body() != null ? response.body().string() : ""; + + if (!response.isSuccessful()) { + logger.error("工作流执行失败: {} - {}", response.code(), responseBody); + throw new DifyException("工作流执行失败: " + responseBody); + } + + logger.info("工作流执行成功: {}", responseBody); + return JSON.parseObject(responseBody, WorkflowRunResponse.class); + } + } catch (IOException e) { + logger.error("工作流执行异常", e); + throw new DifyException("工作流执行异常: " + e.getMessage(), e); + } + } + /** * 停止对话生成 */ diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunRequest.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunRequest.java new file mode 100644 index 00000000..a2661d3f --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunRequest.java @@ -0,0 +1,34 @@ +package org.xyzh.ai.client.dto; + +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; +import java.util.Map; + +/** + * @description 工作流执行请求 + * @filename WorkflowRunRequest.java + * @author system + * @copyright xyzh + * @since 2026-01-01 + */ +@Data +public class WorkflowRunRequest { + + /** + * 输入变量(Dify 工作流 API 必需字段) + */ + @JSONField(serializeFeatures = com.alibaba.fastjson2.JSONWriter.Feature.WriteMapNullValue) + private Map inputs = new java.util.HashMap<>(); + + /** + * 响应模式:streaming(流式)、blocking(阻塞) + */ + @JSONField(name = "response_mode") + private String responseMode = "blocking"; + + /** + * 用户标识 + */ + private String user; + +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunResponse.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunResponse.java new file mode 100644 index 00000000..b87a6fee --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/client/dto/WorkflowRunResponse.java @@ -0,0 +1,86 @@ +package org.xyzh.ai.client.dto; + +import com.alibaba.fastjson2.annotation.JSONField; +import lombok.Data; +import java.util.Map; + +/** + * @description 工作流执行响应(阻塞模式) + * @filename WorkflowRunResponse.java + * @author system + * @copyright xyzh + * @since 2026-01-01 + */ +@Data +public class WorkflowRunResponse { + + /** + * 工作流执行ID + */ + @JSONField(name = "workflow_run_id") + private String workflowRunId; + + /** + * 任务ID + */ + @JSONField(name = "task_id") + private String taskId; + + /** + * 工作流执行数据 + */ + private WorkflowData data; + + @Data + public static class WorkflowData { + /** + * 执行ID + */ + private String id; + + /** + * 工作流ID + */ + @JSONField(name = "workflow_id") + private String workflowId; + + /** + * 执行状态:running、succeeded、failed、stopped + */ + private String status; + + /** + * 工作流输出结果 + */ + private Map outputs; + + /** + * 错误信息 + */ + private String error; + + /** + * 执行耗时(秒) + */ + @JSONField(name = "elapsed_time") + private Double elapsedTime; + + /** + * 总Token数 + */ + @JSONField(name = "total_tokens") + private Integer totalTokens; + + /** + * 创建时间 + */ + @JSONField(name = "created_at") + private Long createdAt; + + /** + * 完成时间 + */ + @JSONField(name = "finished_at") + private Long finishedAt; + } +} 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 index 70ca191d..f6e77cf2 100644 --- 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 @@ -13,6 +13,9 @@ 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.client.dto.ChatResponse; +import org.xyzh.ai.client.dto.WorkflowRunRequest; +import org.xyzh.ai.client.dto.WorkflowRunResponse; import org.xyzh.ai.config.DifyConfig; import org.xyzh.ai.mapper.TbChatMapper; import org.xyzh.ai.mapper.TbChatMessageMapper; @@ -60,6 +63,7 @@ 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 String WORKFLOW_SESSION_PREFIX = "ai:workflow:session:"; private static final long SESSION_TTL = 5 * 60; @Autowired @@ -325,11 +329,15 @@ public class AgentChatServiceImpl implements AgentChatService { sessionData.put("service", prepareData.getService()); sessionData.put("isGuest", isGuest); sessionData.put("inputsMap", inputsMap); // 存储处理好的 inputs + sessionData.put("appType", prepareData.getAppType()); // 存储应用类型 - String cacheKey = CHAT_SESSION_PREFIX + sessionId; + // 根据应用类型选择不同的Redis key前缀 + String appType = prepareData.getAppType(); + String prefix = "workflow".equals(appType) ? WORKFLOW_SESSION_PREFIX : CHAT_SESSION_PREFIX; + String cacheKey = prefix + sessionId; redisService.set(cacheKey, sessionData, SESSION_TTL, TimeUnit.SECONDS); - logger.info("准备对话会话: sessionId={}, agentId={}", sessionId, agentId); + logger.info("准备{}会话: sessionId={}, agentId={}", appType, sessionId, agentId); return ResultDomain.success("准备成功", sessionId); } @@ -479,6 +487,109 @@ public class AgentChatServiceImpl implements AgentChatService { return emitter; } + @Override + public ResultDomain blockingChatMessageWithSession(String sessionId) { + try { + // 1. 从Redis获取会话数据 + String cacheKey = CHAT_SESSION_PREFIX + sessionId; + @SuppressWarnings("unchecked") + Map sessionData = redisService.get(cacheKey, Map.class); + + if (sessionData == null) { + return ResultDomain.failure("会话已过期"); + } + + // 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") + Map inputsMap = (Map) sessionData.get("inputsMap"); + + @SuppressWarnings("unchecked") + List filesData = (List) sessionData.get("filesData"); + + // 3. 删除已使用的会话数据 + redisService.delete(cacheKey); + + // 4. 保存用户消息(如果有 chatId 的话) + if (StringUtils.hasText(chatId)) { + String userMessageId = IdUtil.getSnowflakeId(); + TbChatMessage userMessage = new TbChatMessage(); + userMessage.setOptsn(IdUtil.getOptsn()); + userMessage.setMessageId(userMessageId); + userMessage.setChatId(chatId); + userMessage.setRole("user"); + userMessage.setContent(query); + + // 提取系统文件ID列表保存到消息中 + if (filesData != null && !filesData.isEmpty()) { + List sysFileIds = filesData.stream() + .map(DifyFileInfo::getSysFileId) + .filter(StringUtils::hasText) + .collect(java.util.stream.Collectors.toList()); + if (!sysFileIds.isEmpty()) { + userMessage.setFiles(sysFileIds); + } + } + + chatMessageMapper.insertChatMessage(userMessage); + } + + // 5. 构建Dify请求 + ChatRequest chatRequest = new ChatRequest(); + chatRequest.setQuery(query); + chatRequest.setUser(userId); + chatRequest.setResponseMode("blocking"); + + // 使用从Redis获取的inputsMap,如果为空则创建新的 + if (inputsMap == null) { + inputsMap = new HashMap<>(); + } + chatRequest.setInputs(inputsMap); // Dify API 要求 inputs 必传 + + if (filesData != null && !filesData.isEmpty()) { + chatRequest.setFiles(filesData); + } + + // 6. 调用Dify阻塞式接口 + logger.info("调用Dify阻塞式接口: agentId={}, userId={}", agentId, userId); + ChatResponse chatResponse = difyApiClient.blockingChat(chatRequest, apiKey); + + if (chatResponse == null || chatResponse.getAnswer() == null) { + return ResultDomain.failure("工作流返回结果为空"); + } + + String answer = chatResponse.getAnswer(); + + // 7. 保存AI回复消息(如果有 chatId 的话) + if (StringUtils.hasText(chatId)) { + String aiMessageId = IdUtil.getSnowflakeId(); + TbChatMessage aiMessage = new TbChatMessage(); + aiMessage.setOptsn(IdUtil.getOptsn()); + aiMessage.setMessageId(aiMessageId); + aiMessage.setDifyMessageId(chatResponse.getMessageId()); + aiMessage.setChatId(chatId); + aiMessage.setRole("ai"); + aiMessage.setContent(answer); + chatMessageMapper.insertChatMessage(aiMessage); + + logger.info("阻塞式对话完成: chatId={}, aiMessageId={}", chatId, aiMessageId); + } else { + logger.info("阻塞式对话完成(无chatId): userId={}", userId); + } + + return ResultDomain.success("对话成功", answer); + + } catch (Exception e) { + logger.error("阻塞式对话异常: sessionId={}", sessionId, e); + return ResultDomain.failure("对话失败: " + e.getMessage()); + } + } + @Override public ResultDomain stopChatMessageByTaskId(TbChat filter, String taskId) { // 1. 获取智能体 @@ -547,4 +658,116 @@ public class AgentChatServiceImpl implements AgentChatService { return ResultDomain.failure("评价失败"); } + + @Override + public ResultDomain runWorkflowWithSession(String sessionId) { + try { + // 1. 从Redis获取会话数据(使用workflow前缀) + String cacheKey = WORKFLOW_SESSION_PREFIX + sessionId; + Map sessionData = redisService.get(cacheKey, Map.class); + + if (sessionData == null) { + return ResultDomain.failure("会话已过期"); + } + + // 2. 解析会话数据 + String agentId = (String) sessionData.get("agentId"); + String userId = (String) sessionData.get("userId"); + String apiKey = (String) sessionData.get("apiKey"); + Map inputsMap = (Map) sessionData.get("inputsMap"); + + // 3. 删除已使用的会话数据 + redisService.delete(cacheKey); + + // 4. 构建工作流请求 + WorkflowRunRequest workflowRequest = new WorkflowRunRequest(); + workflowRequest.setInputs(inputsMap != null ? inputsMap : new HashMap<>()); + workflowRequest.setResponseMode("blocking"); + workflowRequest.setUser(userId); + + logger.info("执行工作流: agentId={}, userId={}, sessionId={}", agentId, userId, sessionId); + + // 5. 调用Dify工作流接口 + WorkflowRunResponse workflowResponse = difyApiClient.runWorkflowBlocking(workflowRequest, apiKey); + + if (workflowResponse == null || workflowResponse.getData() == null) { + return ResultDomain.failure("工作流执行失败:返回结果为空"); + } + + // 6. 检查工作流执行状态 + String status = workflowResponse.getData().getStatus(); + if (!"succeeded".equals(status)) { + String error = workflowResponse.getData().getError(); + logger.error("工作流执行失败: status={}, error={}", status, error); + return ResultDomain.failure("工作流执行失败: " + (error != null ? error : status)); + } + + // 7. 提取outputs + Map outputs = workflowResponse.getData().getOutputs(); + if (outputs == null) { + return ResultDomain.failure("工作流执行失败:outputs为空"); + } + + // 8. 将outputs转为JSON字符串返回 + String outputsJson = JSON.toJSONString(outputs); + logger.info("工作流执行成功: agentId={}, workflowRunId={}", agentId, workflowResponse.getWorkflowRunId()); + + return ResultDomain.success("工作流执行成功", outputsJson); + + } catch (Exception e) { + logger.error("工作流执行异常: sessionId={}", sessionId, e); + return ResultDomain.failure("工作流执行异常: " + e.getMessage()); + } + } + + @Override + public ResultDomain runWorkflowBlocking(String agentId, Map inputs, String userId) { + try { + // 1. 获取智能体信息 + ResultDomain agentResult = agentService.selectAgentById(agentId); + if (!agentResult.getSuccess() || agentResult.getData() == null) { + return ResultDomain.failure("智能体不存在"); + } + TbAgent agent = agentResult.getData(); + + // 2. 构建工作流请求 + WorkflowRunRequest workflowRequest = new WorkflowRunRequest(); + workflowRequest.setInputs(inputs); + workflowRequest.setResponseMode("blocking"); + workflowRequest.setUser(userId); + + logger.info("执行工作流: agentId={}, userId={}, inputs={}", agentId, userId, JSON.toJSONString(inputs)); + + // 3. 调用Dify工作流接口 + WorkflowRunResponse workflowResponse = difyApiClient.runWorkflowBlocking(workflowRequest, agent.getApiKey()); + + if (workflowResponse == null || workflowResponse.getData() == null) { + return ResultDomain.failure("工作流执行失败:返回结果为空"); + } + + // 4. 检查工作流执行状态 + String status = workflowResponse.getData().getStatus(); + if (!"succeeded".equals(status)) { + String error = workflowResponse.getData().getError(); + logger.error("工作流执行失败: status={}, error={}", status, error); + return ResultDomain.failure("工作流执行失败: " + (error != null ? error : status)); + } + + // 5. 提取outputs + Map outputs = workflowResponse.getData().getOutputs(); + if (outputs == null) { + return ResultDomain.failure("工作流执行失败:outputs为空"); + } + + // 6. 将outputs转为JSON字符串返回 + String outputsJson = JSON.toJSONString(outputs); + logger.info("工作流执行成功: agentId={}, workflowRunId={}", agentId, workflowResponse.getWorkflowRunId()); + + return ResultDomain.success("工作流执行成功", outputsJson); + + } catch (Exception e) { + logger.error("工作流执行异常: agentId={}, userId={}", agentId, userId, e); + return ResultDomain.failure("工作流执行异常: " + e.getMessage()); + } + } } 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 index 08c2562d..000fd330 100644 --- 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 @@ -35,4 +35,7 @@ public class ChatPrepareData implements Serializable { @Schema(description = "智能体输入参数,不同智能体可能需要不同的输入参数") private Map inputsMap; + + @Schema(description = "应用类型(chat=对话应用,workflow=工作流应用),默认为chat") + private String appType = "chat"; } 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 e4b7f7b6..97d0b6f3 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 @@ -74,6 +74,13 @@ public interface AgentChatService { */ SseEmitter streamChatMessageWithSse(String sessionId); + /** + * 阻塞式对话 - 使用sessionId进行同步调用,等待完整结果返回 + * @param sessionId 会话标识 + * @return ResultDomain 返回AI回复的完整内容 + */ + ResultDomain blockingChatMessageWithSession(String sessionId); + /** * 停止对话生成(通过Dify TaskID) * @param filter 会话过滤条件(包含agentId, userId, userType) @@ -91,5 +98,23 @@ public interface AgentChatService { */ ResultDomain commentChatMessage(TbChat filter, String messageId, String comment); + // ====================== 工作流执行 ====================== + + /** + * 工作流执行(阻塞模式)- 使用sessionId进行同步调用,等待完整结果返回 + * @param sessionId 会话标识(从prepareChatMessageSession返回) + * @return ResultDomain 返回工作流输出结果的JSON字符串 + */ + ResultDomain runWorkflowWithSession(String sessionId); + + /** + * 执行工作流(阻塞模式)- 直接传入inputs执行工作流 + * @param agentId 智能体ID(用于获取API Key) + * @param inputs 工作流输入参数 + * @param userId 用户标识 + * @return ResultDomain 返回工作流输出结果的JSON字符串 + */ + ResultDomain runWorkflowBlocking(String agentId, java.util.Map inputs, String userId); + } diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryRequest.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryRequest.java new file mode 100644 index 00000000..8d503720 --- /dev/null +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryRequest.java @@ -0,0 +1,27 @@ +package org.xyzh.api.workcase.dto; + +import java.io.Serializable; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @description 聊天室总结请求参数 + * @filename ChatRoomSummaryRequest.java + * @author system + * @copyright xyzh + * @since 2026-01-01 + */ +@Data +@Schema(description = "聊天室总结请求参数") +public class ChatRoomSummaryRequest implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "聊天室ID", required = true) + private String roomId; + + @Schema(description = "是否包含系统消息", defaultValue = "false") + private Boolean includeSystemMessages = false; + + @Schema(description = "是否包含会议消息", defaultValue = "false") + private Boolean includeMeetingMessages = false; +} \ No newline at end of file diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryResponse.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryResponse.java new file mode 100644 index 00000000..1fad457d --- /dev/null +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/ChatRoomSummaryResponse.java @@ -0,0 +1,40 @@ +package org.xyzh.api.workcase.dto; + +import java.io.Serializable; +import java.util.List; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * @description 聊天室总结响应结果 + * @filename ChatRoomSummaryResponse.java + * @author system + * @copyright xyzh + * @since 2026-01-01 + */ +@Data +@Schema(description = "聊天室总结响应结果") +public class ChatRoomSummaryResponse implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "用户提出的核心问题") + private String question; + + @Schema(description = "用户的核心诉求列表") + private List needs; + + @Schema(description = "解决方案或答案") + private String answer; + + @Schema(description = "词云关键词列表") + private List workcloud; + + @Schema(description = "聊天室ID") + private String roomId; + + @Schema(description = "总结生成时间") + private String summaryTime; + + @Schema(description = "参与总结的消息数量") + private Integer messageCount; +} \ No newline at end of file diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/TbChatRoomSummaryDTO.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/TbChatRoomSummaryDTO.java new file mode 100644 index 00000000..b58377ad --- /dev/null +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/dto/TbChatRoomSummaryDTO.java @@ -0,0 +1,46 @@ +package org.xyzh.api.workcase.dto; + +import org.xyzh.common.dto.BaseDTO; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * @description 聊天室总结表对象 + * @filename TbChatRoomSummaryDTO.java + * @author system + * @copyright xyzh + * @since 2026-01-01 + */ +@Data +@Schema(description = "聊天室总结表对象") +public class TbChatRoomSummaryDTO extends BaseDTO { + private static final long serialVersionUID = 1L; + + @Schema(description = "总结ID") + private String summaryId; + + @Schema(description = "聊天室ID") + private String roomId; + + @Schema(description = "核心问题") + private String question; + + @Schema(description = "核心诉求数组") + private List needs; + + @Schema(description = "解决方案") + private String answer; + + @Schema(description = "词云关键词数组") + private List workcloud; + + @Schema(description = "参与总结的消息数量") + private Integer messageCount; + + @Schema(description = "总结生成时间") + private String summaryTime; + +} diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/AgentService.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/AgentService.java new file mode 100644 index 00000000..8da41d02 --- /dev/null +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/AgentService.java @@ -0,0 +1,33 @@ +package org.xyzh.api.workcase.service; + +import org.xyzh.api.workcase.dto.ChatRoomSummaryRequest; +import org.xyzh.api.workcase.dto.ChatRoomSummaryResponse; +import org.xyzh.common.core.domain.ResultDomain; + +/** + * @description 智能体服务接口,提供AI相关的业务功能 + * @filename AgentService.java + * @author system + * @copyright xyzh + * @since 2026-01-01 + */ +public interface AgentService { + + /** + * @description 总结聊天室对话内容 + * @param request 聊天室总结请求参数 + * @return 总结结果 + * @author system + * @since 2026-01-01 + */ + ResultDomain summaryChatRoom(ChatRoomSummaryRequest request); + + /** + * @description 获取聊天室最新的总结 + * @param roomId 聊天室ID + * @return 总结结果 + * @author system + * @since 2026-01-01 + */ + ResultDomain getLatestSummary(String roomId); +} \ No newline at end of file diff --git a/urbanLifelineServ/dify/会话总结.yml b/urbanLifelineServ/dify/会话总结.yml new file mode 100644 index 00000000..d3b472d0 --- /dev/null +++ b/urbanLifelineServ/dify/会话总结.yml @@ -0,0 +1,268 @@ +app: + description: '' + icon: 🤖 + icon_background: '#FFEAD5' + mode: workflow + name: 会话总结 + use_icon_as_answer_icon: false +dependencies: +- current_identifier: null + type: marketplace + value: + marketplace_plugin_unique_identifier: langgenius/siliconflow:0.0.38@4795747d4fca05fee9daf34b1bcc110ffbbfcd9112f5f9e914f90b8b5dd549e5 + version: null +kind: app +version: 0.5.0 +workflow: + conversation_variables: [] + environment_variables: [] + features: + file_upload: + allowed_file_extensions: + - .JPG + - .JPEG + - .PNG + - .GIF + - .WEBP + - .SVG + allowed_file_types: + - image + allowed_file_upload_methods: + - local_file + - remote_url + enabled: false + fileUploadConfig: + audio_file_size_limit: 50 + batch_count_limit: 5 + file_size_limit: 500 + image_file_batch_limit: 10 + image_file_size_limit: 10 + single_chunk_attachment_limit: 10 + video_file_size_limit: 100 + workflow_file_upload_limit: 10 + image: + enabled: false + number_limits: 3 + transfer_methods: + - local_file + - remote_url + number_limits: 3 + opening_statement: '' + retriever_resource: + enabled: true + sensitive_word_avoidance: + enabled: false + speech_to_text: + enabled: false + suggested_questions: [] + suggested_questions_after_answer: + enabled: false + text_to_speech: + enabled: false + language: '' + voice: '' + graph: + edges: + - data: + isInIteration: false + isInLoop: false + sourceType: start + targetType: code + id: 1767170626348-source-1767170986690-target + source: '1767170626348' + sourceHandle: source + target: '1767170986690' + targetHandle: target + type: custom + zIndex: 0 + - data: + isInLoop: false + sourceType: code + targetType: llm + id: 1767170986690-source-1767170825066-target + source: '1767170986690' + sourceHandle: source + target: '1767170825066' + targetHandle: target + type: custom + zIndex: 0 + - data: + isInIteration: false + isInLoop: false + sourceType: llm + targetType: end + id: 1767170825066-source-1767173247791-target + source: '1767170825066' + sourceHandle: source + target: '1767173247791' + targetHandle: target + type: custom + zIndex: 0 + nodes: + - data: + selected: false + title: 用户输入 + type: start + variables: + - default: '' + hint: '' + label: 聊天室对话数据 + max_length: 99999 + options: [] + placeholder: '' + required: true + type: paragraph + variable: chatMessages + height: 109 + id: '1767170626348' + position: + x: -40 + y: 267 + positionAbsolute: + x: -40 + y: 267 + selected: false + sourcePosition: right + targetPosition: left + type: custom + width: 242 + - data: + context: + enabled: true + variable_selector: + - '1767170986690' + - result + model: + completion_params: + temperature: 0.7 + mode: chat + name: Qwen/Qwen2.5-VL-72B-Instruct + provider: langgenius/siliconflow/siliconflow + prompt_template: + - id: fd7bffa7-97b7-4a7e-a47f-04dfbd15eca6 + role: system + text: '# 角色定义 + + 你是一个专业的聊天室总结助手,严格按照要求输出指定格式的JSON内容,不输出任何多余文字、注释、换行。 + + 聊天室角色说明:guest=用户、ai=智能助手、agent=人工客服,聊天消息已按send_time时间正序排列。 + + + # 输出规则(必须严格遵守,违反则任务失败) + + 1. 必须输出标准JSON字符串,仅包含 {"question":"","needs":[""],"answer":""} 三个字段,无其他字段、无多余内容; + + 2. question:提炼用户(guest)的核心问题,无业务问题则填"用户无明确业务问题,仅进行友好问候"; + + 3. needs:提取用户的核心诉求,格式为数组,无诉求则填空数组[],仅保留业务相关诉求,过滤问候语; + + 4. answer:整理有效解答(优先ai/agent回复),无有效解答则填"暂无有效解答,需用户补充更具体的问题或背景信息"; + + 5. JSON中禁止出现换行、多余空格,content中的特殊字符/引号自动转义,确保JSON语法合规。 + + + # 输出格式(唯一合法格式,必须原样输出) + + {"question":"用户提出的问题描述", "needs":["客户诉求1","客户诉求2"],"answer":"解决方案"} + + + # 聊天上下文(完整带角色,已排序) + + {{#context#}}' + selected: false + structured_output: + schema: + additionalProperties: false + properties: + answer: + type: string + needs: + items: + type: string + type: array + question: + type: string + required: + - question + - needs + - answer + type: object + structured_output_enabled: false + title: LLM + type: llm + vision: + enabled: false + height: 88 + id: '1767170825066' + position: + x: 886 + y: 294 + positionAbsolute: + x: 886 + y: 294 + selected: false + sourcePosition: right + targetPosition: left + type: custom + width: 242 + - data: + code: "import json\n\ndef main(chatMessages: str):\n # 核心:JSON字符串 转 Python对象/对象数组\n\ + \ obj_array = json.loads(chatMessages)\n # 返回转换后的对象数组,key自定义为你后续要用的名称(示例用result)\n\ + \ # {\"senderType\":\"ai\\guest\\staff\",\"content\":\"xxx\",\"send_time\"\ + :\"xxx\"}\n obj_array_sorted = sorted(obj_array, key=lambda x: x[\"send_time\"\ + ])\n return {\n \"result\": obj_array_sorted\n }" + code_language: python3 + outputs: + result: + children: null + type: array[object] + selected: false + title: jsonstring转对象数组 + type: code + variables: + - value_selector: + - '1767170626348' + - chatMessages + value_type: string + variable: chatMessages + height: 52 + id: '1767170986690' + position: + x: 301 + y: 308 + positionAbsolute: + x: 301 + y: 308 + selected: false + sourcePosition: right + targetPosition: left + type: custom + width: 242 + - data: + outputs: + - value_selector: + - '1767170825066' + - text + value_type: string + variable: text + selected: false + title: 输出 + type: end + height: 88 + id: '1767173247791' + position: + x: 1192 + y: 294 + positionAbsolute: + x: 1192 + y: 294 + selected: true + sourcePosition: right + targetPosition: left + type: custom + width: 242 + viewport: + x: -661 + y: 93.5 + zoom: 1 + rag_pipeline_variables: [] diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java index da407be1..175e00d2 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/config/AiInit.java @@ -86,7 +86,7 @@ public class AiInit { } } - String summaryAgentApiKey = sysConfigService.getStringConfig("dify.workcase.agent.summary"); + String summaryAgentApiKey = sysConfigService.getStringConfig("dify.workcase.workflow.summary"); TbAgent summaryAgent = new TbAgent(); summaryAgent.setIsOuter(true); summaryAgent.setName("工单总结"); diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatController.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatController.java index 2bc724bf..56358d3b 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatController.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatController.java @@ -21,6 +21,9 @@ import org.xyzh.api.workcase.dto.TbChatRoomMessageDTO; import org.xyzh.api.workcase.dto.TbCustomerServiceDTO; import org.xyzh.api.workcase.dto.TbVideoMeetingDTO; import org.xyzh.api.workcase.dto.TbWordCloudDTO; +import org.xyzh.api.workcase.dto.ChatRoomSummaryRequest; +import org.xyzh.api.workcase.dto.ChatRoomSummaryResponse; +import org.xyzh.api.workcase.service.AgentService; import org.xyzh.api.workcase.service.ChatRoomService; import org.xyzh.api.workcase.service.WorkcaseChatService; import org.xyzh.api.workcase.vo.ChatMemberVO; @@ -68,6 +71,9 @@ public class WorkcaseChatController { @Autowired private ChatRoomService chatRoomService; + @Autowired + private AgentService agentService; + @Autowired private JwtTokenUtil jwtTokenUtil; @@ -243,6 +249,31 @@ public class WorkcaseChatController { return chatRoomService.deleteMessage(messageId); } + @Operation(summary = "获取聊天室最新总结") + @PreAuthorize("hasAuthority('workcase:room:view')") + @GetMapping("/room/{roomId}/summary") + public ResultDomain getLatestSummary(@PathVariable(value = "roomId") String roomId) { + return agentService.getLatestSummary(roomId); + } + + @Operation(summary = "生成聊天室对话总结") + @PreAuthorize("hasAuthority('workcase:room:view')") + @PostMapping("/room/{roomId}/summary") + public ResultDomain summaryChatRoom( + @PathVariable(value = "roomId") String roomId, + @RequestBody(required = false) ChatRoomSummaryRequest request) { + // 如果请求体为空,创建一个默认的请求对象 + if (request == null) { + request = new ChatRoomSummaryRequest(); + } + + // 设置聊天室ID + request.setRoomId(roomId); + + // 调用服务层进行总结 + return agentService.summaryChatRoom(request); + } + // ========================= 客服人员管理 ========================= @Operation(summary = "添加客服人员") diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbChatRoomSummaryMapper.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbChatRoomSummaryMapper.java new file mode 100644 index 00000000..40e2b20a --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbChatRoomSummaryMapper.java @@ -0,0 +1,60 @@ +package org.xyzh.workcase.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.xyzh.api.workcase.dto.TbChatRoomSummaryDTO; +import org.xyzh.common.core.page.PageParam; + +/** + * @description 聊天室总结数据访问层 + * @filename TbChatRoomSummaryMapper.java + * @author system + * @copyright xyzh + * @since 2026-01-01 + */ +@Mapper +public interface TbChatRoomSummaryMapper { + + /** + * 插入聊天室总结 + */ + int insertChatRoomSummary(TbChatRoomSummaryDTO summary); + + /** + * 更新聊天室总结(只更新非null字段) + */ + int updateChatRoomSummary(TbChatRoomSummaryDTO summary); + + /** + * 根据ID查询聊天室总结 + */ + TbChatRoomSummaryDTO selectChatRoomSummaryById(@Param("summaryId") String summaryId); + + /** + * 根据聊天室ID查询最新一条总结 + */ + TbChatRoomSummaryDTO selectLatestSummaryByRoomId(@Param("roomId") String roomId); + + /** + * 查询聊天室总结列表 + */ + List selectChatRoomSummaryList(@Param("filter") TbChatRoomSummaryDTO filter); + + /** + * 分页查询聊天室总结 + */ + List selectChatRoomSummaryPage(@Param("filter") TbChatRoomSummaryDTO filter, @Param("pageParam") PageParam pageParam); + + /** + * 统计聊天室总结数量 + */ + long countChatRoomSummaries(@Param("filter") TbChatRoomSummaryDTO filter); + + /** + * 删除聊天室总结(逻辑删除) + */ + int deleteChatRoomSummary(@Param("summaryId") String summaryId); + +} diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/AgentServiceImpl.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/AgentServiceImpl.java new file mode 100644 index 00000000..a2139efc --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/AgentServiceImpl.java @@ -0,0 +1,355 @@ +package org.xyzh.workcase.service; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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 com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; + +import org.xyzh.api.ai.dto.ChatPrepareData; +import org.xyzh.api.ai.dto.TbAgent; +import org.xyzh.api.ai.service.AgentChatService; +import org.xyzh.api.system.service.SysConfigService; +import org.xyzh.api.workcase.dto.ChatRoomSummaryRequest; +import org.xyzh.api.workcase.dto.ChatRoomSummaryResponse; +import org.xyzh.api.workcase.dto.TbChatRoomMessageDTO; +import org.xyzh.api.workcase.dto.TbChatRoomSummaryDTO; +import org.xyzh.api.workcase.dto.TbWordCloudDTO; +import org.xyzh.api.workcase.service.AgentService; +import org.xyzh.api.workcase.vo.ChatRoomMessageVO; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.utils.id.IdUtil; +import org.xyzh.workcase.mapper.TbChatMessageMapper; +import org.xyzh.workcase.mapper.TbChatRoomSummaryMapper; +import org.xyzh.workcase.mapper.TbWordCloudMapper; + +/** + * @description 智能体服务实现类,提供AI相关的业务功能 + * @filename AgentServiceImpl.java + * @author system + * @copyright xyzh + * @since 2026-01-01 + */ +@DubboService(version = "1.0.0", group = "workcase", timeout = 30000, retries = 0) +public class AgentServiceImpl implements AgentService { + private static final Logger logger = LoggerFactory.getLogger(AgentServiceImpl.class); + + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + // 系统配置键名 + private static final String CONFIG_KEY_SUMMARY_API = "dify.workcase.workflow.summary"; + + @Autowired + private TbChatMessageMapper chatMessageMapper; + + @Autowired + private TbWordCloudMapper wordCloudMapper; + + @Autowired + private TbChatRoomSummaryMapper chatRoomSummaryMapper; + + @DubboReference(version = "1.0.0", group = "system", timeout = 30000, retries = 0) + private SysConfigService sysConfigService; + + @DubboReference(version = "1.0.0", group = "ai", timeout = 30000, retries = 0) + private org.xyzh.api.ai.service.AgentService aiAgentService; + + @DubboReference(version = "1.0.0", group = "ai", timeout = 60000, retries = 0) + private AgentChatService agentChatService; + + @Override + public ResultDomain summaryChatRoom(ChatRoomSummaryRequest request) { + try { + logger.info("开始总结聊天室: roomId={}", request.getRoomId()); + + // 1. 从系统配置获取API Key + String apiKey = sysConfigService.getStringConfig(CONFIG_KEY_SUMMARY_API); + if (apiKey == null || apiKey.isEmpty()) { + logger.error("未配置聊天室总结工作流的API Key: {}", CONFIG_KEY_SUMMARY_API); + return ResultDomain.failure("系统未配置聊天室总结功能"); + } + + // 2. 根据API Key查询对应的Agent + TbAgent agentFilter = new TbAgent(); + agentFilter.setApiKey(apiKey); + ResultDomain agentResult = aiAgentService.getAgentList(agentFilter); + if (!agentResult.getSuccess() || agentResult.getDataList() == null || agentResult.getDataList().isEmpty()) { + logger.error("未找到API Key对应的智能体: {}", apiKey); + return ResultDomain.failure("未找到对应的智能体配置"); + } + + TbAgent agent = agentResult.getDataList().get(0); + logger.info("找到智能体: agentId={}, name={}", agent.getAgentId(), agent.getName()); + + // 3. 获取聊天室的所有消息 + TbChatRoomMessageDTO filter = new TbChatRoomMessageDTO(); + filter.setRoomId(request.getRoomId()); + + List messages = chatMessageMapper.selectChatMessageList(filter); + if (messages == null || messages.isEmpty()) { + return ResultDomain.failure("聊天室没有消息"); + } + + // 4. 过滤消息(根据请求参数) + List> filteredMessages = new ArrayList<>(); + for (ChatRoomMessageVO message : messages) { + String messageType = message.getMessageType(); + + // 根据请求参数决定是否包含系统消息和会议消息 + boolean shouldInclude = true; + if ("system".equals(messageType) && !Boolean.TRUE.equals(request.getIncludeSystemMessages())) { + shouldInclude = false; + } + if ("meeting".equals(messageType) && !Boolean.TRUE.equals(request.getIncludeMeetingMessages())) { + shouldInclude = false; + } + + if (shouldInclude) { + Map msgMap = new HashMap<>(); + msgMap.put("senderType", message.getSenderType()); // guest/ai/agent + msgMap.put("content", message.getContent()); + msgMap.put("send_time", DATE_FORMAT.format(message.getSendTime())); + filteredMessages.add(msgMap); + } + } + + if (filteredMessages.isEmpty()) { + return ResultDomain.failure("聊天室没有有效的对话消息"); + } + + logger.info("聊天室 {} 共有 {} 条有效消息", request.getRoomId(), filteredMessages.size()); + + // 5. 将消息列表转换为JSON字符串 + String chatMessagesJson = JSON.toJSONString(filteredMessages); + + // 6. 准备调用工作流的参数 + ChatPrepareData prepareData = new ChatPrepareData(); + prepareData.setAgentId(agent.getAgentId()); + prepareData.setQuery("总结聊天内容"); + prepareData.setUserId("system_summary"); + prepareData.setUserType(true); // 作为员工身份调用 + prepareData.setAppType("workflow"); // 设置为workflow类型 + + // 7. 设置工作流的输入参数 + Map inputsMap = new HashMap<>(); + inputsMap.put("chatMessages", chatMessagesJson); // 工作流期望的输入参数名 + prepareData.setInputsMap(inputsMap); + + logger.info("准备工作流会话,输入参数: chatMessages长度={}", chatMessagesJson.length()); + + // 8. 调用准备会话 + ResultDomain prepareResult = agentChatService.prepareChatMessageSession(prepareData); + if (!prepareResult.getSuccess()) { + logger.error("准备工作流会话失败: {}", prepareResult.getMessage()); + return ResultDomain.failure("准备会话失败: " + prepareResult.getMessage()); + } + + String sessionId = prepareResult.getData(); + logger.info("工作流会话准备成功: sessionId={}", sessionId); + + // 9. 调用工作流执行,获取完整结果 + ResultDomain workflowResult = agentChatService.runWorkflowWithSession(sessionId); + if (!workflowResult.getSuccess()) { + logger.error("工作流执行失败: {}", workflowResult.getMessage()); + return ResultDomain.failure("总结失败: " + workflowResult.getMessage()); + } + + String outputsJson = workflowResult.getData(); + logger.debug("工作流返回结果: {}", outputsJson); + + // 10. 解析工作流返回的outputs(JSON格式) + JSONObject outputsObject; + try { + outputsObject = JSON.parseObject(outputsJson); + } catch (Exception e) { + logger.error("解析工作流输出失败: {}", outputsJson, e); + return ResultDomain.failure("工作流输出格式错误"); + } + + // 11. 从outputs中获取text字段(工作流的输出节点) + String text = outputsObject.getString("text"); + if (text == null || text.isEmpty()) { + logger.error("工作流输出中没有text字段: {}", outputsJson); + return ResultDomain.failure("工作流输出缺少text字段"); + } + + // 12. 解析text中的JSON结果 + JSONObject resultJson; + try { + resultJson = JSON.parseObject(text); + } catch (Exception e) { + logger.error("解析工作流text字段失败: {}", text, e); + return ResultDomain.failure("工作流返回结果格式错误"); + } + + // 13. 构建响应对象 + ChatRoomSummaryResponse response = new ChatRoomSummaryResponse(); + response.setRoomId(request.getRoomId()); + response.setQuestion(resultJson.getString("question")); + + // 安全获取needs数组 + List needs = resultJson.getList("needs", String.class); + response.setNeeds(needs != null ? needs : new ArrayList<>()); + + response.setAnswer(resultJson.getString("answer")); + + // 安全获取workcloud数组 + List workcloud = resultJson.getList("workcloud", String.class); + response.setWorkcloud(workcloud != null ? workcloud : new ArrayList<>()); + + response.setSummaryTime(DATE_FORMAT.format(new java.util.Date())); + response.setMessageCount(filteredMessages.size()); + + // 14. 保存词云数据到数据库 + if (workcloud != null && !workcloud.isEmpty()) { + saveWordCloudData(request.getRoomId(), workcloud); + } + + // 15. 保存总结数据到数据库 + saveChatRoomSummary(request.getRoomId(), resultJson, filteredMessages.size()); + + logger.info("聊天室总结完成: roomId={}, question={}, wordcloud数量={}", + request.getRoomId(), response.getQuestion(), workcloud != null ? workcloud.size() : 0); + + return ResultDomain.success("总结成功", response); + + } catch (Exception e) { + logger.error("总结聊天室异常: roomId={}", request.getRoomId(), e); + return ResultDomain.failure("总结失败: " + e.getMessage()); + } + } + + /** + * 保存词云数据到数据库 + * @param roomId 聊天室ID + * @param wordList 词云关键词列表 + */ + private void saveWordCloudData(String roomId, List wordList) { + try { + String today = new SimpleDateFormat("yyyy-MM-dd").format(new java.util.Date()); + + for (String word : wordList) { + if (word == null || word.trim().isEmpty()) { + continue; + } + + // 查询是否已存在该词条(同一天、同一分类) + TbWordCloudDTO filter = new TbWordCloudDTO(); + filter.setWord(word.trim()); + filter.setCategory("chatroom_summary"); // 分类:聊天室总结 + filter.setStatDate(today); + + TbWordCloudDTO existing = wordCloudMapper.selectWordCloudOne(filter); + + if (existing != null) { + // 已存在,增加词频 + wordCloudMapper.incrementFrequency(existing.getWordId(), 1); + logger.debug("词云词频递增: word={}, wordId={}", word, existing.getWordId()); + } else { + // 不存在,插入新词条 + TbWordCloudDTO newWord = new TbWordCloudDTO(); + newWord.setWordId(IdUtil.getSnowflakeId()); + newWord.setOptsn(IdUtil.getOptsn()); + newWord.setWord(word.trim()); + newWord.setFrequency("1"); + newWord.setCategory("chatroom_summary"); + newWord.setStatDate(today); + + wordCloudMapper.insertWordCloud(newWord); + logger.debug("插入新词云: word={}, wordId={}", word, newWord.getWordId()); + } + } + + logger.info("词云数据保存完成: roomId={}, 词条数量={}", roomId, wordList.size()); + } catch (Exception e) { + logger.error("保存词云数据失败: roomId={}", roomId, e); + // 词云保存失败不影响总结流程,仅记录日志 + } + } + + /** + * 保存聊天室总结数据到数据库 + * @param roomId 聊天室ID + * @param resultJson 工作流返回的JSON结果 + * @param messageCount 参与总结的消息数量 + */ + private void saveChatRoomSummary(String roomId, JSONObject resultJson, int messageCount) { + try { + TbChatRoomSummaryDTO summary = new TbChatRoomSummaryDTO(); + summary.setSummaryId(IdUtil.getSnowflakeId()); + summary.setOptsn(IdUtil.getOptsn()); + summary.setRoomId(roomId); + summary.setQuestion(resultJson.getString("question")); + + // 获取needs数组并转换为String[] + List needsList = resultJson.getList("needs", String.class); + if (needsList != null && !needsList.isEmpty()) { + summary.setNeeds(needsList); + } else { + summary.setNeeds(new ArrayList<>()); + } + + summary.setAnswer(resultJson.getString("answer")); + + // 获取workcloud数组并转换为String[] + List workcloudList = resultJson.getList("workcloud", String.class); + if (workcloudList != null && !workcloudList.isEmpty()) { + summary.setWorkcloud(workcloudList); + } else { + summary.setWorkcloud(new ArrayList<>()); + } + + summary.setMessageCount(messageCount); + summary.setSummaryTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date())); + summary.setCreator("system"); + + chatRoomSummaryMapper.insertChatRoomSummary(summary); + + logger.info("聊天室总结数据保存成功: roomId={}, summaryId={}", roomId, summary.getSummaryId()); + } catch (Exception e) { + logger.error("保存聊天室总结数据失败: roomId={}", roomId, e); + // 总结数据保存失败不影响主流程,仅记录日志 + } + } + + @Override + public ResultDomain getLatestSummary(String roomId) { + try { + logger.info("查询聊天室最新总结: roomId={}", roomId); + + // 查询最新的总结记录 + TbChatRoomSummaryDTO summary = chatRoomSummaryMapper.selectLatestSummaryByRoomId(roomId); + + if (summary == null) { + logger.info("未找到聊天室总结: roomId={}", roomId); + return ResultDomain.failure("暂无总结数据"); + } + + // 构建响应对象 + ChatRoomSummaryResponse response = new ChatRoomSummaryResponse(); + response.setRoomId(summary.getRoomId()); + response.setQuestion(summary.getQuestion()); + response.setNeeds(summary.getNeeds()); + response.setAnswer(summary.getAnswer()); + response.setWorkcloud(summary.getWorkcloud()); + response.setSummaryTime(summary.getSummaryTime()); + response.setMessageCount(summary.getMessageCount()); + + logger.info("查询聊天室总结成功: roomId={}, summaryId={}", roomId, summary.getSummaryId()); + return ResultDomain.success("查询成功", response); + + } catch (Exception e) { + logger.error("查询聊天室总结异常: roomId={}", roomId, e); + return ResultDomain.failure("查询失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/urbanLifelineServ/workcase/src/main/resources/mapper/TbChatRoomSummaryMapper.xml b/urbanLifelineServ/workcase/src/main/resources/mapper/TbChatRoomSummaryMapper.xml new file mode 100644 index 00000000..1ff38749 --- /dev/null +++ b/urbanLifelineServ/workcase/src/main/resources/mapper/TbChatRoomSummaryMapper.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + summary_id, room_id, question, needs, answer, workcloud, message_count, summary_time, + optsn, creator, create_time, update_time, delete_time, deleted + + + + INSERT INTO workcase.tb_chat_room_summary ( + optsn, summary_id, room_id, creator + , question + , needs + , answer + , workcloud + , message_count + , summary_time + ) VALUES ( + #{optsn}, #{summaryId}, #{roomId}, #{creator} + , #{question} + , #{needs, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler} + , #{answer} + , #{workcloud, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler} + , #{messageCount} + , #{summaryTime}::timestamptz + ) + + + + UPDATE workcase.tb_chat_room_summary + + question = #{question}, + needs = #{needs, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler}, + answer = #{answer}, + workcloud = #{workcloud, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler}, + message_count = #{messageCount}, + summary_time = #{summaryTime}::timestamptz, + update_time = now() + + WHERE summary_id = #{summaryId} + + + + + + + + + + + + + + UPDATE workcase.tb_chat_room_summary + SET deleted = true, delete_time = now() + WHERE summary_id = #{summaryId} + + + diff --git a/urbanLifelineWeb/packages/workcase/src/api/workcase/workcaseChat.ts b/urbanLifelineWeb/packages/workcase/src/api/workcase/workcaseChat.ts index 65efbe4a..70fc50d9 100644 --- a/urbanLifelineWeb/packages/workcase/src/api/workcase/workcaseChat.ts +++ b/urbanLifelineWeb/packages/workcase/src/api/workcase/workcaseChat.ts @@ -11,7 +11,9 @@ import type { ChatMemberVO, ChatRoomMessageVO, CustomerServiceVO, - VideoMeetingVO + VideoMeetingVO, + ChatRoomSummaryRequest, + ChatRoomSummaryResponse } from '@/types/workcase' /** @@ -286,5 +288,29 @@ export const workcaseChatAPI = { params: { commentLevel } }) return response.data + }, + + // ====================== 聊天室总结管理 ====================== + + /** + * 获取聊天室最新总结 + * @param roomId 聊天室ID + */ + async getLatestSummary(roomId: string): Promise> { + const response = await api.get(`${this.baseUrl}/room/${roomId}/summary`) + return response.data + }, + + /** + * 生成聊天室对话总结 + * @param request 总结请求参数 + */ + async summaryChatRoom(request: ChatRoomSummaryRequest): Promise> { + const { roomId, ...body } = request + const response = await api.post( + `${this.baseUrl}/room/${roomId}/summary`, + Object.keys(body).length > 0 ? body : {} + ) + return response.data } } diff --git a/urbanLifelineWeb/packages/workcase/src/types/workcase/chatRoom.ts b/urbanLifelineWeb/packages/workcase/src/types/workcase/chatRoom.ts index b63298ec..b2870b18 100644 --- a/urbanLifelineWeb/packages/workcase/src/types/workcase/chatRoom.ts +++ b/urbanLifelineWeb/packages/workcase/src/types/workcase/chatRoom.ts @@ -313,4 +313,26 @@ export interface CreateMeetingParam { export interface MarkReadParam { roomId: string messageIds?: string[] +} + +/** + * 聊天室总结请求参数 + */ +export interface ChatRoomSummaryRequest { + roomId: string + includeSystemMessages?: boolean + includeMeetingMessages?: boolean +} + +/** + * 聊天室总结响应结果 + */ +export interface ChatRoomSummaryResponse { + question?: string + needs?: string[] + answer?: string + workcloud?: string[] + roomId?: string + summaryTime?: string + messageCount?: number } \ No newline at end of file diff --git a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.scss b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.scss index df291511..e0ce76ba 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.scss +++ b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.scss @@ -174,26 +174,166 @@ background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; - padding: 20px; + padding: 24px; + min-height: 420px; +} + +.summary-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 400px; + gap: 16px; + color: #6b7280; + font-size: 14px; + + .loading-spinner { + width: 48px; + height: 48px; + border: 4px solid #e5e7eb; + border-top-color: #4b87ff; + border-radius: 50%; + animation: spin 1s linear infinite; + } +} + +@keyframes spin { + to { transform: rotate(360deg); } } .summary-content { display: flex; flex-direction: column; - gap: 16px; + gap: 24px; +} + +.summary-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 16px; + border-bottom: 2px solid #e5e7eb; + + h3 { + font-size: 18px; + font-weight: 700; + color: #111827; + margin: 0; + } + + .regenerate-btn { + padding: 6px 16px; + background: #fff; + color: #4b87ff; + border: 1px solid #4b87ff; + border-radius: 6px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #4b87ff; + color: #fff; + } + } } .summary-section { .summary-title { font-size: 14px; - font-weight: 700; - color: #111827; - margin-bottom: 8px; + font-weight: 600; + color: #374151; + margin-bottom: 12px; + padding-left: 12px; + border-left: 3px solid #4b87ff; } .summary-text { font-size: 14px; - color: #374151; - line-height: 1.6; + color: #1f2937; + line-height: 1.8; + padding: 16px; + background: #f9fafb; + border-radius: 8px; + white-space: pre-wrap; + } + + .summary-list { + margin: 0; + padding-left: 24px; + + li { + font-size: 14px; + color: #1f2937; + line-height: 2; + margin-bottom: 8px; + position: relative; + + &:last-child { + margin-bottom: 0; + } + + &::marker { + color: #4b87ff; + } + } + } +} + +.summary-meta { + display: flex; + gap: 16px; + padding: 12px 16px; + background: #fef3c7; + border-radius: 8px; + font-size: 12px; + color: #92400e; + margin-top: 8px; + + span { + &:not(:last-child)::after { + content: '•'; + margin-left: 16px; + color: #d97706; + } + } +} + +.summary-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 400px; + gap: 16px; + + .empty-icon { + font-size: 64px; + opacity: 0.5; + } + + .empty-text { + font-size: 14px; + color: #9ca3af; + } + + .generate-btn { + padding: 10px 24px; + background: #4b87ff; + color: #fff; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: #3b77ef; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(75, 135, 255, 0.3); + } } } diff --git a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.vue b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.vue index d87f9460..7b2989c9 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.vue +++ b/urbanLifelineWeb/packages/workcase/src/views/public/ChatRoom/ChatMessage/ChatMessage.vue @@ -81,19 +81,55 @@
-
-
-
问题概述
+ +
+
+ 正在生成对话总结... +
+ + +
+
+

对话总结

+ +
+ +
+
核心问题
- {{ summary.overview || '暂无概述' }} + {{ summaryData.question }}
-
-
客户诉求
+ +
+
核心诉求
+
    +
  • {{ need }}
  • +
+
+ +
+
解决方案
- {{ summary.demand || '暂无诉求' }} + {{ summaryData.answer }}
+ +
+ + 基于 {{ summaryData.messageCount }} 条消息生成 + + + 生成时间:{{ summaryData.summaryTime }} + +
+
+ + +
+
📝
+
暂无对话总结
+
@@ -101,13 +137,9 @@ From b53faca1206f7e1e1a9a0fa19ea3680c96708dc4 Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Thu, 1 Jan 2026 16:19:55 +0800 Subject: [PATCH 4/6] =?UTF-8?q?overview=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/workcase/service/ChatRoomService.java | 8 + .../workcase/service/WordCloudService.java | 29 ++ .../api/workcase/service/WorkcaseService.java | 16 ++ .../java/org/xyzh/common/dto/BaseDTO.java | 3 + .../controller/WorkcaseChatController.java | 9 +- .../controller/WorkcaseController.java | 46 ++++ .../workcase/mapper/TbWorkcaseMapper.java | 2 + .../workcase/service/ChatRoomServiceImpl.java | 6 + .../workcase/service/WorkcaseServiceImpl.java | 12 + .../resources/mapper/TbChatRoomMapper.xml | 3 + .../resources/mapper/TbWordCloudMapper.xml | 3 + .../resources/mapper/TbWorkcaseMapper.xml | 9 + .../workcase/src/api/workcase/workcase.ts | 20 ++ .../workcase/src/api/workcase/workcaseChat.ts | 11 + .../workcase/src/assets/css/common.scss | 1 + .../views/admin/overview/OverviewView.scss | 32 ++- .../src/views/admin/overview/OverviewView.vue | 255 +++++++++++++++--- 17 files changed, 425 insertions(+), 40 deletions(-) create mode 100644 urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WordCloudService.java diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/ChatRoomService.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/ChatRoomService.java index 3389b485..03867adc 100644 --- a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/ChatRoomService.java +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/ChatRoomService.java @@ -72,6 +72,14 @@ public interface ChatRoomService { */ ResultDomain getChatRoomPage(PageRequest pageRequest, String userId); + /** + * @description 统计聊天室数量 + * @param filter 筛选条件 + * @author yslg + * @since 2026-01-01 + */ + ResultDomain countChatRooms(TbChatRoomDTO filter); + // ========================= 聊天室成员管理 ========================== /** diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WordCloudService.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WordCloudService.java new file mode 100644 index 00000000..14b71287 --- /dev/null +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WordCloudService.java @@ -0,0 +1,29 @@ +package org.xyzh.api.workcase.service; + +import org.xyzh.api.workcase.dto.TbWordCloudDTO; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageRequest; + +/** + * @description 词云服务接口 + * @filename WordCloudService.java + * @author yslg + * @copyright xyzh + * @since 2025-12-19 + */ +public interface WordCloudService { + + /** + * 查询词云列表 + * @param filter 筛选条件 + * @return 词云列表 + */ + ResultDomain getWordCloudList(TbWordCloudDTO filter); + + /** + * 分页查询词云 + * @param pageRequest 分页请求 + * @return 词云分页数据 + */ + ResultDomain getWordCloudPage(PageRequest pageRequest); +} diff --git a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseService.java b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseService.java index 1855263a..34eee9dd 100644 --- a/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseService.java +++ b/urbanLifelineServ/apis/api-workcase/src/main/java/org/xyzh/api/workcase/service/WorkcaseService.java @@ -55,6 +55,22 @@ public interface WorkcaseService { */ ResultDomain getWorkcasePage(PageRequest pageRequest); + /** + * @description 统计各个类型的工单数量 + * @param filter + * @author yslg + * @since 2026-01-01 + */ + ResultDomain countWorkcasesByType(TbWorkcaseDTO filter); + + /** + * @description 统计工单数量 + * @param filter + * @author yslg + * @since 2026-01-01 + */ + ResultDomain countWorkcases(TbWorkcaseDTO filter); + /** * @description 获取工单详情 * @param workcaseId diff --git a/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/dto/BaseDTO.java b/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/dto/BaseDTO.java index 65e207c8..1cc25646 100644 --- a/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/dto/BaseDTO.java +++ b/urbanLifelineServ/common/common-dto/src/main/java/org/xyzh/common/dto/BaseDTO.java @@ -50,6 +50,9 @@ public class BaseDTO implements Serializable { @Schema(description = "数量限制") private Integer limit; + @Schema(description = "统计数量") + private Integer count; + @Schema(description = "开始时间") private Date startTime; diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatController.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatController.java index 56358d3b..e96aec9b 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatController.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseChatController.java @@ -162,7 +162,7 @@ public class WorkcaseChatController { if (!vr.isValid()) { return ResultDomain.failure(vr.getAllErrors()); } - + LoginDomain loginDomain = LoginUtil.getCurrentLogin(); String userId = loginDomain.getUser().getUserId(); if("guest".equals(loginDomain.getUser().getStatus())){ @@ -171,6 +171,13 @@ public class WorkcaseChatController { return chatRoomService.getChatRoomPage(pageRequest, userId); } + @Operation(summary = "统计聊天室数量") + @PreAuthorize("hasAuthority('workcase:room:view')") + @PostMapping("/room/count") + public ResultDomain countChatRooms(@RequestBody TbChatRoomDTO filter) { + return chatRoomService.countChatRooms(filter); + } + // ========================= ChatRoom成员管理 ========================= @Operation(summary = "添加聊天室成员") diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseController.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseController.java index e746e1d5..c6903cdd 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseController.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/controller/WorkcaseController.java @@ -19,6 +19,7 @@ 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.core.page.PageRequest; +import org.xyzh.common.utils.validation.ValidationParam; import org.xyzh.common.utils.validation.ValidationResult; import org.xyzh.common.utils.validation.ValidationUtils; @@ -26,6 +27,8 @@ import com.alibaba.fastjson2.JSONObject; import io.swagger.v3.oas.annotations.Operation; import java.util.Arrays; +import java.util.Date; + import io.swagger.v3.oas.annotations.tags.Tag; /** @@ -116,6 +119,49 @@ public class WorkcaseController { return workcaseService.getWorkcasePage(pageRequest); } + @Operation(summary = "查询工单问题统计") + @PreAuthorize("hasAuthority('workcase:ticket:view')") + @PostMapping("/category/count") + public ResultDomain countWorkcasesByType(@RequestBody TbWorkcaseDTO workcase) { + ValidationResult vr = ValidationUtils.validate(workcase, Arrays.asList( + ValidationParam.builder() + .fieldName("startTime") + .fieldLabel("统计开始时间") + .required() + .build(), + // 校验结束时间不为空 + ValidationParam.builder() + .fieldName("endTime") + .fieldLabel("统计结束时间") + .required() + .build(), + // 校验开始时间小于结束时间(使用 fieldCompare 比较两个字段) + ValidationUtils.fieldCompare( + "startTime", + "endTime", + "统计时间", + (startTime, endTime) -> { + if (startTime instanceof Date && endTime instanceof Date) { + return ((Date) startTime).before((Date) endTime); + } + return true; + }, + "统计开始时间不能晚于结束时间" + ) + )); + if (!vr.isValid()) { + return ResultDomain.failure(vr.getAllErrors()); + } + return workcaseService.countWorkcasesByType(workcase); + } + + @Operation(summary = "统计工单数量") + @PreAuthorize("hasAuthority('workcase:ticket:view')") + @PostMapping("/count") + public ResultDomain countWorkcases(@RequestBody TbWorkcaseDTO workcase) { + return workcaseService.countWorkcases(workcase); + } + // ========================= CRM同步接口 ========================= @Operation(summary = "同步工单到CRM") diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbWorkcaseMapper.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbWorkcaseMapper.java index 99f5f55f..9e52ab0b 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbWorkcaseMapper.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/mapper/TbWorkcaseMapper.java @@ -52,4 +52,6 @@ public interface TbWorkcaseMapper { */ long countWorkcases(@Param("filter") TbWorkcaseDTO filter); + List countWorkcasesByType(@Param("filter") TbWorkcaseDTO filter); + } diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/ChatRoomServiceImpl.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/ChatRoomServiceImpl.java index 03d5d3cd..294af30e 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/ChatRoomServiceImpl.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/ChatRoomServiceImpl.java @@ -219,6 +219,12 @@ public class ChatRoomServiceImpl implements ChatRoomService { return ResultDomain.success("查询聊天室成功", pageDomain); } + @Override + public ResultDomain countChatRooms(TbChatRoomDTO filter) { + long count = chatRoomMapper.countChatRooms(filter); + return ResultDomain.success("查询成功", count); + } + // ========================= 聊天室成员管理 ========================== @Override diff --git a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java index 686e75ca..476084b2 100644 --- a/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java +++ b/urbanLifelineServ/workcase/src/main/java/org/xyzh/workcase/service/WorkcaseServiceImpl.java @@ -227,6 +227,18 @@ public class WorkcaseServiceImpl implements WorkcaseService { return ResultDomain.success("查询成功", pageDomain); } + @Override + public ResultDomain countWorkcasesByType(TbWorkcaseDTO filter) { + List workcases = workcaseMapper.countWorkcasesByType(filter); + return ResultDomain.success("查询成功", workcases); + } + + @Override + public ResultDomain countWorkcases(TbWorkcaseDTO filter) { + long count = workcaseMapper.countWorkcases(filter); + return ResultDomain.success("查询成功", count); + } + // ====================== 同步到CRM和接收 =================== @Override diff --git a/urbanLifelineServ/workcase/src/main/resources/mapper/TbChatRoomMapper.xml b/urbanLifelineServ/workcase/src/main/resources/mapper/TbChatRoomMapper.xml index 91c5ce17..e10bab7e 100644 --- a/urbanLifelineServ/workcase/src/main/resources/mapper/TbChatRoomMapper.xml +++ b/urbanLifelineServ/workcase/src/main/resources/mapper/TbChatRoomMapper.xml @@ -158,6 +158,9 @@ AND status = #{filter.status} AND guest_id = #{filter.guestId} AND guest_name LIKE CONCAT('%', #{filter.guestName}, '%') + + AND create_time BETWEEN #{filter.startTime} AND #{filter.endTime} + AND deleted = false diff --git a/urbanLifelineServ/workcase/src/main/resources/mapper/TbWordCloudMapper.xml b/urbanLifelineServ/workcase/src/main/resources/mapper/TbWordCloudMapper.xml index a635f9f0..764bd9f7 100644 --- a/urbanLifelineServ/workcase/src/main/resources/mapper/TbWordCloudMapper.xml +++ b/urbanLifelineServ/workcase/src/main/resources/mapper/TbWordCloudMapper.xml @@ -83,6 +83,9 @@ ORDER BY frequency DESC, create_time DESC + + LIMIT #{filter.limit} + + + diff --git a/urbanLifelineWeb/packages/workcase/src/api/workcase/workcase.ts b/urbanLifelineWeb/packages/workcase/src/api/workcase/workcase.ts index 4c7d7a5b..af9c8453 100644 --- a/urbanLifelineWeb/packages/workcase/src/api/workcase/workcase.ts +++ b/urbanLifelineWeb/packages/workcase/src/api/workcase/workcase.ts @@ -181,5 +181,25 @@ export const workcaseAPI = { async getWorkcaseDevicePage(pageRequest: PageRequest): Promise> { const response = await api.post(`${this.baseUrl}/device/page`, pageRequest) return response.data + }, + + // ========================= 工单统计 ========================= + + /** + * 查询工单问题分类统计 + * @param filter 筛选条件(startTime, endTime) + */ + async countWorkcasesByType(filter: TbWorkcaseDTO): Promise> { + const response = await api.post(`${this.baseUrl}/category/count`, filter) + return response.data + }, + + /** + * 统计工单数量 + * @param filter 筛选条件(status等) + */ + async countWorkcases(filter: TbWorkcaseDTO): Promise> { + const response = await api.post(`${this.baseUrl}/count`, filter) + return response.data } } diff --git a/urbanLifelineWeb/packages/workcase/src/api/workcase/workcaseChat.ts b/urbanLifelineWeb/packages/workcase/src/api/workcase/workcaseChat.ts index 70fc50d9..68912ce9 100644 --- a/urbanLifelineWeb/packages/workcase/src/api/workcase/workcaseChat.ts +++ b/urbanLifelineWeb/packages/workcase/src/api/workcase/workcaseChat.ts @@ -312,5 +312,16 @@ export const workcaseChatAPI = { Object.keys(body).length > 0 ? body : {} ) return response.data + }, + + // ====================== 统计接口 ====================== + + /** + * 统计聊天室数量 + * @param filter 筛选条件(startTime, endTime, status等) + */ + async countChatRooms(filter: TbChatRoomDTO): Promise> { + const response = await api.post(`${this.baseUrl}/room/count`, filter) + return response.data } } diff --git a/urbanLifelineWeb/packages/workcase/src/assets/css/common.scss b/urbanLifelineWeb/packages/workcase/src/assets/css/common.scss index 6c2752e0..c5a2781d 100644 --- a/urbanLifelineWeb/packages/workcase/src/assets/css/common.scss +++ b/urbanLifelineWeb/packages/workcase/src/assets/css/common.scss @@ -139,6 +139,7 @@ $brand-color-hover: #004488; .product-cloud { display: flex; + justify-content: center; flex-wrap: wrap; gap: 8px 12px; padding: 8px 0; diff --git a/urbanLifelineWeb/packages/workcase/src/views/admin/overview/OverviewView.scss b/urbanLifelineWeb/packages/workcase/src/views/admin/overview/OverviewView.scss index b5303b00..b6019418 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/admin/overview/OverviewView.scss +++ b/urbanLifelineWeb/packages/workcase/src/views/admin/overview/OverviewView.scss @@ -1 +1,31 @@ -// OverviewView 样式占位符 +// OverviewView 样式 + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + color: #909399; + + p { + margin-top: 16px; + font-size: 14px; + color: #909399; + } +} + +.question-stats { + min-height: 200px; +} + +.product-cloud { + min-height: 150px; +} + +// 趋势样式 +.stat-trend { + &.down { + color: #f56c6c !important; + } +} diff --git a/urbanLifelineWeb/packages/workcase/src/views/admin/overview/OverviewView.vue b/urbanLifelineWeb/packages/workcase/src/views/admin/overview/OverviewView.vue index dc837d6e..b2841ab3 100644 --- a/urbanLifelineWeb/packages/workcase/src/views/admin/overview/OverviewView.vue +++ b/urbanLifelineWeb/packages/workcase/src/views/admin/overview/OverviewView.vue @@ -8,11 +8,12 @@
-
1,258
+
{{ dashboardData.consultCount }}
咨询次数
-
- - 较昨日 +12.5% +
+ + + 较昨日 {{ dashboardData.consultTrend >= 0 ? '+' : '' }}{{ dashboardData.consultTrend }}%
@@ -60,15 +61,21 @@
-
-
- {{ item.name }} - {{ item.count }} 次 -
-
-
+
+
+
+ {{ item.name }} + {{ item.count }} 次 +
+
+
+
+
+ +

暂无数据

+
@@ -83,10 +90,16 @@
- - {{ product.name }} - +
+ + {{ product.name }} + +
+
+ +

暂无数据

+
@@ -120,49 +133,215 @@ From 05c76fa3ec61bbcefe68b7578da8f3b51378c43a Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Thu, 1 Jan 2026 17:01:56 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../database/postgres/sql/createTableLog.sql | 41 +-- .../postgres/sql/createTableMessage.sql | 8 +- .../database/postgres/sql/initDataMessage.sql | 2 +- urbanLifelineServ/apis/api-log/pom.xml | 22 -- .../api/message/vo/MessageReceiverVO.java | 2 +- .../api/system/constance/SysLogContants.java | 18 ++ .../org/xyzh/api/system}/dto/TbSysLogDTO.java | 8 +- .../api/system}/dto/TbSysLoginLogDTO.java | 2 +- .../xyzh/api/system/service/LogService.java | 54 ++++ urbanLifelineServ/apis/pom.xml | 6 - urbanLifelineServ/log/pom.xml | 111 -------- .../src/main/java/org/xyzh/log/LogApp.java | 24 -- .../org/xyzh/log/config/OpenApiConfig.java | 58 ---- .../log/src/main/resources/application.yml | 99 ------- .../log/src/main/resources/log4j2.xml | 65 ----- urbanLifelineServ/pom.xml | 1 - .../xyzh/system/controller/LogController.java | 69 +++++ .../system}/mapper/log/TbSysLogMapper.java | 4 +- .../mapper/log/TbSysLoginLogMapper.java | 4 +- .../system/service/impl/LogServiceImpl.java | 137 ++++++++++ .../resources/mapper/log/TbSysLogMapper.xml | 254 ++++++++++++++++++ .../mapper/log/TbSysLoginLogMapper.xml | 186 +++++++++++++ 22 files changed, 757 insertions(+), 418 deletions(-) delete mode 100644 urbanLifelineServ/apis/api-log/pom.xml create mode 100644 urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/constance/SysLogContants.java rename urbanLifelineServ/apis/{api-log/src/main/java/org/xyzh/api/log => api-system/src/main/java/org/xyzh/api/system}/dto/TbSysLogDTO.java (87%) rename urbanLifelineServ/apis/{api-log/src/main/java/org/xyzh/api/log => api-system/src/main/java/org/xyzh/api/system}/dto/TbSysLoginLogDTO.java (97%) create mode 100644 urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/LogService.java delete mode 100644 urbanLifelineServ/log/pom.xml delete mode 100644 urbanLifelineServ/log/src/main/java/org/xyzh/log/LogApp.java delete mode 100644 urbanLifelineServ/log/src/main/java/org/xyzh/log/config/OpenApiConfig.java delete mode 100644 urbanLifelineServ/log/src/main/resources/application.yml delete mode 100644 urbanLifelineServ/log/src/main/resources/log4j2.xml create mode 100644 urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/LogController.java rename urbanLifelineServ/{log/src/main/java/org/xyzh/log => system/src/main/java/org/xyzh/system}/mapper/log/TbSysLogMapper.java (96%) rename urbanLifelineServ/{log/src/main/java/org/xyzh/log => system/src/main/java/org/xyzh/system}/mapper/log/TbSysLoginLogMapper.java (96%) create mode 100644 urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/LogServiceImpl.java create mode 100644 urbanLifelineServ/system/src/main/resources/mapper/log/TbSysLogMapper.xml create mode 100644 urbanLifelineServ/system/src/main/resources/mapper/log/TbSysLoginLogMapper.xml diff --git a/urbanLifelineServ/.bin/database/postgres/sql/createTableLog.sql b/urbanLifelineServ/.bin/database/postgres/sql/createTableLog.sql index a09c9171..34f64652 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/createTableLog.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/createTableLog.sql @@ -1,6 +1,5 @@ -CREATE SCHEMA IF NOT EXISTS log; -DROP TABLE IF EXISTS log.tb_sys_log CASCADE; -CREATE TABLE log.tb_sys_log ( +DROP TABLE IF EXISTS sys.tb_sys_log CASCADE; +CREATE TABLE sys.tb_sys_log ( optsn VARCHAR(50) NOT NULL, -- 流水号 log_id VARCHAR(50) NOT NULL, -- 日志ID type VARCHAR(50) NOT NULL, -- 日志类型 @@ -13,7 +12,8 @@ CREATE TABLE log.tb_sys_log ( message VARCHAR(255) NOT NULL, -- 日志消息 data JSONB DEFAULT NULL, -- 日志数据 creator VARCHAR(50) DEFAULT NULL, -- 创建者 - service_type VARCHAR(50) NOT NULL, -- 服务类型 + creator_name VARCHAR(200) DEFAULT NULL, -- 创建者姓名 + service VARCHAR(50) NOT NULL, -- 服务类型 dept_path VARCHAR(255) DEFAULT NULL, -- 部门全路径 updater VARCHAR(50) DEFAULT NULL, -- 更新者 create_time TIMESTAMPTZ NOT NULL DEFAULT now(), -- 日志创建时间 @@ -23,19 +23,20 @@ CREATE TABLE log.tb_sys_log ( PRIMARY KEY (log_id), UNIQUE (optsn) ); -COMMENT ON TABLE log.tb_sys_log IS '系统日志表'; -COMMENT ON COLUMN log.tb_sys_log.optsn IS '流水号'; -COMMENT ON COLUMN log.tb_sys_log.log_id IS '日志ID'; -COMMENT ON COLUMN log.tb_sys_log.type IS '日志类型'; -COMMENT ON COLUMN log.tb_sys_log.level IS '日志级别'; -COMMENT ON COLUMN log.tb_sys_log.module IS '日志模块'; -COMMENT ON COLUMN log.tb_sys_log.message IS '日志消息'; -COMMENT ON COLUMN log.tb_sys_log.data IS '日志数据'; -COMMENT ON COLUMN log.tb_sys_log.creator IS '创建者'; -COMMENT ON COLUMN log.tb_sys_log.service_type IS '服务类型'; -COMMENT ON COLUMN log.tb_sys_log.dept_path IS '部门全路径'; -COMMENT ON COLUMN log.tb_sys_log.updater IS '更新者'; -COMMENT ON COLUMN log.tb_sys_log.create_time IS '日志创建时间'; -COMMENT ON COLUMN log.tb_sys_log.update_time IS '日志更新时间'; -COMMENT ON COLUMN log.tb_sys_log.delete_time IS '日志删除时间'; -COMMENT ON COLUMN log.tb_sys_log.deleted IS '是否删除'; \ No newline at end of file +COMMENT ON TABLE sys.tb_sys_log IS '系统日志表'; +COMMENT ON COLUMN sys.tb_sys_log.optsn IS '流水号'; +COMMENT ON COLUMN sys.tb_sys_log.log_id IS '日志ID'; +COMMENT ON COLUMN sys.tb_sys_log.type IS '日志类型'; +COMMENT ON COLUMN sys.tb_sys_log.level IS '日志级别'; +COMMENT ON COLUMN sys.tb_sys_log.module IS '日志模块'; +COMMENT ON COLUMN sys.tb_sys_log.message IS '日志消息'; +COMMENT ON COLUMN sys.tb_sys_log.data IS '日志数据'; +COMMENT ON COLUMN sys.tb_sys_log.creator IS '创建者'; +COMMENT ON COLUMN sys.tb_sys_log.creator_name IS '创建者姓名'; +COMMENT ON COLUMN sys.tb_sys_log.service IS '服务类型'; +COMMENT ON COLUMN sys.tb_sys_log.dept_path IS '部门全路径'; +COMMENT ON COLUMN sys.tb_sys_log.updater IS '更新者'; +COMMENT ON COLUMN sys.tb_sys_log.create_time IS '日志创建时间'; +COMMENT ON COLUMN sys.tb_sys_log.update_time IS '日志更新时间'; +COMMENT ON COLUMN sys.tb_sys_log.delete_time IS '日志删除时间'; +COMMENT ON COLUMN sys.tb_sys_log.deleted IS '是否删除'; \ No newline at end of file diff --git a/urbanLifelineServ/.bin/database/postgres/sql/createTableMessage.sql b/urbanLifelineServ/.bin/database/postgres/sql/createTableMessage.sql index 6410308e..43aad8ba 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/createTableMessage.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/createTableMessage.sql @@ -8,7 +8,7 @@ CREATE TABLE message.tb_message ( content VARCHAR(255) NOT NULL, -- 消息内容 type VARCHAR(50) NOT NULL, -- 消息类型 status VARCHAR(50) NOT NULL, -- 消息状态 - service_type VARCHAR(50) NOT NULL, -- 服务类型 + service VARCHAR(50) NOT NULL, -- 服务类型 dept_path VARCHAR(255) DEFAULT NULL, -- 部门全路径(隔离) creator VARCHAR(50) NOT NULL DEFAULT 'system',-- 创建者 updater VARCHAR(50) DEFAULT NULL, -- 更新者 @@ -27,7 +27,7 @@ COMMENT ON COLUMN message.tb_message.title IS '消息标题'; COMMENT ON COLUMN message.tb_message.content IS '消息内容'; COMMENT ON COLUMN message.tb_message.type IS '消息类型'; COMMENT ON COLUMN message.tb_message.status IS '消息状态'; -COMMENT ON COLUMN message.tb_message.service_type IS '服务类型'; +COMMENT ON COLUMN message.tb_message.service IS '服务类型'; COMMENT ON COLUMN message.tb_message.dept_path IS '部门全路径'; COMMENT ON COLUMN message.tb_message.creator IS '创建者'; COMMENT ON COLUMN message.tb_message.updater IS '更新者'; @@ -166,7 +166,7 @@ CREATE TABLE message.tb_message_template ( title_template TEXT, -- 标题模板(支持变量) content_template TEXT NOT NULL, -- 内容模板(支持变量) variables JSONB, -- 模板变量定义 - service_type VARCHAR(50) NOT NULL, -- 服务类型 + service VARCHAR(50) NOT NULL, -- 服务类型 dept_path VARCHAR(255) DEFAULT NULL, -- 部门全路径 creator VARCHAR(50) DEFAULT NULL, -- 创建者 updater VARCHAR(50) DEFAULT NULL, -- 更新者 @@ -188,7 +188,7 @@ COMMENT ON COLUMN message.tb_message_template.template_type IS '模板类型:s COMMENT ON COLUMN message.tb_message_template.title_template IS '标题模板(支持变量)'; COMMENT ON COLUMN message.tb_message_template.content_template IS '内容模板(支持变量)'; COMMENT ON COLUMN message.tb_message_template.variables IS '模板变量定义'; -COMMENT ON COLUMN message.tb_message_template.service_type IS '服务类型'; +COMMENT ON COLUMN message.tb_message_template.service IS '服务类型'; COMMENT ON COLUMN message.tb_message_template.dept_path IS '部门全路径'; COMMENT ON COLUMN message.tb_message_template.creator IS '创建者'; COMMENT ON COLUMN message.tb_message_template.updater IS '更新者'; diff --git a/urbanLifelineServ/.bin/database/postgres/sql/initDataMessage.sql b/urbanLifelineServ/.bin/database/postgres/sql/initDataMessage.sql index 61260fe1..cd86f138 100644 --- a/urbanLifelineServ/.bin/database/postgres/sql/initDataMessage.sql +++ b/urbanLifelineServ/.bin/database/postgres/sql/initDataMessage.sql @@ -74,7 +74,7 @@ INSERT INTO message.tb_message_channel ( -- ============================= INSERT INTO message.tb_message_template ( optsn, template_id, template_code, template_name, template_type, - title_template, content_template, variables, service_type, + title_template, content_template, variables, service, creator, create_time, deleted ) VALUES -- 用户注册欢迎消息 diff --git a/urbanLifelineServ/apis/api-log/pom.xml b/urbanLifelineServ/apis/api-log/pom.xml deleted file mode 100644 index 9a829981..00000000 --- a/urbanLifelineServ/apis/api-log/pom.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - 4.0.0 - - org.xyzh - apis - 1.0.0 - - - org.xyzh.apis - api-log - ${urban-lifeline.version} - jar - - - 21 - 21 - - - \ No newline at end of file diff --git a/urbanLifelineServ/apis/api-message/src/main/java/org/xyzh/api/message/vo/MessageReceiverVO.java b/urbanLifelineServ/apis/api-message/src/main/java/org/xyzh/api/message/vo/MessageReceiverVO.java index b393e40c..edbf95c8 100644 --- a/urbanLifelineServ/apis/api-message/src/main/java/org/xyzh/api/message/vo/MessageReceiverVO.java +++ b/urbanLifelineServ/apis/api-message/src/main/java/org/xyzh/api/message/vo/MessageReceiverVO.java @@ -59,5 +59,5 @@ public class MessageReceiverVO extends BaseVO { private Date handleTime; @Schema(description = "服务类型") - private String serviceType; + private String service; } diff --git a/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/constance/SysLogContants.java b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/constance/SysLogContants.java new file mode 100644 index 00000000..c34facea --- /dev/null +++ b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/constance/SysLogContants.java @@ -0,0 +1,18 @@ +package org.xyzh.api.system.constance; + +public class SysLogContants { + + private final static String LOG_LEVEL_DEBUG = "DEBUG"; + private final static String LOG_LEVEL_INFO = "INFO"; + private final static String LOG_LEVEL_WARN = "WARN"; + private final static String LOG_LEVEL_ERROR = "ERROR"; + + + private final static String LOG_MODULE_SYSTEM = "系统"; + private final static String LOG_MODULE_AI = "AI服务"; + private final static String LOG_MODULE_AUTH = "日志"; + private final static String LOG_MODULE_WORKCASE = "工单"; + private final static String LOG_MODULE_KNOWLEDGE = "知识库"; + private final static String LOG_MODULE_FILE = "文件"; + +} diff --git a/urbanLifelineServ/apis/api-log/src/main/java/org/xyzh/api/log/dto/TbSysLogDTO.java b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/dto/TbSysLogDTO.java similarity index 87% rename from urbanLifelineServ/apis/api-log/src/main/java/org/xyzh/api/log/dto/TbSysLogDTO.java rename to urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/dto/TbSysLogDTO.java index 30f25144..6d0e88b1 100644 --- a/urbanLifelineServ/apis/api-log/src/main/java/org/xyzh/api/log/dto/TbSysLogDTO.java +++ b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/dto/TbSysLogDTO.java @@ -1,4 +1,4 @@ -package org.xyzh.api.log.dto; +package org.xyzh.api.system.dto; import com.alibaba.fastjson2.JSONObject; import io.swagger.v3.oas.annotations.media.Schema; @@ -48,4 +48,10 @@ public class TbSysLogDTO extends BaseDTO { @Schema(description = "日志数据") private JSONObject data; + + @Schema(description = "创建人姓名") + private String creatorName; + + @Schema(description = "服务") + private String servce; } \ No newline at end of file diff --git a/urbanLifelineServ/apis/api-log/src/main/java/org/xyzh/api/log/dto/TbSysLoginLogDTO.java b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/dto/TbSysLoginLogDTO.java similarity index 97% rename from urbanLifelineServ/apis/api-log/src/main/java/org/xyzh/api/log/dto/TbSysLoginLogDTO.java rename to urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/dto/TbSysLoginLogDTO.java index c6d4818e..e275ece8 100644 --- a/urbanLifelineServ/apis/api-log/src/main/java/org/xyzh/api/log/dto/TbSysLoginLogDTO.java +++ b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/dto/TbSysLoginLogDTO.java @@ -1,4 +1,4 @@ -package org.xyzh.api.log.dto; +package org.xyzh.api.system.dto; import java.util.Date; import com.alibaba.fastjson2.annotation.JSONField; diff --git a/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/LogService.java b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/LogService.java new file mode 100644 index 00000000..685bf9c2 --- /dev/null +++ b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/service/LogService.java @@ -0,0 +1,54 @@ +package org.xyzh.api.system.service; + + +import java.util.List; + +import org.xyzh.api.system.dto.TbSysLogDTO; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageRequest; + +/** + * @description 系统日志服务 + * @filename LogService.java + * @author yslg + * @copyright yslg + * @since 2026-01-01 + */ +public interface LogService { + + /** + * @description 统计系统日志 + * @param sysLog + * @return 返回值描述 + * @author yslg + * @since 2026-01-01 + */ + ResultDomain addSysLog(TbSysLogDTO sysLog); + + /** + * @description 统计日志数量 + * @param filter + * @return 返回值描述 + * @author yslg + * @since 2026-01-01 + */ + ResultDomain countSysLog(TbSysLogDTO filter); + + /** + * @description 获取日志列表 + * @param filter + * @return 日志列表 + * @author yslg + * @since 2026-01-01 + */ + ResultDomain getSysLogList(TbSysLogDTO filter); + + /** + * @description 获取日志分页 + * @param pageRequest + * @return 日志分页 + * @author yslg + * @since 2026-01-01 + */ + ResultDomain getSysLogPage(PageRequest pageRequest); +} diff --git a/urbanLifelineServ/apis/pom.xml b/urbanLifelineServ/apis/pom.xml index 26444721..c0c760ff 100644 --- a/urbanLifelineServ/apis/pom.xml +++ b/urbanLifelineServ/apis/pom.xml @@ -18,7 +18,6 @@ api-auth api-file api-message - api-log api-system api-crontab api-ai @@ -59,11 +58,6 @@ api-system ${urban-lifeline.version} - - org.xyzh.apis - api-log - ${urban-lifeline.version} - diff --git a/urbanLifelineServ/log/pom.xml b/urbanLifelineServ/log/pom.xml deleted file mode 100644 index 1113cfe6..00000000 --- a/urbanLifelineServ/log/pom.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - 4.0.0 - - org.xyzh - urban-lifeline - 1.0.0 - - - org.xyzh - log - ${urban-lifeline.version} - jar - - 21 - 21 - - - - - org.xyzh.apis - api-log - ${urban-lifeline.version} - - - - - org.springframework.boot - spring-boot-starter-actuator - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-logging - - - - - - - org.springframework.boot - spring-boot-starter-log4j2 - - - - - org.apache.dubbo - dubbo-spring-boot-starter - - - - - org.apache.dubbo - dubbo-nacos-spring-boot-starter - - - - - com.alibaba.cloud - spring-cloud-starter-alibaba-nacos-discovery - - - com.alibaba.nacos - nacos-logback-adapter-12 - - - com.alibaba.nacos - logback-adapter - - - - - - org.mybatis.spring.boot - mybatis-spring-boot-starter - ${mybatis.spring.boot.version} - - - - com.baomidou - mybatis-plus-boot-starter - ${mybatis.plus.version} - - - - org.mybatis - mybatis-spring - - - - - - org.mybatis - mybatis-spring - ${mybatis.spring.version} - - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - - - - \ No newline at end of file diff --git a/urbanLifelineServ/log/src/main/java/org/xyzh/log/LogApp.java b/urbanLifelineServ/log/src/main/java/org/xyzh/log/LogApp.java deleted file mode 100644 index 48577aeb..00000000 --- a/urbanLifelineServ/log/src/main/java/org/xyzh/log/LogApp.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.xyzh.log; - -import org.apache.dubbo.config.spring.context.annotation.EnableDubbo; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.ComponentScan; - -@SpringBootApplication -@EnableDubbo // 启用 Dubbo 服务 -@ComponentScan(basePackages = { - "org.xyzh.log", // log 模块 - "org.xyzh.common" // 公共模块 -}) -public class LogApp { - private static final Logger logger = LoggerFactory.getLogger(LogApp.class); - - public static void main(String[] args) { - logger.info("======================== LogApp 启动中 ========================="); - SpringApplication.run(LogApp.class, args); - logger.info("======================== LogApp 启动成功 ========================="); - } -} diff --git a/urbanLifelineServ/log/src/main/java/org/xyzh/log/config/OpenApiConfig.java b/urbanLifelineServ/log/src/main/java/org/xyzh/log/config/OpenApiConfig.java deleted file mode 100644 index 9552035a..00000000 --- a/urbanLifelineServ/log/src/main/java/org/xyzh/log/config/OpenApiConfig.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.xyzh.log.config; - -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Contact; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.info.License; -import io.swagger.v3.oas.models.security.SecurityRequirement; -import io.swagger.v3.oas.models.security.SecurityScheme; -import io.swagger.v3.oas.models.servers.Server; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.util.List; - -/** - * OpenAPI 配置类 - Log 服务 - * 配置 Swagger/OpenAPI 文档,方便 Apifox 导入接口和对象进行测试 - * - * @author yslg - */ -@Configuration -public class OpenApiConfig { - - @Bean - public OpenAPI logOpenAPI() { - return new OpenAPI() - .info(new Info() - .title("日志服务 API 文档") - .description(""" - 日志服务接口文档,包括系统日志、登录日志等功能。 - - ## 使用说明 - 1. 访问 Swagger UI: http://localhost:8083/urban-lifeline/log/swagger-ui.html - 2. 访问 OpenAPI JSON: http://localhost:8083/urban-lifeline/log/v3/api-docs - 3. 在 Apifox 中导入 OpenAPI JSON 进行接口测试 - """) - .version("1.0.0") - .contact(new Contact() - .name("yslg") - .email("3401275564@qq.com")) - .license(new License() - .name("Apache 2.0") - .url("https://www.apache.org/licenses/LICENSE-2.0.html"))) - .servers(List.of( - new Server().url("http://localhost:8083/urban-lifeline/log").description("本地开发环境") - )) - .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication")) - .components(new Components() - .addSecuritySchemes("Bearer Authentication", - new SecurityScheme() - .type(SecurityScheme.Type.HTTP) - .scheme("bearer") - .bearerFormat("JWT") - .description("请输入JWT Token,格式:Bearer {token}"))); - } -} - diff --git a/urbanLifelineServ/log/src/main/resources/application.yml b/urbanLifelineServ/log/src/main/resources/application.yml deleted file mode 100644 index 5044fc9c..00000000 --- a/urbanLifelineServ/log/src/main/resources/application.yml +++ /dev/null @@ -1,99 +0,0 @@ -# ================== Server ================== -server: - port: 8183 - # servlet: - # context-path: /urban-lifeline/log # 微服务架构下,context-path由Gateway管理 - -# ================== Auth ==================== - -auth: - enabled: true - gateway-mode: true - whitelist: - - /swagger-ui/** - - /swagger-ui.html - - /v3/api-docs/** - - /webjars/** - - /favicon.ico - - /error - - /actuator/health - - /actuator/info - -security: - aes: - secret-key: 1234567890qwer - -# ================== Spring ================== -spring: - application: - name: log-service - - # ================== Spring Cloud Nacos ================== - cloud: - nacos: - discovery: - server-addr: 127.0.0.1:8848 - namespace: dev - group: DEFAULT_GROUP - - # ================== DataSource ================== - datasource: - url: jdbc:postgresql://127.0.0.1:5432/urban_lifeline - username: postgres - password: postgres - driver-class-name: org.postgresql.Driver - - # ================== Redis ================== - data: - redis: - host: 127.0.0.1 # 如果是 docker 跑的 redis,按实际 host / 端口改 - port: 6379 - database: 0 - password: 123456 # 如果有密码就填上,没密码可以去掉这一行 - -# ================== SpringDoc ================== -springdoc: - api-docs: - enabled: true - path: /v3/api-docs - swagger-ui: - enabled: true - path: /swagger-ui.html - try-it-out-enabled: true - show-common-extensions: true - show-extensions: true - show-request-duration: true - filter: true - tags-sorter: alpha - operations-sorter: alpha - group-configs: - - group: 'default' - display-name: '日志服务 API' - paths-to-match: '/**' - -# ================== Dubbo + Nacos ================== -dubbo: - application: - name: urban-lifeline-log - qos-enable: false - protocol: - name: dubbo - port: -1 - registry: - address: nacos://127.0.0.1:8848 - scan: - base-packages: org.xyzh.log.service.impl - -# ================== MyBatis ================== -mybatis-plus: - mapper-locations: classpath:mapper/**/*.xml - type-aliases-package: org.xyzh.common.dto, org.xyzh.api - -# ================== Logging ================== -logging: - config: classpath:log4j2.xml - charset: - console: UTF-8 - file: UTF-8 - level: - org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE diff --git a/urbanLifelineServ/log/src/main/resources/log4j2.xml b/urbanLifelineServ/log/src/main/resources/log4j2.xml deleted file mode 100644 index 3d256d92..00000000 --- a/urbanLifelineServ/log/src/main/resources/log4j2.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/urbanLifelineServ/pom.xml b/urbanLifelineServ/pom.xml index 0d85662e..dceb275b 100644 --- a/urbanLifelineServ/pom.xml +++ b/urbanLifelineServ/pom.xml @@ -11,7 +11,6 @@ common apis gateway - log system auth file diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/LogController.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/LogController.java new file mode 100644 index 00000000..98785db5 --- /dev/null +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/controller/LogController.java @@ -0,0 +1,69 @@ +package org.xyzh.system.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.xyzh.api.system.dto.TbSysLogDTO; +import org.xyzh.api.system.service.LogService; +import org.xyzh.common.core.domain.ResultDomain; +import org.xyzh.common.core.page.PageRequest; + +/** + * @description 系统日志控制器 + * @filename LogController.java + * @author yslg + * @copyright yslg + * @since 2026-01-01 + */ +@RestController +@RequestMapping("/system/log") +public class LogController { + + private static final Logger logger = LoggerFactory.getLogger(LogController.class); + + @Autowired + private LogService logService; + + // ================= 系统日志相关接口 ================= + + /** + * 添加系统日志 + */ + @PostMapping + @PreAuthorize("hasAuthority('log:log:add')") + public ResultDomain addSysLog(@RequestBody TbSysLogDTO sysLog) { + return logService.addSysLog(sysLog); + } + + /** + * 统计日志数量 + */ + @PostMapping("/count") + @PreAuthorize("hasAuthority('log:log:view')") + public ResultDomain countSysLog(@RequestBody TbSysLogDTO filter) { + return logService.countSysLog(filter); + } + + /** + * 获取日志列表 + */ + @PostMapping("/list") + @PreAuthorize("hasAuthority('log:log:view')") + public ResultDomain getSysLogList(@RequestBody TbSysLogDTO filter) { + return logService.getSysLogList(filter); + } + + /** + * 分页查询日志 + */ + @PostMapping("/page") + @PreAuthorize("hasAuthority('log:log:view')") + public ResultDomain getSysLogPage(@RequestBody PageRequest pageRequest) { + return logService.getSysLogPage(pageRequest); + } +} diff --git a/urbanLifelineServ/log/src/main/java/org/xyzh/log/mapper/log/TbSysLogMapper.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/log/TbSysLogMapper.java similarity index 96% rename from urbanLifelineServ/log/src/main/java/org/xyzh/log/mapper/log/TbSysLogMapper.java rename to urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/log/TbSysLogMapper.java index dab52eae..49a0b659 100644 --- a/urbanLifelineServ/log/src/main/java/org/xyzh/log/mapper/log/TbSysLogMapper.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/log/TbSysLogMapper.java @@ -1,11 +1,11 @@ -package org.xyzh.log.mapper.log; +package org.xyzh.system.mapper.log; import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.xyzh.api.system.dto.TbSysLogDTO; import org.xyzh.common.core.page.PageParam; -import org.xyzh.api.log.dto.TbSysLogDTO; import com.baomidou.mybatisplus.core.mapper.BaseMapper; diff --git a/urbanLifelineServ/log/src/main/java/org/xyzh/log/mapper/log/TbSysLoginLogMapper.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/log/TbSysLoginLogMapper.java similarity index 96% rename from urbanLifelineServ/log/src/main/java/org/xyzh/log/mapper/log/TbSysLoginLogMapper.java rename to urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/log/TbSysLoginLogMapper.java index 5f86c324..117594b3 100644 --- a/urbanLifelineServ/log/src/main/java/org/xyzh/log/mapper/log/TbSysLoginLogMapper.java +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/mapper/log/TbSysLoginLogMapper.java @@ -1,11 +1,11 @@ -package org.xyzh.log.mapper.log; +package org.xyzh.system.mapper.log; import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.xyzh.api.system.dto.TbSysLoginLogDTO; import org.xyzh.common.core.page.PageParam; -import org.xyzh.api.log.dto.TbSysLoginLogDTO; import com.baomidou.mybatisplus.core.mapper.BaseMapper; diff --git a/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/LogServiceImpl.java b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/LogServiceImpl.java new file mode 100644 index 00000000..0f0f4ea5 --- /dev/null +++ b/urbanLifelineServ/system/src/main/java/org/xyzh/system/service/impl/LogServiceImpl.java @@ -0,0 +1,137 @@ +package org.xyzh.system.service.impl; + +import jakarta.annotation.Resource; +import org.apache.dubbo.config.annotation.DubboService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xyzh.api.system.dto.TbSysLogDTO; +import org.xyzh.api.system.service.LogService; +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.utils.StringUtils; +import org.xyzh.common.utils.id.IdUtil; +import org.xyzh.system.mapper.log.TbSysLogMapper; + +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * @description 系统日志服务实现类 + * @filename LogServiceImpl.java + * @author yslg + * @copyright yslg + * @since 2026-01-01 + */ +@DubboService( + version = "1.0.0", + group = "system", + timeout = 3000, + retries = 0 +) +public class LogServiceImpl implements LogService { + + private static final Logger logger = LoggerFactory.getLogger(LogServiceImpl.class); + + private static final String MSG_LOG_PARAM_REQUIRED = "日志参数不能为空"; + private static final String MSG_FILTER_PARAM_REQUIRED = "查询条件不能为空"; + private static final String MSG_PAGE_PARAM_REQUIRED = "分页参数不能为空"; + + @Resource + private TbSysLogMapper sysLogMapper; + + @Override + public ResultDomain addSysLog(TbSysLogDTO sysLog) { + if (sysLog == null) { + return ResultDomain.failure(MSG_LOG_PARAM_REQUIRED); + } + + try { + // 设置默认值 + if (StringUtils.isBlank(sysLog.getLogId())) { + sysLog.setLogId(IdUtil.generateID()); + } + if (StringUtils.isBlank(sysLog.getOptsn())) { + sysLog.setOptsn(IdUtil.generateID()); + } + if (sysLog.getCreateTime() == null) { + sysLog.setCreateTime(new Date()); + } + if (sysLog.getDeleted() == null) { + sysLog.setDeleted(false); + } + + int rows = sysLogMapper.insertLog(sysLog); + if (rows > 0) { + logger.info("添加系统日志成功, logId={}", sysLog.getLogId()); + return ResultDomain.success("添加系统日志成功", sysLog); + } + logger.warn("添加系统日志失败, logId={}", sysLog.getLogId()); + return ResultDomain.failure("添加系统日志失败"); + } catch (Exception e) { + logger.error("添加系统日志异常", e); + return ResultDomain.failure("添加系统日志异常: " + e.getMessage()); + } + } + + @Override + public ResultDomain countSysLog(TbSysLogDTO filter) { + if (filter == null) { + return ResultDomain.failure(MSG_FILTER_PARAM_REQUIRED); + } + + try { + int count = sysLogMapper.getLogCount(filter); + return ResultDomain.success("统计日志数量成功", count); + } catch (Exception e) { + logger.error("统计日志数量异常", e); + return ResultDomain.failure("统计日志数量异常: " + e.getMessage()); + } + } + + @Override + public ResultDomain getSysLogList(TbSysLogDTO filter) { + try { + List list = sysLogMapper.getLogByFilter(filter); + if (list == null) { + list = Collections.emptyList(); + } + return ResultDomain.success("获取日志列表成功", list); + } catch (Exception e) { + logger.error("获取日志列表异常", e); + return ResultDomain.failure("获取日志列表异常: " + e.getMessage()); + } + } + + @Override + public ResultDomain getSysLogPage(PageRequest pageRequest) { + if (pageRequest == null) { + return ResultDomain.failure(MSG_PAGE_PARAM_REQUIRED); + } + + try { + PageParam pageParam = pageRequest.getPageParam(); + TbSysLogDTO filter = pageRequest.getFilter(); + + // 查询总数 + int total = sysLogMapper.getLogCount(filter); + pageParam.setTotal(total); + pageParam.setTotalPages(pageParam.getPageSize() == 0 ? 0 : + (int) Math.ceil((double) total / pageParam.getPageSize())); + + // 查询分页数据 + List data = sysLogMapper.getLogPageByFilter(filter, pageParam); + if (data == null) { + data = Collections.emptyList(); + } + + PageDomain pageDomain = new PageDomain<>(pageParam, data); + return ResultDomain.success("分页查询日志成功", pageDomain); + } catch (Exception e) { + logger.error("分页查询日志异常", e); + return ResultDomain.failure("分页查询日志异常: " + e.getMessage()); + } + } +} diff --git a/urbanLifelineServ/system/src/main/resources/mapper/log/TbSysLogMapper.xml b/urbanLifelineServ/system/src/main/resources/mapper/log/TbSysLogMapper.xml new file mode 100644 index 00000000..6956ee96 --- /dev/null +++ b/urbanLifelineServ/system/src/main/resources/mapper/log/TbSysLogMapper.xml @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + log_id, optsn, type, level, module, ip_address, ip_source, browser, os, message, + data, creator, creator_name, service, dept_path, updater, + create_time, update_time, delete_time, deleted + + + + + INSERT INTO sys.tb_sys_log + + log_id, + optsn, + type, + level, + module, + message, + service, + ip_address, + ip_source, + browser, + os, + data, + creator, + creator_name, + dept_path, + updater, + create_time, + update_time, + delete_time, + deleted, + + VALUES + + #{logId}, + #{optsn}, + #{type}, + #{level}, + #{module}, + #{message}, + #{servce}, + #{ipAddress}, + #{ip_source}, + #{browser}, + #{os}, + #{data, typeHandler=org.xyzh.common.jdbc.handler.FastJson2TypeHandler}, + #{creator}, + #{creatorName}, + #{deptPath}, + #{updater}, + #{createTime}, + #{updateTime}, + #{deleteTime}, + #{deleted}, + + + + + + UPDATE sys.tb_sys_log + + type = #{type}, + level = #{level}, + module = #{module}, + ip_address = #{ipAddress}, + ip_source = #{ip_source}, + browser = #{browser}, + os = #{os}, + message = #{message}, + data = #{data, typeHandler=org.xyzh.common.jdbc.handler.FastJson2TypeHandler}, + service = #{servce}, + updater = #{updater}, + dept_path = #{deptPath}, + update_time = #{updateTime}, + delete_time = #{deleteTime}, + deleted = #{deleted}, + + WHERE log_id = #{logId} + + + + + UPDATE sys.tb_sys_log + SET deleted = true, + delete_time = NOW() + WHERE log_id = #{logId} + + + + + + + + + + + + + + + diff --git a/urbanLifelineServ/system/src/main/resources/mapper/log/TbSysLoginLogMapper.xml b/urbanLifelineServ/system/src/main/resources/mapper/log/TbSysLoginLogMapper.xml new file mode 100644 index 00000000..384aced8 --- /dev/null +++ b/urbanLifelineServ/system/src/main/resources/mapper/log/TbSysLoginLogMapper.xml @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + optsn, user_id, username, ip_address, ip_source, browser, os, password, + login_time, status, error_count, message, create_time + + + + + INSERT INTO sys.tb_sys_login_log + + optsn, + user_id, + username, + ip_address, + ip_source, + browser, + os, + password, + login_time, + status, + error_count, + message, + create_time, + + VALUES + + #{optsn}, + #{userId}, + #{username}, + #{ipAddress}, + #{ipSource}, + #{browser}, + #{os}, + #{password}, + #{loginTime}, + #{status}, + #{errorCount}, + #{message}, + #{createTime}, + + + + + + UPDATE sys.tb_sys_login_log + + user_id = #{userId}, + username = #{username}, + ip_address = #{ipAddress}, + ip_source = #{ipSource}, + browser = #{browser}, + os = #{os}, + password = #{password}, + login_time = #{loginTime}, + status = #{status}, + error_count = #{errorCount}, + message = #{message}, + + WHERE optsn = #{optsn} + + + + + DELETE FROM sys.tb_sys_login_log + WHERE optsn = #{optsn} + + + + + + + + + + + + + + + From 4b6d7d04ec9905cabd8b2718fa9b82df0037d7e3 Mon Sep 17 00:00:00 2001 From: wangys <3401275564@qq.com> Date: Thu, 1 Jan 2026 17:36:00 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E5=9C=A8=E7=BA=BF=E7=83=AD=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/xyzh/ai/config/DifyConfig.java | 10 ++- .../xyzh/ai/config/RedisSubscriberConfig.java | 34 +++++++++ .../xyzh/ai/listener/DifyConfigListener.java | 31 ++++++++ .../constance/SysConfigRedisPrefix.java | 75 ++++++++++++++++++- .../listener/AbstractSysConfigListener.java | 66 ++++++++++++++++ .../service/impl/SysConfigServiceImpl.java | 42 +++++++++++ 6 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/config/RedisSubscriberConfig.java create mode 100644 urbanLifelineServ/ai/src/main/java/org/xyzh/ai/listener/DifyConfigListener.java create mode 100644 urbanLifelineServ/common/common-redis/src/main/java/org/xyzh/common/redis/listener/AbstractSysConfigListener.java diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java index 74b4daa4..41f980f4 100644 --- a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java @@ -4,7 +4,6 @@ import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.dubbo.config.annotation.DubboReference; -import org.apache.dubbo.config.annotation.DubboService; import org.springframework.context.annotation.Configuration; import org.xyzh.api.system.service.SysConfigService; @@ -159,6 +158,15 @@ public class DifyConfig { return apiBaseUrl != null && !apiBaseUrl.trim().isEmpty(); } + /** + * 刷新配置(从数据库重新加载) + * 由Redis事件监听器调用 + */ + public void refresh() { + log.info("收到配置刷新请求,重新加载Dify配置..."); + init(); + } + /** * 获取完整的API URL */ diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/config/RedisSubscriberConfig.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/config/RedisSubscriberConfig.java new file mode 100644 index 00000000..2e12b7c8 --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/config/RedisSubscriberConfig.java @@ -0,0 +1,34 @@ +package org.xyzh.ai.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.xyzh.ai.listener.DifyConfigListener; + +/** + * AI模块Redis订阅配置 + * + * @author cascade + * @since 2026-01-01 + */ +@Configuration +public class RedisSubscriberConfig { + + @Autowired + private DifyConfigListener difyConfigListener; + + @Bean + public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + + // 订阅Dify配置变更频道 + container.addMessageListener(difyConfigListener, + new PatternTopic(difyConfigListener.getChannelPattern())); + + return container; + } +} diff --git a/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/listener/DifyConfigListener.java b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/listener/DifyConfigListener.java new file mode 100644 index 00000000..585bb0cd --- /dev/null +++ b/urbanLifelineServ/ai/src/main/java/org/xyzh/ai/listener/DifyConfigListener.java @@ -0,0 +1,31 @@ +package org.xyzh.ai.listener; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.xyzh.api.system.constance.SysConfigRedisPrefix; +import org.xyzh.ai.config.DifyConfig; +import org.xyzh.common.redis.listener.AbstractSysConfigListener; + +/** + * Dify配置变更监听器 + * 监听sys:config:dify频道,接收到事件后延时2秒刷新配置 + * + * @author cascade + * @since 2026-01-01 + */ +@Component +public class DifyConfigListener extends AbstractSysConfigListener { + + @Autowired + private DifyConfig difyConfig; + + @Override + protected void doRefresh(String channel, String body) { + difyConfig.refresh(); + } + + @Override + public String getChannelPattern() { + return SysConfigRedisPrefix.SYS_CONFIG_DIFY; + } +} diff --git a/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/constance/SysConfigRedisPrefix.java b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/constance/SysConfigRedisPrefix.java index 39c014b1..f3f5990f 100644 --- a/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/constance/SysConfigRedisPrefix.java +++ b/urbanLifelineServ/apis/api-system/src/main/java/org/xyzh/api/system/constance/SysConfigRedisPrefix.java @@ -1,8 +1,81 @@ package org.xyzh.api.system.constance; +import java.util.HashMap; +import java.util.Map; + /** * 通过redis事件,实现数据库更新配置,更新其他服务的bean数据 + * 按配置分组定义Redis前缀 + * + * 注意:其他服务接收到事件后需延时2秒再查询数据库,等待发布方事务提交 + * + * @see org.xyzh.common.redis.listener.AbstractSysConfigListener */ public class SysConfigRedisPrefix { - public static final String SYS_CONFIG_DIFY="sys:config:dify"; + + /** 配置变更频道前缀(用于订阅所有配置变更) */ + public static final String SYS_CONFIG_PREFIX = "sys:config:"; + + /** 站点与品牌配置 */ + public static final String SYS_CONFIG_SITE = "sys:config:site"; + + /** 国际化与时区配置 */ + public static final String SYS_CONFIG_I18N = "sys:config:i18n"; + + /** 安全与认证配置 */ + public static final String SYS_CONFIG_SECURITY = "sys:config:security"; + + /** 存储与上传配置(含MinIO、文件管理) */ + public static final String SYS_CONFIG_STORAGE = "sys:config:storage"; + + /** 通知配置(邮件/短信) */ + public static final String SYS_CONFIG_NOTIFY = "sys:config:notify"; + + /** Dify AI配置 */ + public static final String SYS_CONFIG_DIFY = "sys:config:dify"; + + /** 日志与审计配置 */ + public static final String SYS_CONFIG_LOG = "sys:config:log"; + + /** 平台特性配置 */ + public static final String SYS_CONFIG_PLATFORM = "sys:config:platform"; + + /** 微信客服配置 */ + public static final String SYS_CONFIG_WECHAT = "sys:config:wechat"; + + /** group到channel的映射 */ + private static final Map GROUP_CHANNEL_MAP = new HashMap<>(); + + static { + GROUP_CHANNEL_MAP.put("site", SYS_CONFIG_SITE); + GROUP_CHANNEL_MAP.put("i18n", SYS_CONFIG_I18N); + GROUP_CHANNEL_MAP.put("security", SYS_CONFIG_SECURITY); + GROUP_CHANNEL_MAP.put("storage", SYS_CONFIG_STORAGE); + GROUP_CHANNEL_MAP.put("notify", SYS_CONFIG_NOTIFY); + GROUP_CHANNEL_MAP.put("dify", SYS_CONFIG_DIFY); + GROUP_CHANNEL_MAP.put("log", SYS_CONFIG_LOG); + GROUP_CHANNEL_MAP.put("platform", SYS_CONFIG_PLATFORM); + GROUP_CHANNEL_MAP.put("wechat", SYS_CONFIG_WECHAT); + } + + /** + * 根据group获取对应的Redis channel + * @param group 配置分组(对应数据库中的group字段) + * @return channel名称,未匹配则返回 sys:config:{group} + */ + public static String getChannelByGroup(String group) { + if (group == null || group.isEmpty()) { + return null; + } + return GROUP_CHANNEL_MAP.getOrDefault(group, SYS_CONFIG_PREFIX + group); + } + + /** + * 判断group是否有效 + * @param group 配置分组 + * @return 是否为已知的分组 + */ + public static boolean isValidGroup(String group) { + return group != null && GROUP_CHANNEL_MAP.containsKey(group); + } } diff --git a/urbanLifelineServ/common/common-redis/src/main/java/org/xyzh/common/redis/listener/AbstractSysConfigListener.java b/urbanLifelineServ/common/common-redis/src/main/java/org/xyzh/common/redis/listener/AbstractSysConfigListener.java new file mode 100644 index 00000000..46b5f380 --- /dev/null +++ b/urbanLifelineServ/common/common-redis/src/main/java/org/xyzh/common/redis/listener/AbstractSysConfigListener.java @@ -0,0 +1,66 @@ +package org.xyzh.common.redis.listener; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 系统配置变更监听器抽象基类 + * 通过Redis Pub/Sub接收配置变更事件,延时2秒后执行刷新(等待事务提交) + * + * @author cascade + * @since 2026-01-01 + */ +public abstract class AbstractSysConfigListener implements MessageListener { + + private static final Logger logger = LoggerFactory.getLogger(AbstractSysConfigListener.class); + + /** 延时时间(毫秒),等待事务提交 */ + private static final long DELAY_MILLIS = 2000L; + + /** 调度线程池 */ + private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread t = new Thread(r, "sys-config-refresh"); + t.setDaemon(true); + return t; + }); + + @Override + public void onMessage(Message message, byte[] pattern) { + String channel = new String(message.getChannel()); + String body = new String(message.getBody()); + + logger.info("收到配置变更事件: channel={}, body={}", channel, body); + + // 延时2秒执行,等待发布方事务提交 + scheduler.schedule(() -> { + try { + logger.info("开始刷新配置: channel={}", channel); + doRefresh(channel, body); + logger.info("配置刷新完成: channel={}", channel); + } catch (Exception e) { + logger.error("配置刷新失败: channel={}", channel, e); + } + }, DELAY_MILLIS, TimeUnit.MILLISECONDS); + } + + /** + * 执行配置刷新,子类实现具体逻辑 + * + * @param channel 频道名称(对应配置分组) + * @param body 消息体(可选,可传递额外信息) + */ + protected abstract void doRefresh(String channel, String body); + + /** + * 获取监听的频道前缀 + * + * @return 频道前缀 + */ + public abstract String getChannelPattern(); +} 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 57c8031e..4f871c9c 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 @@ -17,8 +17,13 @@ import org.xyzh.common.core.page.PageDomain; import org.xyzh.common.utils.id.IdUtil; import org.xyzh.common.utils.StringUtils; +import org.xyzh.common.redis.service.RedisService; +import org.xyzh.api.system.constance.SysConfigRedisPrefix; import org.xyzh.system.mapper.config.TbSysConfigMapper; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + /** * @description 系统配置服务实现类 * @filename SysConfigServiceImpl.java @@ -41,6 +46,37 @@ public class SysConfigServiceImpl implements SysConfigService { @Resource private TbSysConfigMapper configMapper; + @Resource + private RedisService redisService; + + /** 异步发送事件的线程池 */ + private final ExecutorService eventExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "sys-config-event"); + t.setDaemon(true); + return t; + }); + + /** + * 异步发布配置变更事件 + * @param group 配置分组 + */ + private void publishConfigChangeEvent(String group) { + if (StringUtils.isBlank(group)) { + return; + } + eventExecutor.execute(() -> { + try { + String channel = SysConfigRedisPrefix.getChannelByGroup(group); + if (channel != null) { + redisService.publish(channel, System.currentTimeMillis()); + logger.info("配置变更事件发布成功: channel={}", channel); + } + } catch (Exception e) { + logger.error("配置变更事件发布失败: group={}", group, e); + } + }); + } + /** * 根据key查询配置 */ @@ -242,6 +278,8 @@ public class SysConfigServiceImpl implements SysConfigService { int rows = configMapper.insertConfig(configDTO); if (rows > 0) { logger.info("新增配置成功, configId={}", configDTO.getConfigId()); + // 异步发布配置变更事件 + publishConfigChangeEvent(configDTO.getGroup()); return ResultDomain.success("新增配置成功", configDTO); } logger.warn("新增配置失败, configId={}", configDTO.getConfigId()); @@ -257,6 +295,8 @@ public class SysConfigServiceImpl implements SysConfigService { int rows = configMapper.updateConfig(configDTO); if (rows > 0) { logger.info("更新配置成功, configId={}", configDTO.getConfigId()); + // 异步发布配置变更事件 + publishConfigChangeEvent(configDTO.getGroup()); return ResultDomain.success("更新配置成功", configDTO); } logger.warn("更新配置失败, configId={}", configDTO.getConfigId()); @@ -271,6 +311,8 @@ public class SysConfigServiceImpl implements SysConfigService { int rows = configMapper.deleteConfig(configDTO); if (rows > 0) { logger.info("删除配置成功, configId={}", configDTO.getConfigId()); + // 异步发布配置变更事件 + publishConfigChangeEvent(configDTO.getGroup()); return ResultDomain.success("删除配置成功", Boolean.TRUE); } logger.warn("删除配置失败, configId={}", configDTO.getConfigId());