From ee2770585cbf7e023d35370f94be34e07b4fdf1a Mon Sep 17 00:00:00 2001 From: "liu.zixi" Date: Thu, 4 Dec 2025 18:40:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=9A=E5=AF=BC=E5=87=BA=E6=B6=88=E6=81=AF=E6=97=B6=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/xboe/enums/CaseAiChatErrCodeEnum.java | 33 +++++ .../module/boecase/api/CaseAiChatApi.java | 14 +- .../xboe/module/boecase/dto/EsFieldDTO.java | 49 +++++++ .../entity/AiChatConversationData.java | 13 ++ .../service/IElasticSearchIndexService.java | 9 ++ .../service/impl/CaseAiChatServiceImpl.java | 30 ++++- .../impl/ElasticSearchIndexServiceImpl.java | 126 +++++++++--------- .../module/boecase/vo/CaseAiMessageVo.java | 13 ++ .../src/main/resources/case_ai_index.json | 82 ++++++++++++ 9 files changed, 302 insertions(+), 67 deletions(-) create mode 100644 servers/boe-server-all/src/main/java/com/xboe/enums/CaseAiChatErrCodeEnum.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/dto/EsFieldDTO.java create mode 100644 servers/boe-server-all/src/main/resources/case_ai_index.json diff --git a/servers/boe-server-all/src/main/java/com/xboe/enums/CaseAiChatErrCodeEnum.java b/servers/boe-server-all/src/main/java/com/xboe/enums/CaseAiChatErrCodeEnum.java new file mode 100644 index 00000000..658ed8df --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/enums/CaseAiChatErrCodeEnum.java @@ -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); + } +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java index ea207ec4..8c9216e7 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java @@ -4,6 +4,7 @@ import com.xboe.core.api.ApiBaseController; import com.xboe.core.JsonResponse; import com.xboe.module.boecase.dto.CaseAiChatDto; 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.entity.AiChatConversationData; import com.xboe.module.boecase.service.ICaseAiChatService; @@ -137,7 +138,7 @@ public class CaseAiChatApi extends ApiBaseController { return error("获取失败", e.getMessage()); } } - + /** * 根据conversationId查看会话内消息记录 * @param conversationId 会话ID @@ -207,6 +208,17 @@ public class CaseAiChatApi extends ApiBaseController { return error("刷新失败"); } + /** + * 添加索引字段 + * @param esFieldDTO + * @return + */ + @PostMapping("/index/add_field") + public JsonResponse addField(@RequestBody EsFieldDTO esFieldDTO) { + boolean result = elasticSearchIndexService.updateIndex(esFieldDTO.getFieldName(), esFieldDTO.getIndexProperties()); + return result ? success("添加成功") : error("添加失败"); + } + @PostMapping("/es/create") public JsonResponse createNewConversation(@RequestBody CaseAiMessageVo caseAiMessageVo, @RequestParam String conversationId, diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dto/EsFieldDTO.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dto/EsFieldDTO.java new file mode 100644 index 00000000..0eeafe72 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dto/EsFieldDTO.java @@ -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 getIndexProperties() { + Map 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; + } +} 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 index 9f1dfcb3..821f3933 100644 --- 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 @@ -34,6 +34,19 @@ public class AiChatConversationData { */ private StringBuilder answer = new StringBuilder(); + /** + * 状态 + * 0-正常 + * 1-系统错误 + * 2-AIoT平台错误 + */ + private Integer status; + + /** + * 错误信息 + */ + private String errorMsg; + /** * 案例引用列表 */ diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/IElasticSearchIndexService.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/IElasticSearchIndexService.java index 93908b1e..f33a3ddb 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/IElasticSearchIndexService.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/IElasticSearchIndexService.java @@ -4,6 +4,7 @@ import com.xboe.module.boecase.entity.AiChatConversationData; import com.xboe.module.boecase.vo.CaseAiMessageVo; import java.util.List; +import java.util.Map; /** * es索引 @@ -27,6 +28,14 @@ public interface IElasticSearchIndexService { */ boolean deleteIndex(); + /** + * 更新索引:添加索引字段 + * @param fieldName + * @param fieldProperties + * @return + */ + boolean updateIndex(String fieldName, Map fieldProperties); + /** * 新增数据 * @param data 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 831bf297..64b527b7 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 @@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONObject; import com.xboe.constants.CaseAiConstants; import com.xboe.core.CurrentUser; import com.xboe.core.orm.FieldFilters; +import com.xboe.enums.CaseAiChatErrCodeEnum; import com.xboe.enums.CaseAiChatStatusEnum; import com.xboe.module.boecase.dao.CaseAiConversationsDao; import com.xboe.module.boecase.dao.CaseDocumentLogDao; @@ -138,6 +139,7 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { conversationData.setQuery(caseAiChatDto.getQuery()); conversationData.setConversationId(conversationId); conversationData.setUserId(userId); + conversationData.setStatus(CaseAiChatErrCodeEnum.SUCCESS.getCode()); String kId = caseAiProperties.getCaseKnowledgeId(); JSONObject chatParam = new JSONObject(); @@ -173,6 +175,8 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { errMessage(sseEmitter, conversationId, CaseAiConstants.CHAT_SYS_ERR_MSG); // 先响应给前端 sseEmitter.complete(); + conversationData.setStatus(CaseAiChatErrCodeEnum.AIOT_ERROR.getCode()); + conversationData.setErrorMsg("获取AccessToken时发生异常"); conversationData.appendAnswer(CaseAiConstants.CHAT_SYS_ERR_MSG); saveConversationData(conversationData); return sseEmitter; @@ -182,6 +186,8 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { errMessage(sseEmitter, conversationId, CaseAiConstants.CHAT_SYS_ERR_MSG); // 先响应给前端 sseEmitter.complete(); + conversationData.setStatus(CaseAiChatErrCodeEnum.AIOT_ERROR.getCode()); + conversationData.setErrorMsg("获取AccessToken时发生异常" + e.getMessage()); conversationData.appendAnswer(CaseAiConstants.CHAT_SYS_ERR_MSG); saveConversationData(conversationData); return sseEmitter; @@ -232,6 +238,8 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { errMessage(sseEmitter, conversationId, sseContent); sseEmitter.complete(); + conversationData.setStatus(CaseAiChatErrCodeEnum.AIOT_ERROR.getCode()); + conversationData.setErrorMsg(sseContent); conversationData.appendAnswer(sseContent); saveConversationData(conversationData); // 关闭eventSource @@ -287,6 +295,8 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { // 异常问题,取message内容 String message = jsonData.getString("message"); errMessage(sseEmitter, conversationId, message); + conversationData.setStatus(CaseAiChatErrCodeEnum.AIOT_ERROR.getCode()); + conversationData.setErrorMsg(jsonData.toJSONString()); return; } } @@ -345,6 +355,8 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { // 从Map中移除失败的会话 conversationEventSourceMap.remove(conversationId); // 即使失败,也要将已有的对话数据保存到ES + conversationData.setStatus(CaseAiChatErrCodeEnum.AIOT_ERROR.getCode()); + conversationData.setErrorMsg(errorMessage); conversationData.appendAnswer(errorMessage); saveConversationData(conversationData); } @@ -928,6 +940,8 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { headerRow.createCell(4).setCellValue("回答"); headerRow.createCell(5).setCellValue("开始时间"); headerRow.createCell(6).setCellValue("问答时长(秒)"); + headerRow.createCell(7).setCellValue("消息状态"); + headerRow.createCell(8).setCellValue("错误信息"); // 内容行 if (!excelDataList.isEmpty()) { @@ -948,8 +962,22 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { row.createCell(2).setCellValue(excelData.getUser()); row.createCell(3).setCellValue(message.getQuery() != null ? message.getQuery() : ""); 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); + 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、会话名称、用户三列) diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/ElasticSearchIndexServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/ElasticSearchIndexServiceImpl.java index 88e42bda..d4a735bf 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/ElasticSearchIndexServiceImpl.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/ElasticSearchIndexServiceImpl.java @@ -22,6 +22,8 @@ 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.client.indices.PutMappingRequest; +import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentType; 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.stereotype.Service; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Service @Slf4j @@ -125,6 +132,33 @@ public class ElasticSearchIndexServiceImpl implements IElasticSearchIndexService } } + @Override + public boolean updateIndex(String fieldName, Map 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 public boolean createData(AiChatConversationData conversationData) { if (elasticsearchClient == null) { @@ -145,6 +179,9 @@ public class ElasticSearchIndexServiceImpl implements IElasticSearchIndexService esData.put("timestamp", now.toString()); esData.put("durationSeconds", Duration.between(conversationData.getStartTime(), now).getSeconds()); + esData.put("status", conversationData.getStatus()); + esData.put("errorMsg", conversationData.getErrorMsg()); + // 构建 caseRefer 数据 JSONArray caseReferArray = new JSONArray(); 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 if (sourceMap.containsKey("caseRefer")) { Object caseReferObj = sourceMap.get("caseRefer"); @@ -352,70 +403,15 @@ public class ElasticSearchIndexServiceImpl implements IElasticSearchIndexService * @return JSON格式的映射配置 */ private String getAiChatMessagesMapping() { - return "{\n" + - " \"properties\": {\n" + - " \"conversationId\": {\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" + - " \"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" + - "}"; + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("case_ai_index.json"); + if (inputStream != null) { + try (InputStreamReader isr = new InputStreamReader(inputStream); + BufferedReader reader = new BufferedReader(isr)) { + return reader.lines().collect(Collectors.joining(System.lineSeparator())); + } catch (IOException e) { + throw new RuntimeException("Resource read error: case_ai_index.json", e); + } + } + throw new RuntimeException("Resource not found: case_ai_index.json"); } } diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiMessageVo.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiMessageVo.java index 558b5e93..464b71c0 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiMessageVo.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiMessageVo.java @@ -41,6 +41,19 @@ public class CaseAiMessageVo { */ private List suggestions; + /** + * 状态 + * 0-正常 + * 1-系统错误 + * 2-AIoT平台错误 + */ + private Integer status; + + /** + * 错误信息 + */ + private String errorMsg; + /** * 用户点赞状态 * -1: 踩 diff --git a/servers/boe-server-all/src/main/resources/case_ai_index.json b/servers/boe-server-all/src/main/resources/case_ai_index.json new file mode 100644 index 00000000..6128d1ba --- /dev/null +++ b/servers/boe-server-all/src/main/resources/case_ai_index.json @@ -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" + } + } +} \ No newline at end of file