Merge branch 'master' into docker

This commit is contained in:
2026-01-02 10:46:52 +08:00
65 changed files with 3248 additions and 535 deletions

View File

@@ -1,6 +1,5 @@
CREATE SCHEMA IF NOT EXISTS log; DROP TABLE IF EXISTS sys.tb_sys_log CASCADE;
DROP TABLE IF EXISTS log.tb_sys_log CASCADE; CREATE TABLE sys.tb_sys_log (
CREATE TABLE log.tb_sys_log (
optsn VARCHAR(50) NOT NULL, -- 流水号 optsn VARCHAR(50) NOT NULL, -- 流水号
log_id VARCHAR(50) NOT NULL, -- 日志ID log_id VARCHAR(50) NOT NULL, -- 日志ID
type VARCHAR(50) NOT NULL, -- 日志类型 type VARCHAR(50) NOT NULL, -- 日志类型
@@ -13,7 +12,8 @@ CREATE TABLE log.tb_sys_log (
message VARCHAR(255) NOT NULL, -- 日志消息 message VARCHAR(255) NOT NULL, -- 日志消息
data JSONB DEFAULT NULL, -- 日志数据 data JSONB DEFAULT NULL, -- 日志数据
creator VARCHAR(50) 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, -- 部门全路径 dept_path VARCHAR(255) DEFAULT NULL, -- 部门全路径
updater VARCHAR(50) DEFAULT NULL, -- 更新者 updater VARCHAR(50) DEFAULT NULL, -- 更新者
create_time TIMESTAMPTZ NOT NULL DEFAULT now(), -- 日志创建时间 create_time TIMESTAMPTZ NOT NULL DEFAULT now(), -- 日志创建时间
@@ -23,19 +23,20 @@ CREATE TABLE log.tb_sys_log (
PRIMARY KEY (log_id), PRIMARY KEY (log_id),
UNIQUE (optsn) UNIQUE (optsn)
); );
COMMENT ON TABLE log.tb_sys_log IS '系统日志表'; COMMENT ON TABLE sys.tb_sys_log IS '系统日志表';
COMMENT ON COLUMN log.tb_sys_log.optsn IS '流水号'; COMMENT ON COLUMN sys.tb_sys_log.optsn IS '流水号';
COMMENT ON COLUMN log.tb_sys_log.log_id IS '日志ID'; COMMENT ON COLUMN sys.tb_sys_log.log_id IS '日志ID';
COMMENT ON COLUMN log.tb_sys_log.type IS '日志类型'; COMMENT ON COLUMN sys.tb_sys_log.type IS '日志类型';
COMMENT ON COLUMN log.tb_sys_log.level IS '日志级别'; COMMENT ON COLUMN sys.tb_sys_log.level IS '日志级别';
COMMENT ON COLUMN log.tb_sys_log.module IS '日志模块'; COMMENT ON COLUMN sys.tb_sys_log.module IS '日志模块';
COMMENT ON COLUMN log.tb_sys_log.message IS '日志消息'; COMMENT ON COLUMN sys.tb_sys_log.message IS '日志消息';
COMMENT ON COLUMN log.tb_sys_log.data IS '日志数据'; COMMENT ON COLUMN sys.tb_sys_log.data IS '日志数据';
COMMENT ON COLUMN log.tb_sys_log.creator IS '创建者'; COMMENT ON COLUMN sys.tb_sys_log.creator IS '创建者';
COMMENT ON COLUMN log.tb_sys_log.service_type IS '服务类型'; COMMENT ON COLUMN sys.tb_sys_log.creator_name IS '创建者姓名';
COMMENT ON COLUMN log.tb_sys_log.dept_path IS '部门全路径'; COMMENT ON COLUMN sys.tb_sys_log.service IS '服务类型';
COMMENT ON COLUMN log.tb_sys_log.updater IS '更新者'; COMMENT ON COLUMN sys.tb_sys_log.dept_path IS '部门全路径';
COMMENT ON COLUMN log.tb_sys_log.create_time IS '日志创建时间'; COMMENT ON COLUMN sys.tb_sys_log.updater IS '更新者';
COMMENT ON COLUMN log.tb_sys_log.update_time IS '日志更新时间'; COMMENT ON COLUMN sys.tb_sys_log.create_time IS '日志创建时间';
COMMENT ON COLUMN log.tb_sys_log.delete_time IS '日志删除时间'; COMMENT ON COLUMN sys.tb_sys_log.update_time IS '日志更新时间';
COMMENT ON COLUMN log.tb_sys_log.deleted IS '是否删除'; COMMENT ON COLUMN sys.tb_sys_log.delete_time IS '日志删除时间';
COMMENT ON COLUMN sys.tb_sys_log.deleted IS '是否删除';

View File

@@ -8,7 +8,7 @@ CREATE TABLE message.tb_message (
content VARCHAR(255) NOT NULL, -- 消息内容 content VARCHAR(255) NOT NULL, -- 消息内容
type VARCHAR(50) NOT NULL, -- 消息类型 type VARCHAR(50) NOT NULL, -- 消息类型
status 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, -- 部门全路径(隔离) dept_path VARCHAR(255) DEFAULT NULL, -- 部门全路径(隔离)
creator VARCHAR(50) NOT NULL DEFAULT 'system',-- 创建者 creator VARCHAR(50) NOT NULL DEFAULT 'system',-- 创建者
updater VARCHAR(50) DEFAULT NULL, -- 更新者 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.content IS '消息内容';
COMMENT ON COLUMN message.tb_message.type IS '消息类型'; COMMENT ON COLUMN message.tb_message.type IS '消息类型';
COMMENT ON COLUMN message.tb_message.status 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.dept_path IS '部门全路径';
COMMENT ON COLUMN message.tb_message.creator IS '创建者'; COMMENT ON COLUMN message.tb_message.creator IS '创建者';
COMMENT ON COLUMN message.tb_message.updater IS '更新者'; COMMENT ON COLUMN message.tb_message.updater IS '更新者';
@@ -166,7 +166,7 @@ CREATE TABLE message.tb_message_template (
title_template TEXT, -- 标题模板(支持变量) title_template TEXT, -- 标题模板(支持变量)
content_template TEXT NOT NULL, -- 内容模板(支持变量) content_template TEXT NOT NULL, -- 内容模板(支持变量)
variables JSONB, -- 模板变量定义 variables JSONB, -- 模板变量定义
service_type VARCHAR(50) NOT NULL, -- 服务类型 service VARCHAR(50) NOT NULL, -- 服务类型
dept_path VARCHAR(255) DEFAULT NULL, -- 部门全路径 dept_path VARCHAR(255) DEFAULT NULL, -- 部门全路径
creator VARCHAR(50) DEFAULT NULL, -- 创建者 creator VARCHAR(50) DEFAULT NULL, -- 创建者
updater 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.title_template IS '标题模板(支持变量)';
COMMENT ON COLUMN message.tb_message_template.content_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.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.dept_path IS '部门全路径';
COMMENT ON COLUMN message.tb_message_template.creator IS '创建者'; COMMENT ON COLUMN message.tb_message_template.creator IS '创建者';
COMMENT ON COLUMN message.tb_message_template.updater IS '更新者'; COMMENT ON COLUMN message.tb_message_template.updater IS '更新者';

View File

@@ -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; 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对话和人工客服消息'; 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 -- 4. 视频会议表Jitsi Meet
-- 记录聊天室内创建的视频会议 -- 记录聊天室内创建的视频会议
DROP TABLE IF EXISTS workcase.tb_video_meeting CASCADE; DROP TABLE IF EXISTS workcase.tb_video_meeting CASCADE;

View File

@@ -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-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-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), ('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.workflow.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), ('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),

View File

@@ -74,7 +74,7 @@ INSERT INTO message.tb_message_channel (
-- ============================= -- =============================
INSERT INTO message.tb_message_template ( INSERT INTO message.tb_message_template (
optsn, template_id, template_code, template_name, template_type, 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 creator, create_time, deleted
) VALUES ) VALUES
-- 用户注册欢迎消息 -- 用户注册欢迎消息

View File

@@ -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);
}
}
/** /**
* 停止对话生成 * 停止对话生成
*/ */

View File

@@ -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<String, Object> inputs = new java.util.HashMap<>();
/**
* 响应模式streaming流式、blocking阻塞
*/
@JSONField(name = "response_mode")
private String responseMode = "blocking";
/**
* 用户标识
*/
private String user;
}

View File

@@ -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<String, Object> 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;
}
}

View File

