案例助手:代码整理和部分问题修复

This commit is contained in:
liu.zixi
2025-09-30 15:46:46 +08:00
parent 5f32b8a6fc
commit 66f437427c
4 changed files with 382 additions and 50 deletions

View File

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

View File

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

View File

@@ -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<CaseReferVo> caseRefers = new ArrayList<>();
/**
* 建议列表
*/
private List<String> 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<CaseReferVo> caseRefers, List<String> 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);
}
}
}

View File

@@ -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<CaseReferVo> 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<CaseReferVo> caseRefers = new ArrayList<>();
public List<String> suggestions = new ArrayList<>();
public String conversationId;
public String userId;
}
// ConversationData 已移动到独立的Entity类AiChatConversationData
}