diff --git a/servers/boe-server-all/src/main/java/com/xboe/config/ElasticSearchIndexInitializer.java b/servers/boe-server-all/src/main/java/com/xboe/config/ElasticSearchIndexInitializer.java new file mode 100644 index 00000000..3c3628c7 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/config/ElasticSearchIndexInitializer.java @@ -0,0 +1,193 @@ +package com.xboe.config; + +import lombok.extern.slf4j.Slf4j; +import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; +import org.elasticsearch.action.support.master.AcknowledgedResponse; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.indices.CreateIndexRequest; +import org.elasticsearch.client.indices.CreateIndexResponse; +import org.elasticsearch.client.indices.GetIndexRequest; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +/** + * ElasticSearch索引初始化器 + * 在Spring Boot启动完成并监听到配置文件加载完毕后,检查并创建所需的ES索引 + * + * @author AI Assistant + */ +@Slf4j +@Component +public class ElasticSearchIndexInitializer { + + @Autowired(required = false) + private RestHighLevelClient elasticsearchClient; + + /** + * 监听Spring Boot应用启动完成事件 + * ApplicationReadyEvent在应用启动完成、所有配置加载完毕后触发 + */ + @EventListener(ApplicationReadyEvent.class) + public void initializeElasticSearchIndices() { + if (elasticsearchClient == null) { + log.warn("ElasticSearch客户端未配置,跳过索引初始化"); + return; + } + + log.info("开始检查和初始化ElasticSearch索引..."); + + try { + // 检查并创建ai_chat_messages索引 + checkAndCreateIndex("ai_chat_messages"); + + log.info("ElasticSearch索引初始化完成"); + } catch (Exception e) { + log.error("ElasticSearch索引初始化失败", e); + } + } + + /** + * 检查索引是否存在,如果不存在则创建 + * + * @param indexName 索引名称 + * @throws IOException IO异常 + */ + private void checkAndCreateIndex(String indexName) throws IOException { + // 检查索引是否存在 + GetIndexRequest getIndexRequest = new GetIndexRequest(indexName); + boolean exists = elasticsearchClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT); + + if (exists) { + log.info("ElasticSearch索引 [{}] 已存在", indexName); + return; + } + + log.info("ElasticSearch索引 [{}] 不存在,开始创建...", indexName); + + // 创建索引 + CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName); + + // 设置索引配置 + createIndexRequest.settings(Settings.builder() + .put("index.number_of_shards", 1) + .put("index.number_of_replicas", 0) + .put("index.analysis.analyzer.ik_max_word.tokenizer", "ik_max_word") + .put("index.analysis.analyzer.ik_smart.tokenizer", "ik_smart") + ); + + // 设置字段映射 + String mapping = getAiChatMessagesMapping(); + createIndexRequest.mapping(mapping, XContentType.JSON); + + // 执行创建索引请求 + CreateIndexResponse createIndexResponse = elasticsearchClient.indices() + .create(createIndexRequest, RequestOptions.DEFAULT); + + if (createIndexResponse.isAcknowledged()) { + log.info("ElasticSearch索引 [{}] 创建成功", indexName); + } else { + log.warn("ElasticSearch索引 [{}] 创建可能失败,响应未确认", indexName); + } + } + + /** + * 获取ai_chat_messages索引的字段映射配置 + * 根据项目中的会话消息数据结构规范定义映射 + * + * @return JSON格式的映射配置 + */ + private String getAiChatMessagesMapping() { + return "{\n" + + " \"properties\": {\n" + + " \"conversationId\": {\n" + + " \"type\": \"keyword\",\n" + + " \"index\": true\n" + + " },\n" + + " \"messageId\": {\n" + + " \"type\": \"keyword\",\n" + + " \"index\": true\n" + + " },\n" + + " \"messageType\": {\n" + + " \"type\": \"keyword\",\n" + + " \"index\": true\n" + + " },\n" + + " \"query\": {\n" + + " \"type\": \"text\",\n" + + " \"analyzer\": \"ik_max_word\",\n" + + " \"search_analyzer\": \"ik_smart\",\n" + + " \"fields\": {\n" + + " \"keyword\": {\n" + + " \"type\": \"keyword\",\n" + + " \"ignore_above\": 256\n" + + " }\n" + + " }\n" + + " },\n" + + " \"answer\": {\n" + + " \"type\": \"text\",\n" + + " \"analyzer\": \"ik_max_word\",\n" + + " \"search_analyzer\": \"ik_smart\"\n" + + " },\n" + + " \"caseRefer\": {\n" + + " \"type\": \"nested\",\n" + + " \"properties\": {\n" + + " \"caseId\": {\n" + + " \"type\": \"keyword\",\n" + + " \"index\": true\n" + + " },\n" + + " \"title\": {\n" + + " \"type\": \"text\",\n" + + " \"analyzer\": \"ik_max_word\",\n" + + " \"search_analyzer\": \"ik_smart\"\n" + + " },\n" + + " \"authorName\": {\n" + + " \"type\": \"keyword\",\n" + + " \"index\": true\n" + + " },\n" + + " \"keywords\": {\n" + + " \"type\": \"text\",\n" + + " \"analyzer\": \"ik_max_word\",\n" + + " \"search_analyzer\": \"ik_smart\"\n" + + " },\n" + + " \"content\": {\n" + + " \"type\": \"text\",\n" + + " \"analyzer\": \"ik_max_word\",\n" + + " \"search_analyzer\": \"ik_smart\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"suggestions\": {\n" + + " \"type\": \"text\",\n" + + " \"analyzer\": \"ik_max_word\",\n" + + " \"search_analyzer\": \"ik_smart\"\n" + + " },\n" + + " \"userId\": {\n" + + " \"type\": \"keyword\",\n" + + " \"index\": true\n" + + " },\n" + + " \"userName\": {\n" + + " \"type\": \"keyword\",\n" + + " \"index\": true\n" + + " },\n" + + " \"timestamp\": {\n" + + " \"type\": \"date\",\n" + + " \"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSS'Z'||epoch_millis\"\n" + + " },\n" + + " \"createTime\": {\n" + + " \"type\": \"date\",\n" + + " \"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSS'Z'||epoch_millis\"\n" + + " },\n" + + " \"updateTime\": {\n" + + " \"type\": \"date\",\n" + + " \"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSS'Z'||epoch_millis\"\n" + + " }\n" + + " }\n" + + "}"; + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/enums/CaseAiChatStatusEnum.java b/servers/boe-server-all/src/main/java/com/xboe/enums/CaseAiChatStatusEnum.java new file mode 100644 index 00000000..cfb0466b --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/enums/CaseAiChatStatusEnum.java @@ -0,0 +1,41 @@ +package com.xboe.enums; + +public enum CaseAiChatStatusEnum { + + REFERS(0, "引用文档"), + + CHAT(1, "聊天中"), + + CHAT_COMPLETED(2, "聊天完成"), + + SUGGESTIONS(3, "建议"), + + API_COMPLETED(4, "接口完成"), + ; + + private final int code; + + private final String label; + + CaseAiChatStatusEnum(int code, String label) { + this.code = code; + this.label = label; + } + + public static CaseAiChatStatusEnum getByCode(int code) { + for (CaseAiChatStatusEnum value : values()) { + if (value.code == code) { + return value; + } + } + return API_COMPLETED; + } + + public int getCode() { + return code; + } + + public String getLabel() { + return label; + } +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/AiChatConversationData.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/AiChatConversationData.java new file mode 100644 index 00000000..61d0a97c --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/AiChatConversationData.java @@ -0,0 +1,116 @@ +package com.xboe.module.boecase.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.xboe.module.boecase.vo.CaseReferVo; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * AI聊天会话数据实体 + * 纯数据容器,不与数据库交互 + * + * @author AI Assistant + */ +@Data +@Slf4j +public class AiChatConversationData { + + /** + * 会话ID + */ + private String conversationId; + + /** + * 用户提问内容 + */ + private String query; + + /** + * AI回答内容(使用StringBuilder收集流式数据) + */ + private StringBuilder answer = new StringBuilder(); + + /** + * 案例引用列表 + */ + private List caseRefers = new ArrayList<>(); + + /** + * 建议列表 + */ + private List suggestions = new ArrayList<>(); + + /** + * 用户ID + */ + private String userId; + + /** + * 用户名称 + */ + private String userName; + + /** + * 消息时间戳 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime timestamp; + + // ================== 构造函数 ================== + + public AiChatConversationData() { + this.timestamp = LocalDateTime.now(); + } + + public AiChatConversationData(String conversationId, String query, String answer, + List caseRefers, List suggestions, + String userId) { + this.conversationId = conversationId; + this.query = query; + this.answer = new StringBuilder(answer != null ? answer : ""); + this.caseRefers = caseRefers != null ? caseRefers : new ArrayList<>(); + this.suggestions = suggestions != null ? suggestions : new ArrayList<>(); + this.userId = userId; + this.timestamp = LocalDateTime.now(); + } + + // ================== 便捷方法 ================== + + /** + * 获取回答内容的字符串形式 + */ + public String getAnswerAsString() { + return this.answer.toString(); + } + + /** + * 追加回答内容 + */ + public void appendAnswer(String content) { + if (content != null) { + this.answer.append(content); + } + } + + /** + * 添加案例引用 + */ + public void addCaseRefer(CaseReferVo caseRefer) { + if (caseRefer != null) { + this.caseRefers.add(caseRefer); + } + } + + /** + * 添加建议 + */ + public void addSuggestion(String suggestion) { + if (suggestion != null && !suggestion.trim().isEmpty()) { + this.suggestions.add(suggestion); + } + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseAiChatServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseAiChatServiceImpl.java index acf0574a..d3ea885f 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseAiChatServiceImpl.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseAiChatServiceImpl.java @@ -3,6 +3,7 @@ package com.xboe.module.boecase.service.impl; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.xboe.core.CurrentUser; +import com.xboe.enums.CaseAiChatStatusEnum; import com.xboe.module.boecase.dao.CaseAiConversationsDao; import com.xboe.module.boecase.dao.CaseDocumentLogDao; import com.xboe.module.boecase.dao.CasesDao; @@ -15,6 +16,7 @@ import com.xboe.module.boecase.service.IAiAccessTokenService; import com.xboe.module.boecase.service.ICaseAiChatService; import com.xboe.module.boecase.vo.CaseAiMessageVo; import com.xboe.module.boecase.vo.CaseReferVo; +import com.xboe.module.boecase.entity.AiChatConversationData; import com.xboe.system.organization.vo.OrgSimpleVo; import com.xboe.system.user.service.IUserService; import lombok.extern.slf4j.Slf4j; @@ -89,19 +91,6 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { // 1. 获取conversationId String conversationId = getOrCreateConversationId(caseAiChatDto, currentUser); - // 2. 检查是否为新会话,如果是则保存会话记录 - boolean isNewConversation = StringUtils.isEmpty(caseAiChatDto.getConversationId()); - CaseAiConversations conversation = null; - if (isNewConversation) { - // 新会话,需要保存到数据库 - conversation = new CaseAiConversations(); - conversation.setAiConversationId(conversationId); - conversation.setConversationName("AI案例咨询-" + LocalDateTime.now()); - conversation.setConversationUser(currentUser.getAccountId()); - // 保存会话到数据库 - caseAiConversationsDao.save(conversation); - } - // 3. 构建请求参数 String userId = currentUser.getAccountId(); String kId = caseAiProperties.getCaseKnowledgeId(); @@ -128,10 +117,10 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { SseEmitter sseEmitter = new SseEmitter(); // 6. 用于收集对话数据的容器 - ConversationData conversationData = new ConversationData(); - conversationData.query = caseAiChatDto.getQuery(); - conversationData.conversationId = conversationId; - conversationData.userId = userId; + AiChatConversationData conversationData = new AiChatConversationData(); + conversationData.setQuery(caseAiChatDto.getQuery()); + conversationData.setConversationId(conversationId); + conversationData.setUserId(userId); // 7. 创建事件监听器 EventSourceListener listener = new EventSourceListener() { @@ -150,7 +139,7 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { @Override public void onEvent(@NotNull EventSource eventSource, @Nullable String id, @Nullable String type, @NotNull String data) { - log.info("调用接口 [{}] 监听数据 id: [{}] type: [{}] data: [{}]", request.url(), id, type, data); + log.debug("调用接口 [{}] 监听数据 id: [{}] type: [{}] data: [{}]", request.url(), id, type, data); try { // 解析返回的数据 @@ -160,8 +149,9 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { Integer status = responseData.getInteger("status"); if (status != null) { - switch (status) { - case 0: // 返回引用文件 + CaseAiChatStatusEnum statusEnum = CaseAiChatStatusEnum.getByCode(status); + switch (statusEnum) { + case REFERS: // 返回引用文件 // 处理文件引用并构建返给前端的数据 JSONObject modifiedData = handleFileReferAndBuildResponse(responseData, conversationData); if (modifiedData != null) { @@ -170,19 +160,18 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { return; // 早期返回,不发送原始数据 } break; - case 1: // 流式对话中 + case CHAT: // 流式对话中 String content = responseData.getString("content"); if (content != null) { - conversationData.answer.append(content); + conversationData.appendAnswer(content); } break; - case 2: // 回答完成 - // 不做特殊处理 - break; - case 3: // 返回建议 + case SUGGESTIONS: // 返回建议 handleSuggestions(responseData, conversationData); break; - case 4: // 接口交互完成 + case CHAT_COMPLETED: + case API_COMPLETED: // 接口交互完成 + default: // 不做特殊处理 break; } @@ -402,7 +391,7 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { /** * 处理文件引用并构建返给前端的响应数据 */ - private JSONObject handleFileReferAndBuildResponse(JSONObject responseData, ConversationData conversationData) { + private JSONObject handleFileReferAndBuildResponse(JSONObject responseData, AiChatConversationData conversationData) { try { // 先处理文件引用,收集CaseReferVo数据 List currentCaseRefers = new ArrayList<>(); @@ -418,7 +407,7 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { CaseReferVo caseRefer = getCaseReferByDocId(docId); if (caseRefer != null) { currentCaseRefers.add(caseRefer); - conversationData.caseRefers.add(caseRefer); // 也添加到总的收集器中 + conversationData.addCaseRefer(caseRefer); // 也添加到总的收集器中 } } } @@ -427,7 +416,7 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { // 构建返给前端的数据结构 JSONObject data = new JSONObject(); data.put("status", 0); - data.put("conversationId", conversationData.conversationId); + data.put("conversationId", conversationData.getConversationId()); data.put("content", responseData.getString("content")); // 添加处理后的案例引用数据 @@ -471,7 +460,7 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { /** * 处理文件引用(原方法,保留用于数据收集) */ - private void handleFileRefer(JSONObject responseData, ConversationData conversationData) { + private void handleFileRefer(JSONObject responseData, AiChatConversationData conversationData) { try { JSONObject fileRefer = responseData.getJSONObject("fileRefer"); if (fileRefer != null && fileRefer.containsKey("files")) { @@ -483,7 +472,7 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { // 根据docId从 case_document_log 表查询案例数据 CaseReferVo caseRefer = getCaseReferByDocId(docId); if (caseRefer != null) { - conversationData.caseRefers.add(caseRefer); + conversationData.addCaseRefer(caseRefer); } } } @@ -496,14 +485,14 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { /** * 处理建议 */ - private void handleSuggestions(JSONObject responseData, ConversationData conversationData) { + private void handleSuggestions(JSONObject responseData, AiChatConversationData conversationData) { try { JSONArray suggestions = responseData.getJSONArray("suggestions"); if (suggestions != null) { for (int i = 0; i < suggestions.size(); i++) { String suggestion = suggestions.getString(i); if (suggestion != null) { - conversationData.suggestions.add(suggestion); + conversationData.addSuggestion(suggestion); } } } @@ -559,7 +548,7 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { /** * 保存对话记录到ES */ - private void saveConversationToES(ConversationData conversationData) { + private void saveConversationToES(AiChatConversationData conversationData) { if (elasticsearchClient == null) { log.warn("未配置Elasticsearch客户端,无法保存对话记录"); return; @@ -568,15 +557,15 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { try { // 构建要保存的数据 JSONObject esData = new JSONObject(); - esData.put("query", conversationData.query); - esData.put("answer", conversationData.answer.toString()); - esData.put("conversationId", conversationData.conversationId); - esData.put("userId", conversationData.userId); + esData.put("query", conversationData.getQuery()); + esData.put("answer", conversationData.getAnswerAsString()); + esData.put("conversationId", conversationData.getConversationId()); + esData.put("userId", conversationData.getUserId()); esData.put("timestamp", LocalDateTime.now().toString()); // 构建 caseRefer 数据 JSONArray caseReferArray = new JSONArray(); - for (CaseReferVo caseRefer : conversationData.caseRefers) { + for (CaseReferVo caseRefer : conversationData.getCaseRefers()) { JSONObject caseReferObj = new JSONObject(); caseReferObj.put("caseId", caseRefer.getCaseId()); caseReferObj.put("title", caseRefer.getTitle()); @@ -588,7 +577,7 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { esData.put("caseRefer", caseReferArray); // 添加建议 - esData.put("suggestions", conversationData.suggestions); + esData.put("suggestions", conversationData.getSuggestions()); // 保存到ES IndexRequest indexRequest = new IndexRequest("ai_chat_messages"); @@ -605,7 +594,7 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { /** * 当 SSE 失败时,作为普通 HTTP 请求处理 */ - private void handleAsRegularHttpRequest(Request request, SseEmitter sseEmitter, ConversationData conversationData) { + private void handleAsRegularHttpRequest(Request request, SseEmitter sseEmitter, AiChatConversationData conversationData) { try { OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(60, TimeUnit.SECONDS) @@ -634,12 +623,5 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { /** * 对话数据容器 */ - private static class ConversationData { - public String query; - public StringBuilder answer = new StringBuilder(); - public List caseRefers = new ArrayList<>(); - public List suggestions = new ArrayList<>(); - public String conversationId; - public String userId; - } + // ConversationData 已移动到独立的Entity类:AiChatConversationData }