@@ -4,7 +4,6 @@ import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference; import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.xyzh.api.system.service.SysConfigService; import org.xyzh.api.system.service.SysConfigService;
@@ -159,6 +158,15 @@ public class DifyConfig {
return apiBaseUrl != null && !apiBaseUrl.trim().isEmpty(); return apiBaseUrl != null && !apiBaseUrl.trim().isEmpty();
} }
/**
* 刷新配置(从数据库重新加载)
* 由Redis事件监听器调用
*/
public void refresh() {
log.info("收到配置刷新请求重新加载Dify配置...");
init();
}
/** /**
* 获取完整的API URL * 获取完整的API URL
*/ */

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -13,6 +13,9 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.xyzh.ai.client.DifyApiClient; import org.xyzh.ai.client.DifyApiClient;
import org.xyzh.ai.client.callback.StreamCallback; import org.xyzh.ai.client.callback.StreamCallback;
import org.xyzh.ai.client.dto.ChatRequest; 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.config.DifyConfig;
import org.xyzh.ai.mapper.TbChatMapper; import org.xyzh.ai.mapper.TbChatMapper;
import org.xyzh.ai.mapper.TbChatMessageMapper; 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 Logger logger = LoggerFactory.getLogger(AgentChatServiceImpl.class);
private static final String CHAT_SESSION_PREFIX = "ai:chat:session:"; 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; private static final long SESSION_TTL = 5 * 60;
@Autowired @Autowired
@@ -256,7 +260,7 @@ public class AgentChatServiceImpl implements AgentChatService {
String agentId = prepareData.getAgentId(); String agentId = prepareData.getAgentId();
String chatId = prepareData.getChatId(); String chatId = prepareData.getChatId();
String query = prepareData.getQuery(); String query = prepareData.getQuery();
// 1. 校验智能体 // 1. 校验智能体
ResultDomain<TbAgent> agentResult = agentService.selectAgentById(agentId); ResultDomain<TbAgent> agentResult = agentService.selectAgentById(agentId);
if (!agentResult.getSuccess() || agentResult.getData() == null || !agentResult.getData().getIsOuter()) { if (!agentResult.getSuccess() || agentResult.getData() == null || !agentResult.getData().getIsOuter()) {
@@ -269,7 +273,7 @@ public class AgentChatServiceImpl implements AgentChatService {
chatFilter.setAgentId(agentId); chatFilter.setAgentId(agentId);
chatFilter.setUserId(prepareData.getUserId()); chatFilter.setUserId(prepareData.getUserId());
chatFilter.setUserType(prepareData.getUserType()); chatFilter.setUserType(prepareData.getUserType());
LoginDomain loginDomain = LoginUtil.getCurrentLogin(); LoginDomain loginDomain = LoginUtil.getCurrentLogin();
String userId = loginDomain.getUser().getUserId(); String userId = loginDomain.getUser().getUserId();
if (userId == null) { if (userId == null) {
@@ -290,7 +294,30 @@ public class AgentChatServiceImpl implements AgentChatService {
// 4. 生成临时消息IDsessionId // 4. 生成临时消息IDsessionId
String sessionId = IdUtil.getSnowflakeId(); String sessionId = IdUtil.getSnowflakeId();
// 5. 存储会话数据到Redis // 5. 准备 inputs 参数
Map<String, Object> 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<TbKnowledge> knowledgeRD = knowledgeService.listKnowledges(filter);
List<String> 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<String, Object> sessionData = new HashMap<>(); Map<String, Object> sessionData = new HashMap<>();
sessionData.put("agentId", agentId); sessionData.put("agentId", agentId);
sessionData.put("chatId", chatId); sessionData.put("chatId", chatId);
@@ -300,12 +327,17 @@ public class AgentChatServiceImpl implements AgentChatService {
sessionData.put("apiKey", agent.getApiKey()); sessionData.put("apiKey", agent.getApiKey());
sessionData.put("outer", agent.getIsOuter()); sessionData.put("outer", agent.getIsOuter());
sessionData.put("service", prepareData.getService()); sessionData.put("service", prepareData.getService());
sessionData.put("isGuest", "guest".equals(loginDomain.getUser().getStatus())); 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); 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); return ResultDomain.success("准备成功", sessionId);
} }
@@ -334,9 +366,9 @@ public class AgentChatServiceImpl implements AgentChatService {
String query = (String) sessionData.get("query"); String query = (String) sessionData.get("query");
String userId = (String) sessionData.get("userId"); String userId = (String) sessionData.get("userId");
String apiKey = (String) sessionData.get("apiKey"); String apiKey = (String) sessionData.get("apiKey");
String service = (String) sessionData.get("service");
Boolean outer = (Boolean) sessionData.get("outer"); @SuppressWarnings("unchecked")
Boolean isGuest = (Boolean) sessionData.get("isGuest"); Map<String, Object> inputsMap = (Map<String, Object>) sessionData.get("inputsMap");
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<DifyFileInfo> filesData = (List<DifyFileInfo>) sessionData.get("filesData"); List<DifyFileInfo> filesData = (List<DifyFileInfo>) sessionData.get("filesData");
@@ -352,7 +384,7 @@ public class AgentChatServiceImpl implements AgentChatService {
userMessage.setChatId(chatId); userMessage.setChatId(chatId);
userMessage.setRole("user"); userMessage.setRole("user");
userMessage.setContent(query); userMessage.setContent(query);
// 提取系统文件ID列表保存到消息中 // 提取系统文件ID列表保存到消息中
if (filesData != null && !filesData.isEmpty()) { if (filesData != null && !filesData.isEmpty()) {
List<String> sysFileIds = filesData.stream() List<String> sysFileIds = filesData.stream()
@@ -363,7 +395,7 @@ public class AgentChatServiceImpl implements AgentChatService {
userMessage.setFiles(sysFileIds); userMessage.setFiles(sysFileIds);
} }
} }
chatMessageMapper.insertChatMessage(userMessage); chatMessageMapper.insertChatMessage(userMessage);
// 5. 构建Dify请求 // 5. 构建Dify请求
@@ -371,23 +403,12 @@ public class AgentChatServiceImpl implements AgentChatService {
chatRequest.setQuery(query); chatRequest.setQuery(query);
chatRequest.setUser(userId); chatRequest.setUser(userId);
chatRequest.setResponseMode("streaming"); chatRequest.setResponseMode("streaming");
Map<String, Object> 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<TbKnowledge> knowledgeRD = knowledgeService.listKnowledges(filter);
List<String> 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()) { if (filesData != null && !filesData.isEmpty()) {
chatRequest.setFiles(filesData); chatRequest.setFiles(filesData);
@@ -466,6 +487,109 @@ public class AgentChatServiceImpl implements AgentChatService {
return emitter; return emitter;
} }
@Override
public ResultDomain<String> blockingChatMessageWithSession(String sessionId) {
try {
// 1. 从Redis获取会话数据
String cacheKey = CHAT_SESSION_PREFIX + sessionId;
@SuppressWarnings("unchecked")
Map<String, Object> 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<String, Object> inputsMap = (Map<String, Object>) sessionData.get("inputsMap");
@SuppressWarnings("unchecked")
List<DifyFileInfo> filesData = (List<DifyFileInfo>) 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<String> 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 @Override
public ResultDomain<Boolean> stopChatMessageByTaskId(TbChat filter, String taskId) { public ResultDomain<Boolean> stopChatMessageByTaskId(TbChat filter, String taskId) {
// 1. 获取智能体 // 1. 获取智能体
@@ -534,4 +658,116 @@ public class AgentChatServiceImpl implements AgentChatService {
return ResultDomain.failure("评价失败"); return ResultDomain.failure("评价失败");
} }
@Override
public ResultDomain<String> runWorkflowWithSession(String sessionId) {
try {
// 1. 从Redis获取会话数据使用workflow前缀
String cacheKey = WORKFLOW_SESSION_PREFIX + sessionId;
Map<String, Object> 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<String, Object> inputsMap = (Map<String, Object>) 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<String, Object> 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<String> runWorkflowBlocking(String agentId, Map<String, Object> inputs, String userId) {
try {
// 1. 获取智能体信息
ResultDomain<TbAgent> 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<String, Object> 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());
}
}
} }

View File

@@ -2,6 +2,7 @@ package org.xyzh.api.ai.dto;
import java.io.Serializable; import java.io.Serializable;
import java.util.List; import java.util.List;
import java.util.Map;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -31,4 +32,10 @@ public class ChatPrepareData implements Serializable {
@Schema(description = "服务名称") @Schema(description = "服务名称")
private String service; private String service;
@Schema(description = "智能体输入参数,不同智能体可能需要不同的输入参数")
private Map<String, Object> inputsMap;
@Schema(description = "应用类型chat=对话应用workflow=工作流应用默认为chat")
private String appType = "chat";
} }

View File

@@ -74,6 +74,13 @@ public interface AgentChatService {
*/ */
SseEmitter streamChatMessageWithSse(String sessionId); SseEmitter streamChatMessageWithSse(String sessionId);
/**
* 阻塞式对话 - 使用sessionId进行同步调用等待完整结果返回
* @param sessionId 会话标识
* @return ResultDomain<String> 返回AI回复的完整内容
*/
ResultDomain<String> blockingChatMessageWithSession(String sessionId);
/** /**
* 停止对话生成通过Dify TaskID * 停止对话生成通过Dify TaskID
* @param filter 会话过滤条件包含agentId, userId, userType * @param filter 会话过滤条件包含agentId, userId, userType
@@ -91,5 +98,23 @@ public interface AgentChatService {
*/ */
ResultDomain<Boolean> commentChatMessage(TbChat filter, String messageId, String comment); ResultDomain<Boolean> commentChatMessage(TbChat filter, String messageId, String comment);
// ====================== 工作流执行 ======================
/**
* 工作流执行(阻塞模式)- 使用sessionId进行同步调用等待完整结果返回
* @param sessionId 会话标识从prepareChatMessageSession返回
* @return ResultDomain<String> 返回工作流输出结果的JSON字符串
*/
ResultDomain<String> runWorkflowWithSession(String sessionId);
/**
* 执行工作流(阻塞模式)- 直接传入inputs执行工作流
* @param agentId 智能体ID用于获取API Key
* @param inputs 工作流输入参数
* @param userId 用户标识
* @return ResultDomain<String> 返回工作流输出结果的JSON字符串
*/
ResultDomain<String> runWorkflowBlocking(String agentId, java.util.Map<String, Object> inputs, String userId);
} }

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.xyzh</groupId>
<artifactId>apis</artifactId>
<version>1.0.0</version>
</parent>
<groupId>org.xyzh.apis</groupId>
<artifactId>api-log</artifactId>
<version>${urban-lifeline.version}</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
</project>

View File

@@ -59,5 +59,5 @@ public class MessageReceiverVO extends BaseVO {
private Date handleTime; private Date handleTime;
@Schema(description = "服务类型") @Schema(description = "服务类型")
private String serviceType; private String service;
} }

View File

@@ -1,8 +1,81 @@
package org.xyzh.api.system.constance; package org.xyzh.api.system.constance;
import java.util.HashMap;
import java.util.Map;
/** /**
* 通过redis事件实现数据库更新配置更新其他服务的bean数据 * 通过redis事件实现数据库更新配置更新其他服务的bean数据
* 按配置分组定义Redis前缀
*
* 注意其他服务接收到事件后需延时2秒再查询数据库等待发布方事务提交
*
* @see org.xyzh.common.redis.listener.AbstractSysConfigListener
*/ */
public class SysConfigRedisPrefix { 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<String, String> 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);
}
} }

View File

@@ -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 = "文件";
}

View File

@@ -1,4 +1,4 @@
package org.xyzh.api.log.dto; package org.xyzh.api.system.dto;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
@@ -48,4 +48,10 @@ public class TbSysLogDTO extends BaseDTO {
@Schema(description = "日志数据") @Schema(description = "日志数据")
private JSONObject data; private JSONObject data;
@Schema(description = "创建人姓名")
private String creatorName;
@Schema(description = "服务")
private String servce;
} }

View File

@@ -1,4 +1,4 @@
package org.xyzh.api.log.dto; package org.xyzh.api.system.dto;
import java.util.Date; import java.util.Date;
import com.alibaba.fastjson2.annotation.JSONField; import com.alibaba.fastjson2.annotation.JSONField;

View File

@@ -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<TbSysLogDTO> addSysLog(TbSysLogDTO sysLog);
/**
* @description 统计日志数量
* @param filter
* @return 返回值描述
* @author yslg
* @since 2026-01-01
*/
ResultDomain<Integer> countSysLog(TbSysLogDTO filter);
/**
* @description 获取日志列表
* @param filter
* @return 日志列表
* @author yslg
* @since 2026-01-01
*/
ResultDomain<TbSysLogDTO> getSysLogList(TbSysLogDTO filter);
/**
* @description 获取日志分页
* @param pageRequest
* @return 日志分页
* @author yslg
* @since 2026-01-01
*/
ResultDomain<TbSysLogDTO> getSysLogPage(PageRequest<TbSysLogDTO> pageRequest);
}

View File

@@ -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;
}

View File

@@ -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<String> needs;
@Schema(description = "解决方案或答案")
private String answer;
@Schema(description = "词云关键词列表")
private List<String> workcloud;
@Schema(description = "聊天室ID")
private String roomId;
@Schema(description = "总结生成时间")
private String summaryTime;
@Schema(description = "参与总结的消息数量")
private Integer messageCount;
}

View File

@@ -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<String> needs;
@Schema(description = "解决方案")
private String answer;
@Schema(description = "词云关键词数组")
private List<String> workcloud;
@Schema(description = "参与总结的消息数量")
private Integer messageCount;
@Schema(description = "总结生成时间")
private String summaryTime;
}

View File

