feat: 新增功能:导出消息时记录错误提示

This commit is contained in:
liu.zixi
2025-12-04 18:40:45 +08:00
committed by joshen
parent f7adb42c8b
commit ee2770585c
9 changed files with 302 additions and 67 deletions

View File

@@ -0,0 +1,33 @@
package com.xboe.enums;
import lombok.Getter;
import java.util.Arrays;
/**
* 错误码枚举
*/
@Getter
public enum CaseAiChatErrCodeEnum {
SUCCESS(0, "成功"),
INTERNAL_ERROR(1, "内部错误"),
AIOT_ERROR(2, "AIoT平台错误"),
;
private final int code;
private final String label;
CaseAiChatErrCodeEnum(int code, String label) {
this.code = code;
this.label = label;
}
public static CaseAiChatErrCodeEnum getByCode(int code) {
return Arrays.stream(values()).filter(e -> e.code == code)
.findFirst().orElse(SUCCESS);
}
}

View File

@@ -4,6 +4,7 @@ import com.xboe.core.api.ApiBaseController;
import com.xboe.core.JsonResponse; import com.xboe.core.JsonResponse;
import com.xboe.module.boecase.dto.CaseAiChatDto; import com.xboe.module.boecase.dto.CaseAiChatDto;
import com.xboe.module.boecase.dto.CaseAiMsgLikeDto; import com.xboe.module.boecase.dto.CaseAiMsgLikeDto;
import com.xboe.module.boecase.dto.EsFieldDTO;
import com.xboe.module.boecase.dto.GetCaseAiMsgDto; import com.xboe.module.boecase.dto.GetCaseAiMsgDto;
import com.xboe.module.boecase.entity.AiChatConversationData; import com.xboe.module.boecase.entity.AiChatConversationData;
import com.xboe.module.boecase.service.ICaseAiChatService; import com.xboe.module.boecase.service.ICaseAiChatService;
@@ -207,6 +208,17 @@ public class CaseAiChatApi extends ApiBaseController {
return error("刷新失败"); return error("刷新失败");
} }
/**
* 添加索引字段
* @param esFieldDTO
* @return
*/
@PostMapping("/index/add_field")
public JsonResponse<String> addField(@RequestBody EsFieldDTO esFieldDTO) {
boolean result = elasticSearchIndexService.updateIndex(esFieldDTO.getFieldName(), esFieldDTO.getIndexProperties());
return result ? success("添加成功") : error("添加失败");
}
@PostMapping("/es/create") @PostMapping("/es/create")
public JsonResponse<String> createNewConversation(@RequestBody CaseAiMessageVo caseAiMessageVo, public JsonResponse<String> createNewConversation(@RequestBody CaseAiMessageVo caseAiMessageVo,
@RequestParam String conversationId, @RequestParam String conversationId,

View File

@@ -0,0 +1,49 @@
package com.xboe.module.boecase.dto;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.Map;
@Data
public class EsFieldDTO {
/**
* 字段名称
*/
private String fieldName;
/**
* 字段属性
*/
private Properties properties;
public Map<String, Object> getIndexProperties() {
Map<String, Object> indexProperties = new HashMap<>();
if (properties != null) {
indexProperties.put("type", properties.type);
if (properties.index != null) {
indexProperties.put("index", properties.index);
}
if (StringUtils.isNotBlank(properties.analyzer)) {
indexProperties.put("analyzer", properties.analyzer);
}
if (StringUtils.isNotBlank(properties.searchAnalyzer)) {
indexProperties.put("search_analyzer", properties.searchAnalyzer);
}
}
return indexProperties;
}
@Data
public static class Properties {
private String type;
private Boolean index;
private String analyzer;
private String searchAnalyzer;
}
}

View File

@@ -34,6 +34,19 @@ public class AiChatConversationData {
*/ */
private StringBuilder answer = new StringBuilder(); private StringBuilder answer = new StringBuilder();
/**
* 状态
* 0-正常
* 1-系统错误
* 2-AIoT平台错误
*/
private Integer status;
/**
* 错误信息
*/
private String errorMsg;
/** /**
* 案例引用列表 * 案例引用列表
*/ */

View File

@@ -4,6 +4,7 @@ import com.xboe.module.boecase.entity.AiChatConversationData;
import com.xboe.module.boecase.vo.CaseAiMessageVo; import com.xboe.module.boecase.vo.CaseAiMessageVo;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* es索引 * es索引
@@ -27,6 +28,14 @@ public interface IElasticSearchIndexService {
*/ */
boolean deleteIndex(); boolean deleteIndex();
/**
* 更新索引:添加索引字段
* @param fieldName
* @param fieldProperties
* @return
*/
boolean updateIndex(String fieldName, Map<String, Object> fieldProperties);
/** /**
* 新增数据 * 新增数据
* @param data * @param data

View File

@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONObject;
import com.xboe.constants.CaseAiConstants; import com.xboe.constants.CaseAiConstants;
import com.xboe.core.CurrentUser; import com.xboe.core.CurrentUser;
import com.xboe.core.orm.FieldFilters; import com.xboe.core.orm.FieldFilters;
import com.xboe.enums.CaseAiChatErrCodeEnum;
import com.xboe.enums.CaseAiChatStatusEnum; import com.xboe.enums.CaseAiChatStatusEnum;
import com.xboe.module.boecase.dao.CaseAiConversationsDao; import com.xboe.module.boecase.dao.CaseAiConversationsDao;
import com.xboe.module.boecase.dao.CaseDocumentLogDao; import com.xboe.module.boecase.dao.CaseDocumentLogDao;
@@ -138,6 +139,7 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService {
conversationData.setQuery(caseAiChatDto.getQuery()); conversationData.setQuery(caseAiChatDto.getQuery());
conversationData.setConversationId(conversationId); conversationData.setConversationId(conversationId);
conversationData.setUserId(userId); conversationData.setUserId(userId);
conversationData.setStatus(CaseAiChatErrCodeEnum.SUCCESS.getCode());
String kId = caseAiProperties.getCaseKnowledgeId(); String kId = caseAiProperties.getCaseKnowledgeId();
JSONObject chatParam = new JSONObject(); JSONObject chatParam = new JSONObject();
@@ -173,6 +175,8 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService {
errMessage(sseEmitter, conversationId, CaseAiConstants.CHAT_SYS_ERR_MSG); errMessage(sseEmitter, conversationId, CaseAiConstants.CHAT_SYS_ERR_MSG);
// 先响应给前端 // 先响应给前端
sseEmitter.complete(); sseEmitter.complete();
conversationData.setStatus(CaseAiChatErrCodeEnum.AIOT_ERROR.getCode());
conversationData.setErrorMsg("获取AccessToken时发生异常");
conversationData.appendAnswer(CaseAiConstants.CHAT_SYS_ERR_MSG); conversationData.appendAnswer(CaseAiConstants.CHAT_SYS_ERR_MSG);
saveConversationData(conversationData); saveConversationData(conversationData);
return sseEmitter; return sseEmitter;
@@ -182,6 +186,8 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService {
errMessage(sseEmitter, conversationId, CaseAiConstants.CHAT_SYS_ERR_MSG); errMessage(sseEmitter, conversationId, CaseAiConstants.CHAT_SYS_ERR_MSG);
// 先响应给前端 // 先响应给前端
sseEmitter.complete(); sseEmitter.complete();
conversationData.setStatus(CaseAiChatErrCodeEnum.AIOT_ERROR.getCode());
conversationData.setErrorMsg("获取AccessToken时发生异常" + e.getMessage());
conversationData.appendAnswer(CaseAiConstants.CHAT_SYS_ERR_MSG); conversationData.appendAnswer(CaseAiConstants.CHAT_SYS_ERR_MSG);
saveConversationData(conversationData); saveConversationData(conversationData);
return sseEmitter; return sseEmitter;
@@ -232,6 +238,8 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService {
errMessage(sseEmitter, conversationId, sseContent); errMessage(sseEmitter, conversationId, sseContent);
sseEmitter.complete(); sseEmitter.complete();
conversationData.setStatus(CaseAiChatErrCodeEnum.AIOT_ERROR.getCode());
conversationData.setErrorMsg(sseContent);
conversationData.appendAnswer(sseContent); conversationData.appendAnswer(sseContent);
saveConversationData(conversationData); saveConversationData(conversationData);
// 关闭eventSource // 关闭eventSource
@@ -287,6 +295,8 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService {
// 异常问题取message内容 // 异常问题取message内容
String message = jsonData.getString("message"); String message = jsonData.getString("message");
errMessage(sseEmitter, conversationId, message); errMessage(sseEmitter, conversationId, message);
conversationData.setStatus(CaseAiChatErrCodeEnum.AIOT_ERROR.getCode());
conversationData.setErrorMsg(jsonData.toJSONString());
return; return;
} }
} }
@@ -345,6 +355,8 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService {
// 从Map中移除失败的会话 // 从Map中移除失败的会话
conversationEventSourceMap.remove(conversationId); conversationEventSourceMap.remove(conversationId);
// 即使失败也要将已有的对话数据保存到ES // 即使失败也要将已有的对话数据保存到ES
conversationData.setStatus(CaseAiChatErrCodeEnum.AIOT_ERROR.getCode());
conversationData.setErrorMsg(errorMessage);
conversationData.appendAnswer(errorMessage); conversationData.appendAnswer(errorMessage);
saveConversationData(conversationData); saveConversationData(conversationData);
} }
@@ -928,6 +940,8 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService {
headerRow.createCell(4).setCellValue("回答"); headerRow.createCell(4).setCellValue("回答");
headerRow.createCell(5).setCellValue("开始时间"); headerRow.createCell(5).setCellValue("开始时间");
headerRow.createCell(6).setCellValue("问答时长(秒)"); headerRow.createCell(6).setCellValue("问答时长(秒)");
headerRow.createCell(7).setCellValue("消息状态");
headerRow.createCell(8).setCellValue("错误信息");
// 内容行 // 内容行
if (!excelDataList.isEmpty()) { if (!excelDataList.isEmpty()) {
@@ -948,8 +962,22 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService {
row.createCell(2).setCellValue(excelData.getUser()); row.createCell(2).setCellValue(excelData.getUser());
row.createCell(3).setCellValue(message.getQuery() != null ? message.getQuery() : ""); row.createCell(3).setCellValue(message.getQuery() != null ? message.getQuery() : "");
row.createCell(4).setCellValue(message.getAnswer() != null ? message.getAnswer() : ""); row.createCell(4).setCellValue(message.getAnswer() != null ? message.getAnswer() : "");
row.createCell(5).setCellValue(""); // 开始时间字段暂留空 LocalDateTime messageStartTime = message.getStartTime();
if (messageStartTime != null) {
String startTimeStr = messageStartTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
row.createCell(5).setCellValue(startTimeStr);
} else {
row.createCell(5).setCellValue("");
}
row.createCell(6).setCellValue(message.getDurationSeconds() != null ? message.getDurationSeconds() : 0); row.createCell(6).setCellValue(message.getDurationSeconds() != null ? message.getDurationSeconds() : 0);
if (message.getStatus() != null) {
int status = message.getStatus();
CaseAiChatErrCodeEnum errCodeEnum = CaseAiChatErrCodeEnum.getByCode(status);
row.createCell(7).setCellValue(errCodeEnum.getLabel());
}
if (StringUtils.isNotBlank(message.getErrorMsg())) {
row.createCell(8).setCellValue(message.getErrorMsg());
}
} }
// 合并单元格会话ID、会话名称、用户三列 // 合并单元格会话ID、会话名称、用户三列

View File

@@ -22,6 +22,8 @@ import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest; import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse; import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest; import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.client.indices.PutMappingRequest;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.query.QueryBuilders;
@@ -32,12 +34,17 @@ import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
@Service @Service
@Slf4j @Slf4j
@@ -125,6 +132,33 @@ public class ElasticSearchIndexServiceImpl implements IElasticSearchIndexService
} }
} }
@Override
public boolean updateIndex(String fieldName, Map<String, Object> fieldProperties) {
if (elasticsearchClient == null) {
log.error("ElasticSearch客户端未配置");
return false;
}
// 执行新增字段请求
JSONObject newField = new JSONObject();
newField.put(fieldName, fieldProperties);
PutMappingRequest request = new PutMappingRequest(CaseAiConstants.CASE_AI_INDEX_NAME);
request.source(newField.toJSONString(), XContentType.JSON);
try {
AcknowledgedResponse response = elasticsearchClient.indices().putMapping(request, RequestOptions.DEFAULT);
if (response.isAcknowledged()) {
log.info("成功更新Elasticsearch索引: {}, 新增字段: {}", CaseAiConstants.CASE_AI_INDEX_NAME, fieldName);
return true;
} else {
log.error("更新索引 [{}] 未被确认(可能部分节点未响应)", CaseAiConstants.CASE_AI_INDEX_NAME);
return false;
}
} catch (Exception e) {
log.error("更新ElasticSearch索引时发生异常", e);
return false;
}
}
@Override @Override
public boolean createData(AiChatConversationData conversationData) { public boolean createData(AiChatConversationData conversationData) {
if (elasticsearchClient == null) { if (elasticsearchClient == null) {
@@ -145,6 +179,9 @@ public class ElasticSearchIndexServiceImpl implements IElasticSearchIndexService
esData.put("timestamp", now.toString()); esData.put("timestamp", now.toString());
esData.put("durationSeconds", Duration.between(conversationData.getStartTime(), now).getSeconds()); esData.put("durationSeconds", Duration.between(conversationData.getStartTime(), now).getSeconds());
esData.put("status", conversationData.getStatus());
esData.put("errorMsg", conversationData.getErrorMsg());
// 构建 caseRefer 数据 // 构建 caseRefer 数据
JSONArray caseReferArray = new JSONArray(); JSONArray caseReferArray = new JSONArray();
for (CaseReferVo caseRefer : conversationData.getCaseRefers()) { for (CaseReferVo caseRefer : conversationData.getCaseRefers()) {
@@ -231,6 +268,20 @@ public class ElasticSearchIndexServiceImpl implements IElasticSearchIndexService
} }
} }
if (sourceMap.containsKey("status")) {
Object statusObj = sourceMap.get("status");
if (statusObj != null) {
messageVo.setStatus(Integer.valueOf(statusObj.toString()));
}
}
if (sourceMap.containsKey("errorMsg")) {
Object errorMsgObj = sourceMap.get("errorMsg");
if (errorMsgObj != null) {
messageVo.setErrorMsg(errorMsgObj.toString());
}
}
// 解析 caseRefer // 解析 caseRefer
if (sourceMap.containsKey("caseRefer")) { if (sourceMap.containsKey("caseRefer")) {
Object caseReferObj = sourceMap.get("caseRefer"); Object caseReferObj = sourceMap.get("caseRefer");
@@ -352,70 +403,15 @@ public class ElasticSearchIndexServiceImpl implements IElasticSearchIndexService
* @return JSON格式的映射配置 * @return JSON格式的映射配置
*/ */
private String getAiChatMessagesMapping() { private String getAiChatMessagesMapping() {
return "{\n" + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("case_ai_index.json");
" \"properties\": {\n" + if (inputStream != null) {
" \"conversationId\": {\n" + try (InputStreamReader isr = new InputStreamReader(inputStream);
" \"type\": \"keyword\",\n" + BufferedReader reader = new BufferedReader(isr)) {
" \"index\": true\n" + return reader.lines().collect(Collectors.joining(System.lineSeparator()));
" },\n" + } catch (IOException e) {
" \"query\": {\n" + throw new RuntimeException("Resource read error: case_ai_index.json", e);
" \"type\": \"text\",\n" + }
" \"analyzer\": \"ik_max_word\",\n" + }
" \"search_analyzer\": \"ik_smart\",\n" + throw new RuntimeException("Resource not found: case_ai_index.json");
" \"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" +
" \"timestamp\": {\n" +
" \"type\": \"date\",\n" +
" \"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSS||yyyy-MM-dd'T'HH:mm:ss.SSS'Z'||epoch_millis\"\n" +
" }\n" +
" }\n" +
"}";
} }
} }

View File

@@ -41,6 +41,19 @@ public class CaseAiMessageVo {
*/ */
private List<String> suggestions; private List<String> suggestions;
/**
* 状态
* 0-正常
* 1-系统错误
* 2-AIoT平台错误
*/
private Integer status;
/**
* 错误信息
*/
private String errorMsg;
/** /**
* 用户点赞状态 * 用户点赞状态
* -1: 踩 * -1: 踩

View File

@@ -0,0 +1,82 @@
{
"properties": {
"conversationId": {
"type": "keyword",
"index": true
},
"query": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"answer": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"caseRefer": {
"type": "nested",
"properties": {
"caseId": {
"type": "keyword",
"index": true
},
"title": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"authorName": {
"type": "keyword",
"index": true
},
"keywords": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
},
"suggestions": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"userId": {
"type": "keyword",
"index": true
},
"timestamp": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss||yyyy-MM-dd'T'HH:mm:ss.SSS||yyyy-MM-dd'T'HH:mm:ss.SSS'Z'||epoch_millis"
},
"status": {
"type": "integer"
},
"errorMsg": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"likeStatus": {
"type": "keyword",
"index": true
},
"feedback": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
}