@@ -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<ChatRoomSummaryResponse> summaryChatRoom(ChatRoomSummaryRequest request);
/**
* @description 获取聊天室最新的总结
* @param roomId 聊天室ID
* @return 总结结果
* @author system
* @since 2026-01-01
*/
ResultDomain<ChatRoomSummaryResponse> getLatestSummary(String roomId);
}

View File

@@ -72,6 +72,14 @@ public interface ChatRoomService {
*/ */
ResultDomain<ChatRoomVO> getChatRoomPage(PageRequest<TbChatRoomDTO> pageRequest, String userId); ResultDomain<ChatRoomVO> getChatRoomPage(PageRequest<TbChatRoomDTO> pageRequest, String userId);
/**
* @description 统计聊天室数量
* @param filter 筛选条件
* @author yslg
* @since 2026-01-01
*/
ResultDomain<Long> countChatRooms(TbChatRoomDTO filter);
// ========================= 聊天室成员管理 ========================== // ========================= 聊天室成员管理 ==========================
/** /**

View File

@@ -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<TbWordCloudDTO> getWordCloudList(TbWordCloudDTO filter);
/**
* 分页查询词云
* @param pageRequest 分页请求
* @return 词云分页数据
*/
ResultDomain<TbWordCloudDTO> getWordCloudPage(PageRequest<TbWordCloudDTO> pageRequest);
}

View File

@@ -55,6 +55,22 @@ public interface WorkcaseService {
*/ */
ResultDomain<TbWorkcaseDTO> getWorkcasePage(PageRequest<TbWorkcaseDTO> pageRequest); ResultDomain<TbWorkcaseDTO> getWorkcasePage(PageRequest<TbWorkcaseDTO> pageRequest);
/**
* @description 统计各个类型的工单数量
* @param filter
* @author yslg
* @since 2026-01-01
*/
ResultDomain<TbWorkcaseDTO> countWorkcasesByType(TbWorkcaseDTO filter);
/**
* @description 统计工单数量
* @param filter
* @author yslg
* @since 2026-01-01
*/
ResultDomain<Long> countWorkcases(TbWorkcaseDTO filter);
/** /**
* @description 获取工单详情 * @description 获取工单详情
* @param workcaseId * @param workcaseId

View File

@@ -18,7 +18,6 @@
<module>api-auth</module> <module>api-auth</module>
<module>api-file</module> <module>api-file</module>
<module>api-message</module> <module>api-message</module>
<module>api-log</module>
<module>api-system</module> <module>api-system</module>
<module>api-crontab</module> <module>api-crontab</module>
<module>api-ai</module> <module>api-ai</module>
@@ -59,11 +58,6 @@
<artifactId>api-system</artifactId> <artifactId>api-system</artifactId>
<version>${urban-lifeline.version}</version> <version>${urban-lifeline.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.xyzh.apis</groupId>
<artifactId>api-log</artifactId>
<version>${urban-lifeline.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

@@ -50,6 +50,9 @@ public class BaseDTO implements Serializable {
@Schema(description = "数量限制") @Schema(description = "数量限制")
private Integer limit; private Integer limit;
@Schema(description = "统计数量")
private Integer count;
@Schema(description = "开始时间") @Schema(description = "开始时间")
private Date startTime; private Date startTime;

View File

@@ -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();
}

View File

@@ -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: []

View File

@@ -1,111 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.xyzh</groupId>
<artifactId>urban-lifeline</artifactId>
<version>1.0.0</version>
</parent>
<groupId>org.xyzh</groupId>
<artifactId>log</artifactId>
<version>${urban-lifeline.version}</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.xyzh.apis</groupId>
<artifactId>api-log</artifactId>
<version>${urban-lifeline.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Log4j2 日志 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- Dubbo Spring Boot Starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!-- Dubbo Nacos Spring Boot Starter (服务注册与发现) -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-nacos-spring-boot-starter</artifactId>
</dependency>
<!-- Spring Cloud Alibaba Nacos Discovery (可选,用于服务发现) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-logback-adapter-12</artifactId>
</exclusion>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>logback-adapter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<!-- MyBatis-Plus (可选,如不需要增强 CRUD 可移除) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.plus.version}</version>
<exclusions>
<!-- 排除旧版 mybatis-spring防止与 3.x 冲突 -->
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 强制引入与 Spring Boot 3.x 匹配的 mybatis-spring 3.x -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis.spring.version}</version>
</dependency>
<!-- SpringDoc OpenAPI -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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 启动成功 =========================");
}
}

View File

@@ -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}")));
}
}

View File

@@ -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

View File

@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="WARN" monitorInterval="30">
<Properties>
<property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />
<property name="FILE_PATH" value="./logs" />
<property name="FILE_NAME" value="log-service" />
<property name="file.encoding" value="UTF-8" />
<property name="console.encoding" value="UTF-8" />
</Properties>
<appenders>
<console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
<ThresholdFilter level="trace" onMatch="ACCEPT" onMismatch="DENY"/>
</console>
<RollingFile name="RollingFile" fileName="${FILE_PATH}/${FILE_NAME}.log"
filePattern="${FILE_PATH}/${FILE_NAME}-%d{yyyy-MM-dd}_%i.log.gz">
<ThresholdFilter level="trace" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${LOG_PATTERN}" charset="UTF-8"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="100MB"/>
</Policies>
<DefaultRolloverStrategy max="30"/>
</RollingFile>
</appenders>
<loggers>
<logger name="com.alibaba.nacos" level="info" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</logger>
<logger name="org.mybatis" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</logger>
<Logger name="org.springframework" level="info" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Logger>
<Logger name="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" level="TRACE" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Logger>
<Logger name="org.xyzh.log" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Logger>
<Logger name="org.xyzh.common" level="debug" additivity="false">
<AppenderRef ref="Console"/>
<AppenderRef ref="RollingFile"/>
</Logger>
<root level="info">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFile"/>
</root>
</loggers>
</configuration>

View File

@@ -11,7 +11,6 @@
<module>common</module> <module>common</module>
<module>apis</module> <module>apis</module>
<module>gateway</module> <module>gateway</module>
<module>log</module>
<module>system</module> <module>system</module>
<module>auth</module> <module>auth</module>
<module>file</module> <module>file</module>

View File

@@ -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<TbSysLogDTO> addSysLog(@RequestBody TbSysLogDTO sysLog) {
return logService.addSysLog(sysLog);
}
/**
* 统计日志数量
*/
@PostMapping("/count")
@PreAuthorize("hasAuthority('log:log:view')")
public ResultDomain<Integer> countSysLog(@RequestBody TbSysLogDTO filter) {
return logService.countSysLog(filter);
}
/**
* 获取日志列表
*/
@PostMapping("/list")
@PreAuthorize("hasAuthority('log:log:view')")
public ResultDomain<TbSysLogDTO> getSysLogList(@RequestBody TbSysLogDTO filter) {
return logService.getSysLogList(filter);
}
/**
* 分页查询日志
*/
@PostMapping("/page")
@PreAuthorize("hasAuthority('log:log:view')")
public ResultDomain<TbSysLogDTO> getSysLogPage(@RequestBody PageRequest<TbSysLogDTO> pageRequest) {
return logService.getSysLogPage(pageRequest);
}
}

View File

@@ -1,11 +1,11 @@
package org.xyzh.log.mapper.log; package org.xyzh.system.mapper.log;
import java.util.List; import java.util.List;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.xyzh.api.system.dto.TbSysLogDTO;
import org.xyzh.common.core.page.PageParam; import org.xyzh.common.core.page.PageParam;
import org.xyzh.api.log.dto.TbSysLogDTO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;

View File

@@ -1,11 +1,11 @@
package org.xyzh.log.mapper.log; package org.xyzh.system.mapper.log;
import java.util.List; import java.util.List;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.xyzh.api.system.dto.TbSysLoginLogDTO;
import org.xyzh.common.core.page.PageParam; import org.xyzh.common.core.page.PageParam;
import org.xyzh.api.log.dto.TbSysLoginLogDTO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;

View File

@@ -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<TbSysLogDTO> 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<Integer> 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<TbSysLogDTO> getSysLogList(TbSysLogDTO filter) {
try {
List<TbSysLogDTO> 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<TbSysLogDTO> getSysLogPage(PageRequest<TbSysLogDTO> 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<TbSysLogDTO> data = sysLogMapper.getLogPageByFilter(filter, pageParam);
if (data == null) {
data = Collections.emptyList();
}
PageDomain<TbSysLogDTO> pageDomain = new PageDomain<>(pageParam, data);
return ResultDomain.success("分页查询日志成功", pageDomain);
} catch (Exception e) {
logger.error("分页查询日志异常", e);
return ResultDomain.failure("分页查询日志异常: " + e.getMessage());
}
}
}

View File

@@ -17,8 +17,13 @@ import org.xyzh.common.core.page.PageDomain;
import org.xyzh.common.utils.id.IdUtil; import org.xyzh.common.utils.id.IdUtil;
import org.xyzh.common.utils.StringUtils; 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 org.xyzh.system.mapper.config.TbSysConfigMapper;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** /**
* @description 系统配置服务实现类 * @description 系统配置服务实现类
* @filename SysConfigServiceImpl.java * @filename SysConfigServiceImpl.java
@@ -41,6 +46,37 @@ public class SysConfigServiceImpl implements SysConfigService {
@Resource @Resource
private TbSysConfigMapper configMapper; 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查询配置 * 根据key查询配置
*/ */
@@ -242,6 +278,8 @@ public class SysConfigServiceImpl implements SysConfigService {
int rows = configMapper.insertConfig(configDTO); int rows = configMapper.insertConfig(configDTO);
if (rows > 0) { if (rows > 0) {
logger.info("新增配置成功, configId={}", configDTO.getConfigId()); logger.info("新增配置成功, configId={}", configDTO.getConfigId());
// 异步发布配置变更事件
publishConfigChangeEvent(configDTO.getGroup());
return ResultDomain.success("新增配置成功", configDTO); return ResultDomain.success("新增配置成功", configDTO);
} }
logger.warn("新增配置失败, configId={}", configDTO.getConfigId()); logger.warn("新增配置失败, configId={}", configDTO.getConfigId());
@@ -257,6 +295,8 @@ public class SysConfigServiceImpl implements SysConfigService {
int rows = configMapper.updateConfig(configDTO); int rows = configMapper.updateConfig(configDTO);
if (rows > 0) { if (rows > 0) {
logger.info("更新配置成功, configId={}", configDTO.getConfigId()); logger.info("更新配置成功, configId={}", configDTO.getConfigId());
// 异步发布配置变更事件
publishConfigChangeEvent(configDTO.getGroup());
return ResultDomain.success("更新配置成功", configDTO); return ResultDomain.success("更新配置成功", configDTO);
} }
logger.warn("更新配置失败, configId={}", configDTO.getConfigId()); logger.warn("更新配置失败, configId={}", configDTO.getConfigId());
@@ -271,6 +311,8 @@ public class SysConfigServiceImpl implements SysConfigService {
int rows = configMapper.deleteConfig(configDTO); int rows = configMapper.deleteConfig(configDTO);
if (rows > 0) { if (rows > 0) {
logger.info("删除配置成功, configId={}", configDTO.getConfigId()); logger.info("删除配置成功, configId={}", configDTO.getConfigId());
// 异步发布配置变更事件
publishConfigChangeEvent(configDTO.getGroup());
return ResultDomain.success("删除配置成功", Boolean.TRUE); return ResultDomain.success("删除配置成功", Boolean.TRUE);
} }
logger.warn("删除配置失败, configId={}", configDTO.getConfigId()); logger.warn("删除配置失败, configId={}", configDTO.getConfigId());

View File

@@ -0,0 +1,254 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.system.mapper.log.TbSysLogMapper">
<!-- 结果映射 -->
<resultMap id="BaseResultMap" type="org.xyzh.api.system.dto.TbSysLogDTO">
<id column="log_id" property="logId" jdbcType="VARCHAR"/>
<result column="optsn" property="optsn" jdbcType="VARCHAR"/>
<result column="type" property="type" jdbcType="INTEGER"/>
<result column="level" property="level" jdbcType="VARCHAR"/>
<result column="module" property="module" jdbcType="VARCHAR"/>
<result column="ip_address" property="ipAddress" jdbcType="VARCHAR"/>
<result column="ip_source" property="ip_source" jdbcType="VARCHAR"/>
<result column="browser" property="browser" jdbcType="VARCHAR"/>
<result column="os" property="os" jdbcType="VARCHAR"/>
<result column="message" property="message" jdbcType="VARCHAR"/>
<result column="data" property="data" typeHandler="org.xyzh.common.jdbc.handler.FastJson2TypeHandler"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="creator_name" property="creatorName" jdbcType="VARCHAR"/>
<result column="service" property="servce" jdbcType="VARCHAR"/>
<result column="dept_path" property="deptPath" jdbcType="VARCHAR"/>
<result column="updater" property="updater" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="delete_time" property="deleteTime" jdbcType="TIMESTAMP"/>
<result column="deleted" property="deleted" jdbcType="BOOLEAN"/>
</resultMap>
<!-- 基础列 -->
<sql id="Base_Column_List">
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
</sql>
<!-- 插入系统日志 -->
<insert id="insertLog" parameterType="org.xyzh.api.system.dto.TbSysLogDTO">
INSERT INTO sys.tb_sys_log
<trim prefix="(" suffix=")" suffixOverrides=",">
log_id,
optsn,
type,
level,
module,
message,
service,
<if test="ipAddress != null and ipAddress != ''">ip_address,</if>
<if test="ip_source != null and ip_source != ''">ip_source,</if>
<if test="browser != null and browser != ''">browser,</if>
<if test="os != null and os != ''">os,</if>
<if test="data != null">data,</if>
<if test="creator != null and creator != ''">creator,</if>
<if test="creatorName != null and creatorName != ''">creator_name,</if>
<if test="deptPath != null and deptPath != ''">dept_path,</if>
<if test="updater != null and updater != ''">updater,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
<if test="deleteTime != null">delete_time,</if>
<if test="deleted != null">deleted,</if>
</trim>
VALUES
<trim prefix="(" suffix=")" suffixOverrides=",">
#{logId},
#{optsn},
#{type},
#{level},
#{module},
#{message},
#{servce},
<if test="ipAddress != null and ipAddress != ''">#{ipAddress},</if>
<if test="ip_source != null and ip_source != ''">#{ip_source},</if>
<if test="browser != null and browser != ''">#{browser},</if>
<if test="os != null and os != ''">#{os},</if>
<if test="data != null">#{data, typeHandler=org.xyzh.common.jdbc.handler.FastJson2TypeHandler},</if>
<if test="creator != null and creator != ''">#{creator},</if>
<if test="creatorName != null and creatorName != ''">#{creatorName},</if>
<if test="deptPath != null and deptPath != ''">#{deptPath},</if>
<if test="updater != null and updater != ''">#{updater},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="deleteTime != null">#{deleteTime},</if>
<if test="deleted != null">#{deleted},</if>
</trim>
</insert>
<!-- 更新系统日志 -->
<update id="updateLog" parameterType="org.xyzh.api.system.dto.TbSysLogDTO">
UPDATE sys.tb_sys_log
<set>
<if test="type != null">type = #{type},</if>
<if test="level != null and level != ''">level = #{level},</if>
<if test="module != null and module != ''">module = #{module},</if>
<if test="ipAddress != null and ipAddress != ''">ip_address = #{ipAddress},</if>
<if test="ip_source != null and ip_source != ''">ip_source = #{ip_source},</if>
<if test="browser != null and browser != ''">browser = #{browser},</if>
<if test="os != null and os != ''">os = #{os},</if>
<if test="message != null and message != ''">message = #{message},</if>
<if test="data != null">data = #{data, typeHandler=org.xyzh.common.jdbc.handler.FastJson2TypeHandler},</if>
<if test="servce != null and servce != ''">service = #{servce},</if>
<if test="updater != null and updater != ''">updater = #{updater},</if>
<if test="deptPath != null and deptPath != ''">dept_path = #{deptPath},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="deleteTime != null">delete_time = #{deleteTime},</if>
<if test="deleted != null">deleted = #{deleted},</if>
</set>
WHERE log_id = #{logId}
</update>
<!-- 删除系统日志 -->
<update id="deleteLog" parameterType="org.xyzh.api.system.dto.TbSysLogDTO">
UPDATE sys.tb_sys_log
SET deleted = true,
delete_time = NOW()
WHERE log_id = #{logId}
</update>
<!-- 根据日志ID查询系统日志 -->
<select id="getLogById" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM sys.tb_sys_log
WHERE log_id = #{logId}
AND (deleted IS NULL OR deleted = false)
</select>
<!-- 根据条件查询系统日志列表 -->
<select id="getLogByFilter" resultMap="BaseResultMap" parameterType="org.xyzh.api.system.dto.TbSysLogDTO">
SELECT <include refid="Base_Column_List" />
FROM sys.tb_sys_log
<where>
<if test="filter.logId != null and filter.logId != ''">
AND log_id = #{filter.logId}
</if>
<if test="filter.type != null">
AND type = #{filter.type}
</if>
<if test="filter.level != null and filter.level != ''">
AND level = #{filter.level}
</if>
<if test="filter.module != null and filter.module != ''">
AND module = #{filter.module}
</if>
<if test="filter.ipAddress != null and filter.ipAddress != ''">
AND ip_address = #{filter.ipAddress}
</if>
<if test="filter.creator != null and filter.creator != ''">
AND creator = #{filter.creator}
</if>
<if test="filter.servce != null and filter.servce != ''">
AND service = #{filter.servce}
</if>
<if test="filter.deptPath != null and filter.deptPath != ''">
AND dept_path LIKE CONCAT(#{filter.deptPath}, '%')
</if>
<if test="filter.message != null and filter.message != ''">
AND message LIKE CONCAT('%', #{filter.message}, '%')
</if>
<if test="filter.startTime != null">
AND create_time &gt;= #{filter.startTime}
</if>
<if test="filter.endTime != null">
AND create_time &lt;= #{filter.endTime}
</if>
AND (deleted IS NULL OR deleted = false)
</where>
ORDER BY create_time DESC
</select>
<!-- 根据条件查询系统日志分页列表 -->
<select id="getLogPageByFilter" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM sys.tb_sys_log
<where>
<if test="filter.logId != null and filter.logId != ''">
AND log_id = #{filter.logId}
</if>
<if test="filter.type != null">
AND type = #{filter.type}
</if>
<if test="filter.level != null and filter.level != ''">
AND level = #{filter.level}
</if>
<if test="filter.module != null and filter.module != ''">
AND module = #{filter.module}
</if>
<if test="filter.ipAddress != null and filter.ipAddress != ''">
AND ip_address = #{filter.ipAddress}
</if>
<if test="filter.creator != null and filter.creator != ''">
AND creator = #{filter.creator}
</if>
<if test="filter.servce != null and filter.servce != ''">
AND service = #{filter.servce}
</if>
<if test="filter.deptPath != null and filter.deptPath != ''">
AND dept_path LIKE CONCAT(#{filter.deptPath}, '%')
</if>
<if test="filter.message != null and filter.message != ''">
AND message LIKE CONCAT('%', #{filter.message}, '%')
</if>
<if test="filter.startTime != null">
AND create_time &gt;= #{filter.startTime}
</if>
<if test="filter.endTime != null">
AND create_time &lt;= #{filter.endTime}
</if>
AND (deleted IS NULL OR deleted = false)
</where>
ORDER BY create_time DESC
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
</select>
<!-- 根据条件查询系统日志数量 -->
<select id="getLogCount" resultType="java.lang.Integer" parameterType="org.xyzh.api.system.dto.TbSysLogDTO">
SELECT COUNT(1)
FROM sys.tb_sys_log
<where>
<if test="logId != null and logId != ''">
AND log_id = #{logId}
</if>
<if test="type != null">
AND type = #{type}
</if>
<if test="level != null and level != ''">
AND level = #{level}
</if>
<if test="module != null and module != ''">
AND module = #{module}
</if>
<if test="ipAddress != null and ipAddress != ''">
AND ip_address = #{ipAddress}
</if>
<if test="creator != null and creator != ''">
AND creator = #{creator}
</if>
<if test="servce != null and servce != ''">
AND service = #{servce}
</if>
<if test="deptPath != null and deptPath != ''">
AND dept_path LIKE CONCAT(#{deptPath}, '%')
</if>
<if test="message != null and message != ''">
AND message LIKE CONCAT('%', #{message}, '%')
</if>
<if test="startTime != null">
AND create_time &gt;= #{startTime}
</if>
<if test="endTime != null">
AND create_time &lt;= #{endTime}
</if>
AND (deleted IS NULL OR deleted = false)
</where>
</select>
</mapper>

View File

@@ -0,0 +1,186 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.system.mapper.log.TbSysLoginLogMapper">
<!-- 结果映射 -->
<resultMap id="BaseResultMap" type="org.xyzh.api.system.dto.TbSysLoginLogDTO">
<id column="optsn" property="optsn" jdbcType="VARCHAR"/>
<result column="user_id" property="userId" jdbcType="VARCHAR"/>
<result column="username" property="username" jdbcType="VARCHAR"/>
<result column="ip_address" property="ipAddress" jdbcType="VARCHAR"/>
<result column="ip_source" property="ipSource" jdbcType="VARCHAR"/>
<result column="browser" property="browser" jdbcType="VARCHAR"/>
<result column="os" property="os" jdbcType="VARCHAR"/>
<result column="password" property="password" jdbcType="VARCHAR"/>
<result column="login_time" property="loginTime" jdbcType="TIMESTAMP"/>
<result column="status" property="status" jdbcType="INTEGER"/>
<result column="error_count" property="errorCount" jdbcType="INTEGER"/>
<result column="message" property="message" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
</resultMap>
<!-- 基础列 -->
<sql id="Base_Column_List">
optsn, user_id, username, ip_address, ip_source, browser, os, password,
login_time, status, error_count, message, create_time
</sql>
<!-- 插入系统登录日志 -->
<insert id="insertLoginLog" parameterType="org.xyzh.api.system.dto.TbSysLoginLogDTO">
INSERT INTO sys.tb_sys_login_log
<trim prefix="(" suffix=")" suffixOverrides=",">
optsn,
user_id,
username,
<if test="ipAddress != null and ipAddress != ''">ip_address,</if>
<if test="ipSource != null and ipSource != ''">ip_source,</if>
<if test="browser != null and browser != ''">browser,</if>
<if test="os != null and os != ''">os,</if>
<if test="password != null and password != ''">password,</if>
<if test="loginTime != null">login_time,</if>
<if test="status != null">status,</if>
<if test="errorCount != null">error_count,</if>
<if test="message != null and message != ''">message,</if>
<if test="createTime != null">create_time,</if>
</trim>
VALUES
<trim prefix="(" suffix=")" suffixOverrides=",">
#{optsn},
#{userId},
#{username},
<if test="ipAddress != null and ipAddress != ''">#{ipAddress},</if>
<if test="ipSource != null and ipSource != ''">#{ipSource},</if>
<if test="browser != null and browser != ''">#{browser},</if>
<if test="os != null and os != ''">#{os},</if>
<if test="password != null and password != ''">#{password},</if>
<if test="loginTime != null">#{loginTime},</if>
<if test="status != null">#{status},</if>
<if test="errorCount != null">#{errorCount},</if>
<if test="message != null and message != ''">#{message},</if>
<if test="createTime != null">#{createTime},</if>
</trim>
</insert>
<!-- 更新系统登录日志 -->
<update id="updateLoginLog" parameterType="org.xyzh.api.system.dto.TbSysLoginLogDTO">
UPDATE sys.tb_sys_login_log
<set>
<if test="userId != null and userId != ''">user_id = #{userId},</if>
<if test="username != null and username != ''">username = #{username},</if>
<if test="ipAddress != null and ipAddress != ''">ip_address = #{ipAddress},</if>
<if test="ipSource != null and ipSource != ''">ip_source = #{ipSource},</if>
<if test="browser != null and browser != ''">browser = #{browser},</if>
<if test="os != null and os != ''">os = #{os},</if>
<if test="password != null and password != ''">password = #{password},</if>
<if test="loginTime != null">login_time = #{loginTime},</if>
<if test="status != null">status = #{status},</if>
<if test="errorCount != null">error_count = #{errorCount},</if>
<if test="message != null and message != ''">message = #{message},</if>
</set>
WHERE optsn = #{optsn}
</update>
<!-- 删除系统登录日志 -->
<delete id="deleteLoginLog" parameterType="org.xyzh.api.system.dto.TbSysLoginLogDTO">
DELETE FROM sys.tb_sys_login_log
WHERE optsn = #{optsn}
</delete>
<!-- 根据登录日志ID查询系统登录日志 -->
<select id="getLoginLogById" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM sys.tb_sys_login_log
WHERE optsn = #{loginLogId}
</select>
<!-- 根据条件查询系统登录日志列表 -->
<select id="getLoginLogByFilter" resultMap="BaseResultMap" parameterType="org.xyzh.api.system.dto.TbSysLoginLogDTO">
SELECT <include refid="Base_Column_List" />
FROM sys.tb_sys_login_log
<where>
<if test="filter.optsn != null and filter.optsn != ''">
AND optsn = #{filter.optsn}
</if>
<if test="filter.userId != null and filter.userId != ''">
AND user_id = #{filter.userId}
</if>
<if test="filter.username != null and filter.username != ''">
AND username LIKE CONCAT('%', #{filter.username}, '%')
</if>
<if test="filter.ipAddress != null and filter.ipAddress != ''">
AND ip_address = #{filter.ipAddress}
</if>
<if test="filter.status != null">
AND status = #{filter.status}
</if>
<if test="filter.startTime != null">
AND login_time &gt;= #{filter.startTime}
</if>
<if test="filter.endTime != null">
AND login_time &lt;= #{filter.endTime}
</if>
</where>
ORDER BY login_time DESC
</select>
<!-- 根据条件查询系统登录日志分页列表 -->
<select id="getLoginLogPageByFilter" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List" />
FROM sys.tb_sys_login_log
<where>
<if test="filter.optsn != null and filter.optsn != ''">
AND optsn = #{filter.optsn}
</if>
<if test="filter.userId != null and filter.userId != ''">
AND user_id = #{filter.userId}
</if>
<if test="filter.username != null and filter.username != ''">
AND username LIKE CONCAT('%', #{filter.username}, '%')
</if>
<if test="filter.ipAddress != null and filter.ipAddress != ''">
AND ip_address = #{filter.ipAddress}
</if>
<if test="filter.status != null">
AND status = #{filter.status}
</if>
<if test="filter.startTime != null">
AND login_time &gt;= #{filter.startTime}
</if>
<if test="filter.endTime != null">
AND login_time &lt;= #{filter.endTime}
</if>
</where>
ORDER BY login_time DESC
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
</select>
<!-- 根据条件查询系统登录日志数量 -->
<select id="getLoginLogCount" resultType="java.lang.Integer" parameterType="org.xyzh.api.system.dto.TbSysLoginLogDTO">
SELECT COUNT(1)
FROM sys.tb_sys_login_log
<where>
<if test="optsn != null and optsn != ''">
AND optsn = #{optsn}
</if>
<if test="userId != null and userId != ''">
AND user_id = #{userId}
</if>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="ipAddress != null and ipAddress != ''">
AND ip_address = #{ipAddress}
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="startTime != null">
AND login_time &gt;= #{startTime}
</if>
<if test="endTime != null">
AND login_time &lt;= #{endTime}
</if>
</where>
</select>
</mapper>

View File

@@ -66,6 +66,7 @@ public class AiInit {
@Bean @Bean
public CommandLineRunner agentInitRunner(){ public CommandLineRunner agentInitRunner(){
return args -> { return args -> {
String chatAgentApiKey = sysConfigService.getStringConfig("dify.workcase.agent.chat");
logger.info("开始初始化客服系统智能体..."); logger.info("开始初始化客服系统智能体...");
TbAgent agent = new TbAgent(); TbAgent agent = new TbAgent();
agent.setIsOuter(true); agent.setIsOuter(true);
@@ -73,17 +74,35 @@ public class AiInit {
ResultDomain<TbAgent> listDomain = agentService.getAgentList(agent); ResultDomain<TbAgent> listDomain = agentService.getAgentList(agent);
if (listDomain.getSuccess()&&!listDomain.getDataList().isEmpty()) { if (listDomain.getSuccess()&&!listDomain.getDataList().isEmpty()) {
logger.info("泰豪小电智能体已经存在"); logger.info("泰豪小电智能体已经存在");
return;
}
agent.setApiKey("app-CDKy0wYkPnl6dA6G7eu113Vw");
agent.setIntroduce("您好,我是泰豪小电智能客服。请描述您的问题,我会尽力协助。");
agent.setCategory("客服智能体");
agent.setCategory("user_admin");
ResultDomain<TbAgent> resultDomain = agentService.addAgent(agent);
if(resultDomain.getSuccess()){
logger.info("泰豪小电智能体初始化成功");
}else{ }else{
logger.error("泰豪小电智能体初始化失败"+resultDomain.getMessage()); agent.setApiKey(chatAgentApiKey);
agent.setIntroduce("您好,我是泰豪小电智能客服。请描述您的问题,我会尽力协助。");
agent.setCategory("客服智能体");
ResultDomain<TbAgent> resultDomain = agentService.addAgent(agent);
if(resultDomain.getSuccess()){
logger.info("泰豪小电智能体初始化成功");
}else{
logger.error("泰豪小电智能体初始化失败"+resultDomain.getMessage());
}
}
String summaryAgentApiKey = sysConfigService.getStringConfig("dify.workcase.workflow.summary");
TbAgent summaryAgent = new TbAgent();
summaryAgent.setIsOuter(true);
summaryAgent.setName("工单总结");
ResultDomain<TbAgent> listDomain2 = agentService.getAgentList(summaryAgent);
if (listDomain2.getSuccess()&&!listDomain2.getDataList().isEmpty()) {
logger.info("工单总结智能体已经存在");
}else {
summaryAgent.setApiKey(summaryAgentApiKey);
summaryAgent.setIntroduce("工单总结工作流");
summaryAgent.setCategory("工作流");
ResultDomain<TbAgent> resultDomain2 = agentService.addAgent(summaryAgent);
if(resultDomain2.getSuccess()){
logger.info("泰豪小电智能体初始化成功");
}else{
logger.error("泰豪小电智能体初始化失败"+resultDomain2.getMessage());
}
} }
}; };
} }

View File

@@ -21,6 +21,9 @@ import org.xyzh.api.workcase.dto.TbChatRoomMessageDTO;
import org.xyzh.api.workcase.dto.TbCustomerServiceDTO; import org.xyzh.api.workcase.dto.TbCustomerServiceDTO;
import org.xyzh.api.workcase.dto.TbVideoMeetingDTO; import org.xyzh.api.workcase.dto.TbVideoMeetingDTO;
import org.xyzh.api.workcase.dto.TbWordCloudDTO; 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.ChatRoomService;
import org.xyzh.api.workcase.service.WorkcaseChatService; import org.xyzh.api.workcase.service.WorkcaseChatService;
import org.xyzh.api.workcase.vo.ChatMemberVO; import org.xyzh.api.workcase.vo.ChatMemberVO;
@@ -68,6 +71,9 @@ public class WorkcaseChatController {
@Autowired @Autowired
private ChatRoomService chatRoomService; private ChatRoomService chatRoomService;
@Autowired
private AgentService agentService;
@Autowired @Autowired
private JwtTokenUtil jwtTokenUtil; private JwtTokenUtil jwtTokenUtil;
@@ -156,7 +162,7 @@ public class WorkcaseChatController {
if (!vr.isValid()) { if (!vr.isValid()) {
return ResultDomain.failure(vr.getAllErrors()); return ResultDomain.failure(vr.getAllErrors());
} }
LoginDomain loginDomain = LoginUtil.getCurrentLogin(); LoginDomain loginDomain = LoginUtil.getCurrentLogin();
String userId = loginDomain.getUser().getUserId(); String userId = loginDomain.getUser().getUserId();
if("guest".equals(loginDomain.getUser().getStatus())){ if("guest".equals(loginDomain.getUser().getStatus())){
@@ -165,6 +171,13 @@ public class WorkcaseChatController {
return chatRoomService.getChatRoomPage(pageRequest, userId); return chatRoomService.getChatRoomPage(pageRequest, userId);
} }
@Operation(summary = "统计聊天室数量")
@PreAuthorize("hasAuthority('workcase:room:view')")
@PostMapping("/room/count")
public ResultDomain<Long> countChatRooms(@RequestBody TbChatRoomDTO filter) {
return chatRoomService.countChatRooms(filter);
}
// ========================= ChatRoom成员管理 ========================= // ========================= ChatRoom成员管理 =========================
@Operation(summary = "添加聊天室成员") @Operation(summary = "添加聊天室成员")
@@ -243,6 +256,31 @@ public class WorkcaseChatController {
return chatRoomService.deleteMessage(messageId); return chatRoomService.deleteMessage(messageId);
} }
@Operation(summary = "获取聊天室最新总结")
@PreAuthorize("hasAuthority('workcase:room:view')")
@GetMapping("/room/{roomId}/summary")
public ResultDomain<ChatRoomSummaryResponse> getLatestSummary(@PathVariable(value = "roomId") String roomId) {
return agentService.getLatestSummary(roomId);
}
@Operation(summary = "生成聊天室对话总结")
@PreAuthorize("hasAuthority('workcase:room:view')")
@PostMapping("/room/{roomId}/summary")
public ResultDomain<ChatRoomSummaryResponse> 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 = "添加客服人员") @Operation(summary = "添加客服人员")

View File

@@ -19,6 +19,7 @@ import org.xyzh.common.auth.utils.LoginUtil;
import org.xyzh.common.core.domain.LoginDomain; import org.xyzh.common.core.domain.LoginDomain;
import org.xyzh.common.core.domain.ResultDomain; import org.xyzh.common.core.domain.ResultDomain;
import org.xyzh.common.core.page.PageRequest; 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.ValidationResult;
import org.xyzh.common.utils.validation.ValidationUtils; import org.xyzh.common.utils.validation.ValidationUtils;
@@ -26,6 +27,8 @@ import com.alibaba.fastjson2.JSONObject;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
/** /**
@@ -116,6 +119,49 @@ public class WorkcaseController {
return workcaseService.getWorkcasePage(pageRequest); return workcaseService.getWorkcasePage(pageRequest);
} }
@Operation(summary = "查询工单问题统计")
@PreAuthorize("hasAuthority('workcase:ticket:view')")
@PostMapping("/category/count")
public ResultDomain<TbWorkcaseDTO> 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<Long> countWorkcases(@RequestBody TbWorkcaseDTO workcase) {
return workcaseService.countWorkcases(workcase);
}
// ========================= CRM同步接口 ========================= // ========================= CRM同步接口 =========================
@Operation(summary = "同步工单到CRM") @Operation(summary = "同步工单到CRM")

View File

@@ -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<TbChatRoomSummaryDTO> selectChatRoomSummaryList(@Param("filter") TbChatRoomSummaryDTO filter);
/**
* 分页查询聊天室总结
*/
List<TbChatRoomSummaryDTO> selectChatRoomSummaryPage(@Param("filter") TbChatRoomSummaryDTO filter, @Param("pageParam") PageParam pageParam);
/**
* 统计聊天室总结数量
*/
long countChatRoomSummaries(@Param("filter") TbChatRoomSummaryDTO filter);
/**
* 删除聊天室总结(逻辑删除)
*/
int deleteChatRoomSummary(@Param("summaryId") String summaryId);
}

View File

@@ -52,4 +52,6 @@ public interface TbWorkcaseMapper {
*/ */
long countWorkcases(@Param("filter") TbWorkcaseDTO filter); long countWorkcases(@Param("filter") TbWorkcaseDTO filter);
List<TbWorkcaseDTO> countWorkcasesByType(@Param("filter") TbWorkcaseDTO filter);
} }

View File

@@ -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<ChatRoomSummaryResponse> 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<TbAgent> 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<ChatRoomMessageVO> messages = chatMessageMapper.selectChatMessageList(filter);
if (messages == null || messages.isEmpty()) {
return ResultDomain.failure("聊天室没有消息");
}
// 4. 过滤消息(根据请求参数)
List<Map<String, Object>> 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<String, Object> 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<String, Object> inputsMap = new HashMap<>();
inputsMap.put("chatMessages", chatMessagesJson); // 工作流期望的输入参数名
prepareData.setInputsMap(inputsMap);
logger.info("准备工作流会话,输入参数: chatMessages长度={}", chatMessagesJson.length());
// 8. 调用准备会话
ResultDomain<String> 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<String> workflowResult = agentChatService.runWorkflowWithSession(sessionId);
if (!workflowResult.getSuccess()) {
logger.error("工作流执行失败: {}", workflowResult.getMessage());
return ResultDomain.failure("总结失败: " + workflowResult.getMessage());
}
String outputsJson = workflowResult.getData();
logger.debug("工作流返回结果: {}", outputsJson);
// 10. 解析工作流返回的outputsJSON格式
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<String> needs = resultJson.getList("needs", String.class);
response.setNeeds(needs != null ? needs : new ArrayList<>());
response.setAnswer(resultJson.getString("answer"));
// 安全获取workcloud数组
List<String> 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<String> 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<String> 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<String> 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<ChatRoomSummaryResponse> 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());
}
}
}

View File

@@ -219,6 +219,12 @@ public class ChatRoomServiceImpl implements ChatRoomService {
return ResultDomain.success("查询聊天室成功", pageDomain); return ResultDomain.success("查询聊天室成功", pageDomain);
} }
@Override
public ResultDomain<Long> countChatRooms(TbChatRoomDTO filter) {
long count = chatRoomMapper.countChatRooms(filter);
return ResultDomain.success("查询成功", count);
}
// ========================= 聊天室成员管理 ========================== // ========================= 聊天室成员管理 ==========================
@Override @Override

View File

@@ -227,6 +227,18 @@ public class WorkcaseServiceImpl implements WorkcaseService {
return ResultDomain.success("查询成功", pageDomain); return ResultDomain.success("查询成功", pageDomain);
} }
@Override
public ResultDomain<TbWorkcaseDTO> countWorkcasesByType(TbWorkcaseDTO filter) {
List<TbWorkcaseDTO> workcases = workcaseMapper.countWorkcasesByType(filter);
return ResultDomain.success("查询成功", workcases);
}
@Override
public ResultDomain<Long> countWorkcases(TbWorkcaseDTO filter) {
long count = workcaseMapper.countWorkcases(filter);
return ResultDomain.success("查询成功", count);
}
// ====================== 同步到CRM和接收 =================== // ====================== 同步到CRM和接收 ===================
@Override @Override

View File

@@ -158,6 +158,9 @@
<if test="filter.status != null and filter.status != ''">AND status = #{filter.status}</if> <if test="filter.status != null and filter.status != ''">AND status = #{filter.status}</if>
<if test="filter.guestId != null and filter.guestId != ''">AND guest_id = #{filter.guestId}</if> <if test="filter.guestId != null and filter.guestId != ''">AND guest_id = #{filter.guestId}</if>
<if test="filter.guestName != null and filter.guestName != ''">AND guest_name LIKE CONCAT('%', #{filter.guestName}, '%')</if> <if test="filter.guestName != null and filter.guestName != ''">AND guest_name LIKE CONCAT('%', #{filter.guestName}, '%')</if>
<if test="filter.startTime != null and filter.endTime != null">
AND create_time BETWEEN #{filter.startTime} AND #{filter.endTime}
</if>
AND deleted = false AND deleted = false
</where> </where>
</select> </select>

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.xyzh.workcase.mapper.TbChatRoomSummaryMapper">
<resultMap id="BaseResultMap" type="org.xyzh.api.workcase.dto.TbChatRoomSummaryDTO">
<id column="summary_id" property="summaryId" jdbcType="VARCHAR"/>
<result column="room_id" property="roomId" jdbcType="VARCHAR"/>
<result column="question" property="question" jdbcType="VARCHAR"/>
<result column="needs" property="needs" jdbcType="ARRAY" typeHandler="org.xyzh.common.jdbc.handler.StringArrayTypeHandler"/>
<result column="answer" property="answer" jdbcType="VARCHAR"/>
<result column="workcloud" property="workcloud" jdbcType="ARRAY" typeHandler="org.xyzh.common.jdbc.handler.StringArrayTypeHandler"/>
<result column="message_count" property="messageCount" jdbcType="INTEGER"/>
<result column="summary_time" property="summaryTime" jdbcType="TIMESTAMP"/>
<result column="optsn" property="optsn" jdbcType="VARCHAR"/>
<result column="creator" property="creator" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
<result column="delete_time" property="deleteTime" jdbcType="TIMESTAMP"/>
<result column="deleted" property="deleted" jdbcType="BOOLEAN"/>
</resultMap>
<sql id="Base_Column_List">
summary_id, room_id, question, needs, answer, workcloud, message_count, summary_time,
optsn, creator, create_time, update_time, delete_time, deleted
</sql>
<insert id="insertChatRoomSummary" parameterType="org.xyzh.api.workcase.dto.TbChatRoomSummaryDTO">
INSERT INTO workcase.tb_chat_room_summary (
optsn, summary_id, room_id, creator
<if test="question != null">, question</if>
<if test="needs != null">, needs</if>
<if test="answer != null">, answer</if>
<if test="workcloud != null">, workcloud</if>
<if test="messageCount != null">, message_count</if>
<if test="summaryTime != null">, summary_time</if>
) VALUES (
#{optsn}, #{summaryId}, #{roomId}, #{creator}
<if test="question != null">, #{question}</if>
<if test="needs != null">, #{needs, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler}</if>
<if test="answer != null">, #{answer}</if>
<if test="workcloud != null">, #{workcloud, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler}</if>
<if test="messageCount != null">, #{messageCount}</if>
<if test="summaryTime != null">, #{summaryTime}::timestamptz</if>
)
</insert>
<update id="updateChatRoomSummary" parameterType="org.xyzh.api.workcase.dto.TbChatRoomSummaryDTO">
UPDATE workcase.tb_chat_room_summary
<set>
<if test="question != null">question = #{question},</if>
<if test="needs != null">needs = #{needs, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler},</if>
<if test="answer != null">answer = #{answer},</if>
<if test="workcloud != null">workcloud = #{workcloud, jdbcType=ARRAY, typeHandler=org.xyzh.common.jdbc.handler.StringArrayTypeHandler},</if>
<if test="messageCount != null">message_count = #{messageCount},</if>
<if test="summaryTime != null">summary_time = #{summaryTime}::timestamptz,</if>
update_time = now()
</set>
WHERE summary_id = #{summaryId}
</update>
<select id="selectChatRoomSummaryById" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM workcase.tb_chat_room_summary
WHERE summary_id = #{summaryId} AND deleted = false
</select>
<select id="selectLatestSummaryByRoomId" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM workcase.tb_chat_room_summary
WHERE room_id = #{roomId} AND deleted = false
ORDER BY summary_time DESC
LIMIT 1
</select>
<select id="selectChatRoomSummaryList" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM workcase.tb_chat_room_summary
<where>
deleted = false
<if test="filter.summaryId != null and filter.summaryId != ''">
AND summary_id = #{filter.summaryId}
</if>
<if test="filter.roomId != null and filter.roomId != ''">
AND room_id = #{filter.roomId}
</if>
<if test="filter.question != null and filter.question != ''">
AND question LIKE CONCAT('%', #{filter.question}, '%')
</if>
</where>
ORDER BY summary_time DESC
</select>
<select id="selectChatRoomSummaryPage" resultMap="BaseResultMap">
SELECT <include refid="Base_Column_List"/>
FROM workcase.tb_chat_room_summary
<where>
deleted = false
<if test="filter.summaryId != null and filter.summaryId != ''">
AND summary_id = #{filter.summaryId}
</if>
<if test="filter.roomId != null and filter.roomId != ''">
AND room_id = #{filter.roomId}
</if>
<if test="filter.question != null and filter.question != ''">
AND question LIKE CONCAT('%', #{filter.question}, '%')
</if>
</where>
ORDER BY summary_time DESC
LIMIT #{pageParam.pageSize} OFFSET #{pageParam.offset}
</select>
<select id="countChatRoomSummaries" resultType="long">
SELECT COUNT(*)
FROM workcase.tb_chat_room_summary
<where>
deleted = false
<if test="filter.summaryId != null and filter.summaryId != ''">
AND summary_id = #{filter.summaryId}
</if>
<if test="filter.roomId != null and filter.roomId != ''">
AND room_id = #{filter.roomId}
</if>
<if test="filter.question != null and filter.question != ''">
AND question LIKE CONCAT('%', #{filter.question}, '%')
</if>
</where>
</select>
<update id="deleteChatRoomSummary">
UPDATE workcase.tb_chat_room_summary
SET deleted = true, delete_time = now()
WHERE summary_id = #{summaryId}
</update>
</mapper>

View File

@@ -83,6 +83,9 @@
</if> </if>
</where> </where>
ORDER BY frequency DESC, create_time DESC ORDER BY frequency DESC, create_time DESC
<if test="filter.limit != null and filter.limit > 0">
LIMIT #{filter.limit}
</if>
</select> </select>
<select id="selectWordCloudPage" resultMap="BaseResultMap"> <select id="selectWordCloudPage" resultMap="BaseResultMap">

View File

@@ -207,4 +207,13 @@
</where> </where>
</select> </select>
<select id="countWorkcasesByType" resultType="org.xyzh.api.workcase.dto.TbWorkcaseDTO">
SELECT type, COUNT(*) as count
FROM workcase.tb_workcase
WHERE create_time BETWEEN #{filter.startTime} AND #{filter.endTime}
AND deleted = false
GROUP BY type
ORDER BY count DESC
</select>
</mapper> </mapper>

View File

@@ -181,5 +181,25 @@ export const workcaseAPI = {
async getWorkcaseDevicePage(pageRequest: PageRequest<TbWorkcaseDeviceDTO>): Promise<ResultDomain<TbWorkcaseDeviceDTO>> { async getWorkcaseDevicePage(pageRequest: PageRequest<TbWorkcaseDeviceDTO>): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
const response = await api.post<TbWorkcaseDeviceDTO>(`${this.baseUrl}/device/page`, pageRequest) const response = await api.post<TbWorkcaseDeviceDTO>(`${this.baseUrl}/device/page`, pageRequest)
return response.data return response.data
},
// ========================= 工单统计 =========================
/**
* 查询工单问题分类统计
* @param filter 筛选条件startTime, endTime
*/
async countWorkcasesByType(filter: TbWorkcaseDTO): Promise<ResultDomain<TbWorkcaseDTO>> {
const response = await api.post<TbWorkcaseDTO>(`${this.baseUrl}/category/count`, filter)
return response.data
},
/**
* 统计工单数量
* @param filter 筛选条件status等
*/
async countWorkcases(filter: TbWorkcaseDTO): Promise<ResultDomain<number>> {
const response = await api.post<number>(`${this.baseUrl}/count`, filter)
return response.data
} }
} }

View File

@@ -11,7 +11,9 @@ import type {
ChatMemberVO, ChatMemberVO,
ChatRoomMessageVO, ChatRoomMessageVO,
CustomerServiceVO, CustomerServiceVO,
VideoMeetingVO VideoMeetingVO,
ChatRoomSummaryRequest,
ChatRoomSummaryResponse
} from '@/types/workcase' } from '@/types/workcase'
/** /**
@@ -286,5 +288,40 @@ export const workcaseChatAPI = {
params: { commentLevel } params: { commentLevel }
}) })
return response.data return response.data
},
// ====================== 聊天室总结管理 ======================
/**
* 获取聊天室最新总结
* @param roomId 聊天室ID
*/
async getLatestSummary(roomId: string): Promise<ResultDomain<ChatRoomSummaryResponse>> {
const response = await api.get<ChatRoomSummaryResponse>(`${this.baseUrl}/room/${roomId}/summary`)
return response.data
},
/**
* 生成聊天室对话总结
* @param request 总结请求参数
*/
async summaryChatRoom(request: ChatRoomSummaryRequest): Promise<ResultDomain<ChatRoomSummaryResponse>> {
const { roomId, ...body } = request
const response = await api.post<ChatRoomSummaryResponse>(
`${this.baseUrl}/room/${roomId}/summary`,
Object.keys(body).length > 0 ? body : {}
)
return response.data
},
// ====================== 统计接口 ======================
/**
* 统计聊天室数量
* @param filter 筛选条件startTime, endTime, status等
*/
async countChatRooms(filter: TbChatRoomDTO): Promise<ResultDomain<number>> {
const response = await api.post<number>(`${this.baseUrl}/room/count`, filter)
return response.data
} }
} }

View File

@@ -139,6 +139,7 @@ $brand-color-hover: #004488;
.product-cloud { .product-cloud {
display: flex; display: flex;
justify-content: center;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px 12px; gap: 8px 12px;
padding: 8px 0; padding: 8px 0;

View File

@@ -313,4 +313,26 @@ export interface CreateMeetingParam {
export interface MarkReadParam { export interface MarkReadParam {
roomId: string roomId: string
messageIds?: 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
} }

View File

@@ -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;
}
}

View File

@@ -8,11 +8,12 @@
<el-icon><ChatDotRound /></el-icon> <el-icon><ChatDotRound /></el-icon>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<div class="stat-value">1,258</div> <div class="stat-value">{{ dashboardData.consultCount }}</div>
<div class="stat-label">咨询次数</div> <div class="stat-label">咨询次数</div>
<div class="stat-trend up"> <div class="stat-trend" :class="dashboardData.consultTrend >= 0 ? 'up' : 'down'">
<el-icon><Top /></el-icon> <el-icon v-if="dashboardData.consultTrend >= 0"><Top /></el-icon>
<span>较昨日 +12.5%</span> <el-icon v-else><Top style="transform: rotate(180deg);" /></el-icon>
<span>较昨日 {{ dashboardData.consultTrend >= 0 ? '+' : '' }}{{ dashboardData.consultTrend }}%</span>
</div> </div>
</div> </div>
</div> </div>
@@ -60,15 +61,21 @@
</div> </div>
</template> </template>
<div class="question-stats"> <div class="question-stats">
<div v-for="item in questionCategories" :key="item.name" class="question-stat-item clickable"> <div v-if="questionCategories.length > 0">
<div class="stat-bar-header"> <div v-for="item in questionCategories" :key="item.name" class="question-stat-item clickable">
<span class="stat-name">{{ item.name }}</span> <div class="stat-bar-header">
<span class="stat-count">{{ item.count }} </span> <span class="stat-name">{{ item.name }}</span>
</div> <span class="stat-count">{{ item.count }} </span>
<div class="stat-bar"> </div>
<div class="stat-bar-fill" :style="{ width: item.percent + '%', background: item.color }"></div> <div class="stat-bar">
<div class="stat-bar-fill" :style="{ width: item.percent + '%', background: item.color }"></div>
</div>
</div> </div>
</div> </div>
<div v-else class="empty-state">
<el-icon :size="48" style="color: #dcdfe6;"><Document /></el-icon>
<p>暂无数据</p>
</div>
</div> </div>
</el-card> </el-card>
@@ -83,10 +90,16 @@
</div> </div>
</template> </template>
<div class="product-cloud"> <div class="product-cloud">
<span v-for="(product, index) in productCloudData" :key="index" class="cloud-tag" <div v-if="productCloudData.length > 0">
:style="{ fontSize: product.size + 'px', color: product.color, opacity: 0.7 + product.weight * 0.3 }"> <span v-for="(product, index) in productCloudData" :key="index" class="cloud-tag"
{{ product.name }} :style="{ fontSize: product.size + 'px', color: product.color, opacity: 0.7 + product.weight * 0.3 }">
</span> {{ product.name }}
</span>
</div>
<div v-else class="empty-state">
<el-icon :size="48" style="color: #dcdfe6;"><Document /></el-icon>
<p>暂无数据</p>
</div>
</div> </div>
</el-card> </el-card>
</div> </div>
@@ -120,49 +133,215 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted, watch, computed } from 'vue'
import { useRouter } from 'vue-router'
import AdminLayout from '@/views/admin/AdminLayout.vue' import AdminLayout from '@/views/admin/AdminLayout.vue'
import { MessageCircle as ChatDotRound, Clock, CheckSquare as Select, ArrowUp as Top, ArrowRight as Right, FileText as Document, Ticket as Tickets, Headphones as Service } from 'lucide-vue-next' import { MessageCircle as ChatDotRound, Clock, CheckSquare as Select, ArrowUp as Top, ArrowRight as Right, FileText as Document, Ticket as Tickets, Headphones as Service } from 'lucide-vue-next'
import { workcaseAPI } from '@/api/workcase/workcase'
import { workcaseChatAPI } from '@/api/workcase/workcaseChat'
import type { TbWorkcaseDTO, TbWordCloudDTO, TbChatRoomDTO } from '@/types/workcase'
const router = useRouter()
const questionStatPeriod = ref('today') const questionStatPeriod = ref('today')
// 核心数据统计
const dashboardData = ref({ const dashboardData = ref({
pendingTickets: 24, consultCount: 0, // 咨询次数(昨日聊天室数量)
completedTickets: 856 consultTrend: 0, // 较前日的增减百分比
pendingTickets: 0, // 待处理工单总量
completedTickets: 0 // 已处理工单总量
}) })
const questionCategories = ref([ // 问题分类统计数据
{ name: '设备故障', count: 156, percent: 85, color: '#409eff' }, const questionCategories = ref<Array<{ name: string; count: number; percent: number; color: string }>>([])
{ name: '使用咨询', count: 98, percent: 65, color: '#67c23a' },
{ name: '配件更换', count: 45, percent: 35, color: '#e6a23c' },
{ name: '安装调试', count: 32, percent: 25, color: '#f56c6c' },
{ name: '其他问题', count: 28, percent: 18, color: '#909399' }
])
const productCloudData = ref([ // 词云数据
{ name: 'TH-500GF', size: 24, weight: 0.9, color: '#409eff' }, const productCloudData = ref<Array<{ name: string; size: number; weight: number; color: string }>>([])
{ name: 'TH-300D', size: 20, weight: 0.7, color: '#67c23a' },
{ name: 'TH-800GF', size: 22, weight: 0.8, color: '#e6a23c' }, // 颜色配置
{ name: 'S-200X', size: 18, weight: 0.6, color: '#f56c6c' }, const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399']
{ name: 'S-150X', size: 16, weight: 0.5, color: '#909399' },
{ name: 'G-100S', size: 19, weight: 0.65, color: '#409eff' }, // 获取昨日和前日的时间范围
{ name: 'G-200S', size: 21, weight: 0.75, color: '#67c23a' } const getYesterdayRange = () => {
]) const today = new Date()
today.setHours(0, 0, 0, 0)
const yesterday = new Date(today)
yesterday.setDate(today.getDate() - 1)
const yesterdayEnd = new Date(today)
yesterdayEnd.setMilliseconds(-1)
const dayBeforeYesterday = new Date(yesterday)
dayBeforeYesterday.setDate(yesterday.getDate() - 1)
const dayBeforeYesterdayEnd = new Date(yesterday)
dayBeforeYesterdayEnd.setMilliseconds(-1)
return {
yesterday: { start: yesterday, end: yesterdayEnd },
dayBeforeYesterday: { start: dayBeforeYesterday, end: dayBeforeYesterdayEnd }
}
}
// 根据时间周期计算开始和结束时间
const getTimeRange = (period: string) => {
const now = new Date()
const endTime = now
const startTime = new Date()
if (period === 'today') {
startTime.setHours(0, 0, 0, 0)
} else if (period === 'week') {
startTime.setDate(now.getDate() - 7)
} else if (period === 'month') {
startTime.setMonth(now.getMonth() - 1)
}
return { startTime, endTime }
}
// 加载核心数据统计
const loadDashboardData = async () => {
try {
// 1. 统计昨日和前日的咨询次数使用count接口
const timeRanges = getYesterdayRange()
const yesterdayResult = await workcaseChatAPI.countChatRooms({
startTime: timeRanges.yesterday.start,
endTime: timeRanges.yesterday.end
} as TbChatRoomDTO)
const dayBeforeResult = await workcaseChatAPI.countChatRooms({
startTime: timeRanges.dayBeforeYesterday.start,
endTime: timeRanges.dayBeforeYesterday.end
} as TbChatRoomDTO)
if (yesterdayResult.success && dayBeforeResult.success) {
const yesterdayCount = Number(yesterdayResult.data || 0)
const dayBeforeCount = Number(dayBeforeResult.data || 0)
dashboardData.value.consultCount = yesterdayCount
// 计算增减百分比
if (dayBeforeCount > 0) {
const change = ((yesterdayCount - dayBeforeCount) / dayBeforeCount) * 100
dashboardData.value.consultTrend = Math.round(change * 10) / 10 // 保留1位小数
} else {
dashboardData.value.consultTrend = yesterdayCount > 0 ? 100 : 0
}
}
// 2. 统计待处理工单总量使用count接口
const pendingResult = await workcaseAPI.countWorkcases({
status: 'pending'
} as TbWorkcaseDTO)
if (pendingResult.success && pendingResult.data) {
dashboardData.value.pendingTickets = Number(pendingResult.data || 0)
}
// 3. 统计已处理工单总量使用count接口
const completedResult = await workcaseAPI.countWorkcases({
status: 'done'
} as TbWorkcaseDTO)
if (completedResult.success && completedResult.data) {
dashboardData.value.completedTickets = Number(completedResult.data || 0)
}
} catch (error) {
console.error('加载核心数据统计失败:', error)
}
}
// 加载问题分类统计
const loadQuestionStats = async () => {
try {
const { startTime, endTime } = getTimeRange(questionStatPeriod.value)
const result = await workcaseAPI.countWorkcasesByType({
startTime,
endTime
} as TbWorkcaseDTO)
if (result.success && result.data) {
const data = Array.isArray(result.data) ? result.data : [result.data]
// 计算总数
const total = data.reduce((sum, item) => sum + (item.count || 0), 0)
// 转换数据格式并计算百分比
questionCategories.value = data.map((item, index) => ({
name: item.type || '未分类',
count: Number(item.count || 0),
percent: total > 0 ? Math.round((Number(item.count || 0) / total) * 100) : 0,
color: colors[index % colors.length]
}))
}
} catch (error) {
console.error('加载问题分类统计失败:', error)
}
}
// 加载词云数据
const loadWordCloud = async () => {
try {
const result = await workcaseChatAPI.getWordCloudList({
limit: 10,
category: 'product'
} as TbWordCloudDTO)
if (result.success && result.data) {
const data = Array.isArray(result.data) ? result.data : [result.data]
// 找出最大词频用于归一化
const maxFreq = Math.max(...data.map(item => Number(item.frequency || 0)))
// 转换数据格式
productCloudData.value = data.map((item, index) => {
const freq = Number(item.frequency || 0)
const weight = maxFreq > 0 ? freq / maxFreq : 0
return {
name: item.word || '',
size: 16 + Math.round(weight * 10), // 16-26px
weight,
color: colors[index % colors.length]
}
})
}
} catch (error) {
console.error('加载词云数据失败:', error)
}
}
// 监听时间周期变化
watch(questionStatPeriod, () => {
loadQuestionStats()
})
// 页面加载时获取数据
onMounted(() => {
loadDashboardData()
loadQuestionStats()
loadWordCloud()
})
const goToChat = () => { const goToChat = () => {
console.log('跳转到对话数据') router.push('/admin/customerChat')
} }
const goToWorkcase = (status?: string) => { const goToWorkcase = (status?: string) => {
console.log('跳转到工单管理', status) router.push('/admin/workcase')
} }
const goToKnowledge = () => { const goToKnowledge = () => {
console.log('跳转到知识库管理') router.push('/admin/knowledge')
} }
const goToAgent = () => { const goToAgent = () => {
console.log('跳转到智能体管理') router.push('/admin/agent')
} }
</script> </script>

View File

@@ -174,26 +174,166 @@
background: #fff; background: #fff;
border: 1px solid #e5e7eb; border: 1px solid #e5e7eb;
border-radius: 12px; 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 { .summary-content {
display: flex; display: flex;
flex-direction: column; 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-section {
.summary-title { .summary-title {
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 600;
color: #111827; color: #374151;
margin-bottom: 8px; margin-bottom: 12px;
padding-left: 12px;
border-left: 3px solid #4b87ff;
} }
.summary-text { .summary-text {
font-size: 14px; font-size: 14px;
color: #374151; color: #1f2937;
line-height: 1.6; 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);
}
} }
} }

View File

@@ -81,19 +81,55 @@
<!-- 对话纪要 --> <!-- 对话纪要 -->
<div v-else class="summary-container"> <div v-else class="summary-container">
<div class="summary-content"> <!-- 加载状态 -->
<div class="summary-section"> <div v-if="loadingSummary" class="summary-loading">
<div class="summary-title">问题概述</div> <div class="loading-spinner"></div>
<span>正在生成对话总结...</span>
</div>
<!-- 总结内容 -->
<div v-else-if="summaryData" class="summary-content">
<div class="summary-header">
<h3>对话总结</h3>
<button class="regenerate-btn" @click="generateSummary">重新生成</button>
</div>
<div v-if="summaryData.question" class="summary-section">
<div class="summary-title">核心问题</div>
<div class="summary-text"> <div class="summary-text">
{{ summary.overview || '暂无概述' }} {{ summaryData.question }}
</div> </div>
</div> </div>
<div class="summary-section">
<div class="summary-title">客户诉求</div> <div v-if="summaryData.needs && summaryData.needs.length > 0" class="summary-section">
<div class="summary-title">核心诉求</div>
<ul class="summary-list">
<li v-for="(need, index) in summaryData.needs" :key="index">{{ need }}</li>
</ul>
</div>
<div v-if="summaryData.answer" class="summary-section">
<div class="summary-title">解决方案</div>
<div class="summary-text"> <div class="summary-text">
{{ summary.demand || '暂无诉求' }} {{ summaryData.answer }}
</div> </div>
</div> </div>
<div class="summary-meta">
<span v-if="summaryData.messageCount">
基于 {{ summaryData.messageCount }} 条消息生成
</span>
<span v-if="summaryData.summaryTime">
生成时间{{ summaryData.summaryTime }}
</span>
</div>
</div>
<!-- 空状态 -->
<div v-else class="summary-empty">
<div class="empty-icon">📝</div>
<div class="empty-text">暂无对话总结</div>
<button class="generate-btn" @click="generateSummary">生成总结</button>
</div> </div>
</div> </div>
</main> </main>
@@ -101,13 +137,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch, computed, nextTick } from 'vue' import { ref, onMounted, watch, computed, nextTick } from 'vue'
import type { ChatRoomVO, ChatRoomMessageVO } from '@/types/workcase/chatRoom' import type { ChatRoomVO, ChatRoomMessageVO, ChatRoomSummaryResponse } from '@/types/workcase/chatRoom'
import { workcaseChatAPI } from '@/api/workcase/workcaseChat' import { workcaseChatAPI } from '@/api/workcase/workcaseChat'
import { ElMessage } from 'element-plus'
interface Summary {
overview?: string
demand?: string
}
interface Props { interface Props {
chatRoom?: ChatRoomVO chatRoom?: ChatRoomVO
@@ -123,10 +155,8 @@ const activeTab = ref<'record' | 'summary'>('record')
const messages = ref<ChatRoomMessageVO[]>([]) const messages = ref<ChatRoomMessageVO[]>([])
const loading = ref(false) const loading = ref(false)
const loadingMore = ref(false) const loadingMore = ref(false)
const summary = ref<Summary>({ const summaryData = ref<ChatRoomSummaryResponse | null>(null)
overview: '', const loadingSummary = ref(false)
demand: ''
})
// 分页相关 // 分页相关
const currentPage = ref(1) const currentPage = ref(1)
@@ -241,34 +271,82 @@ const handleScroll = () => {
} }
} }
// 生成对话纪要可以后续接入AI生成 // 查询最新的对话总结
const generateSummary = () => { const loadLatestSummary = async () => {
if (messages.value.length === 0) return if (!targetRoomId.value) {
return
// 简单提取第一条和最后一条消息作为概述 }
const firstMsg = messages.value[0]
const lastMsg = messages.value[messages.value.length - 1] loadingSummary.value = true
summaryData.value = null
summary.value = {
overview: `客户反馈:${firstMsg.content?.substring(0, 50) || ''}`, try {
demand: lastMsg.content?.substring(0, 100) || '' const res = await workcaseChatAPI.getLatestSummary(targetRoomId.value)
if (res.success && res.data) {
summaryData.value = res.data
} else {
// 查询失败,表示还没有总结
summaryData.value = null
}
} catch (error) {
console.error('查询总结失败:', error)
summaryData.value = null
} finally {
loadingSummary.value = false
}
}
// 生成对话纪要调用后端AI接口
const generateSummary = async () => {
if (!targetRoomId.value) {
ElMessage.warning('无效的聊天室ID')
return
}
loadingSummary.value = true
summaryData.value = null
try {
const res = await workcaseChatAPI.summaryChatRoom({
roomId: targetRoomId.value,
includeSystemMessages: false,
includeMeetingMessages: false
})
if (res.success && res.data) {
summaryData.value = res.data
ElMessage.success('总结生成成功')
} else {
ElMessage.error(res.message || '生成总结失败')
}
} catch (error) {
console.error('生成总结失败:', error)
ElMessage.error('生成总结失败')
} finally {
loadingSummary.value = false
} }
} }
onMounted(async () => { onMounted(async () => {
await loadMessages() await loadMessages()
generateSummary()
}) })
// 监听 roomId 变化,重新加载数据 // 监听 roomId 变化,重新加载数据
watch(targetRoomId, async (newVal) => { watch(targetRoomId, async (newVal) => {
if (newVal) { if (newVal) {
messages.value = [] messages.value = []
summary.value = { overview: '', demand: '' } summaryData.value = null
currentPage.value = 1 currentPage.value = 1
total.value = 0 total.value = 0
await loadMessages() await loadMessages()
generateSummary() }
})
// 监听切换到对话纪要标签,自动查询总结
watch(activeTab, async (newVal) => {
if (newVal === 'summary' && !summaryData.value && !loadingSummary.value) {
await loadLatestSummary()
} }
}) })
</script> </script>