From 278336c9f7198590413e57165d7743211356c84d Mon Sep 17 00:00:00 2001 From: "liu.zixi" Date: Tue, 23 Sep 2025 17:04:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A1=88=E4=BE=8B=E4=B8=93=E5=AE=B6=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- servers/boe-server-all/pom.xml | 22 + .../enums/CaseDocumentLogCaseStatusEnum.java | 50 ++ .../enums/CaseDocumentLogOptStatusEnum.java | 51 ++ .../enums/CaseDocumentLogOptTypeEnum.java | 39 + .../module/boecase/api/CaseAiChatApi.java | 66 ++ .../boecase/api/CaseDocumentLogApi.java | 206 ++++++ .../boecase/dao/CaseAiConversationsDao.java | 24 + .../boecase/dao/CaseDocumentLogDao.java | 24 + .../module/boecase/dto/CaseAiChatDto.java | 21 + .../boecase/dto/CaseDocumentLogQueryDto.java | 48 ++ .../boecase/entity/CaseAiConversations.java | 38 + .../boecase/entity/CaseDocumentLog.java | 89 +++ .../boecase/properties/CaseAiProperties.java | 42 ++ .../service/IAiAccessTokenService.java | 13 + .../boecase/service/ICaseAiChatService.java | 38 + .../service/ICaseDocumentLogService.java | 40 + .../service/ICaseKnowledgeService.java | 41 ++ .../impl/AiAccessTokenServiceImpl.java | 88 +++ .../service/impl/CaseAiChatServiceImpl.java | 602 +++++++++++++++ .../impl/CaseDocumentLogServiceImpl.java | 280 +++++++ .../impl/CaseKnowledgeServiceImpl.java | 687 ++++++++++++++++++ .../boecase/vo/CaseAiConversationVo.java | 10 + .../module/boecase/vo/CaseAiMessageVo.java | 32 + .../module/boecase/vo/CaseDocumentLogVo.java | 76 ++ .../xboe/module/boecase/vo/CaseReferVo.java | 37 + .../src/main/resources/application-dev.yml | 8 + .../src/main/resources/application-pre.yml | 8 + .../src/main/resources/application-prod.yml | 8 + .../src/main/resources/application-test.yml | 8 + 29 files changed, 2696 insertions(+) create mode 100644 servers/boe-server-all/src/main/java/com/xboe/enums/CaseDocumentLogCaseStatusEnum.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/enums/CaseDocumentLogOptStatusEnum.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/enums/CaseDocumentLogOptTypeEnum.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseDocumentLogApi.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/dao/CaseAiConversationsDao.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/dao/CaseDocumentLogDao.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/dto/CaseAiChatDto.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/dto/CaseDocumentLogQueryDto.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/CaseAiConversations.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/CaseDocumentLog.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/properties/CaseAiProperties.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/IAiAccessTokenService.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseAiChatService.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseDocumentLogService.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseKnowledgeService.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/AiAccessTokenServiceImpl.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseAiChatServiceImpl.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseDocumentLogServiceImpl.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseKnowledgeServiceImpl.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiConversationVo.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiMessageVo.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseDocumentLogVo.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseReferVo.java diff --git a/servers/boe-server-all/pom.xml b/servers/boe-server-all/pom.xml index 9f4e7327..2c185edb 100644 --- a/servers/boe-server-all/pom.xml +++ b/servers/boe-server-all/pom.xml @@ -161,6 +161,11 @@ org.apache.httpcomponents httpclient + + org.apache.httpcomponents + httpmime + 4.5.13 + javax.mail javax.mail-api @@ -227,6 +232,23 @@ elasticsearch-rest-high-level-client 7.9.0 + + com.squareup.okhttp3 + okhttp + 4.2.0 + + + + com.squareup.okhttp3 + okhttp-sse + 4.2.0 + + + + com.alibaba + fastjson + 2.0.31 + com.xboe diff --git a/servers/boe-server-all/src/main/java/com/xboe/enums/CaseDocumentLogCaseStatusEnum.java b/servers/boe-server-all/src/main/java/com/xboe/enums/CaseDocumentLogCaseStatusEnum.java new file mode 100644 index 00000000..9ac42b7d --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/enums/CaseDocumentLogCaseStatusEnum.java @@ -0,0 +1,50 @@ +package com.xboe.enums; + +/** + * AI调用日志业务处理状态枚举 + */ +public enum CaseDocumentLogCaseStatusEnum { + + SUCCESS(1, "处理成功"), + FAILED(2, "处理失败"); + + private final Integer code; + private final String desc; + + CaseDocumentLogCaseStatusEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + + public Integer getCode() { + return code; + } + + public String getDesc() { + return desc; + } + + /** + * 根据code获取描述 + */ + public static String getDescByCode(Integer code) { + for (CaseDocumentLogCaseStatusEnum statusEnum : values()) { + if (statusEnum.getCode().equals(code)) { + return statusEnum.getDesc(); + } + } + return ""; + } + + /** + * 根据code获取枚举 + */ + public static CaseDocumentLogCaseStatusEnum getByCode(Integer code) { + for (CaseDocumentLogCaseStatusEnum statusEnum : values()) { + if (statusEnum.getCode().equals(code)) { + return statusEnum; + } + } + return null; + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/enums/CaseDocumentLogOptStatusEnum.java b/servers/boe-server-all/src/main/java/com/xboe/enums/CaseDocumentLogOptStatusEnum.java new file mode 100644 index 00000000..8c643236 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/enums/CaseDocumentLogOptStatusEnum.java @@ -0,0 +1,51 @@ +package com.xboe.enums; + +/** + * AI调用日志接口调用状态枚举 + */ +public enum CaseDocumentLogOptStatusEnum { + + CALLING(0, "调用中"), + SUCCESS(1, "调用成功"), + FAILED(2, "调用失败"); + + private final Integer code; + private final String desc; + + CaseDocumentLogOptStatusEnum(Integer code, String desc) { + this.code = code; + this.desc = desc; + } + + public Integer getCode() { + return code; + } + + public String getDesc() { + return desc; + } + + /** + * 根据code获取描述 + */ + public static String getDescByCode(Integer code) { + for (CaseDocumentLogOptStatusEnum statusEnum : values()) { + if (statusEnum.getCode().equals(code)) { + return statusEnum.getDesc(); + } + } + return ""; + } + + /** + * 根据code获取枚举 + */ + public static CaseDocumentLogOptStatusEnum getByCode(Integer code) { + for (CaseDocumentLogOptStatusEnum statusEnum : values()) { + if (statusEnum.getCode().equals(code)) { + return statusEnum; + } + } + return null; + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/enums/CaseDocumentLogOptTypeEnum.java b/servers/boe-server-all/src/main/java/com/xboe/enums/CaseDocumentLogOptTypeEnum.java new file mode 100644 index 00000000..c46b5fde --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/enums/CaseDocumentLogOptTypeEnum.java @@ -0,0 +1,39 @@ +package com.xboe.enums; + +/** + * AI调用日志操作类型枚举 + */ +public enum CaseDocumentLogOptTypeEnum { + + CREATE("create", "新增"), + DELETE("delete", "删除"), + UPDATE("update", "更改"); + + private final String code; + private final String desc; + + CaseDocumentLogOptTypeEnum(String code, String desc) { + this.code = code; + this.desc = desc; + } + + public String getCode() { + return code; + } + + public String getDesc() { + return desc; + } + + /** + * 根据code获取描述 + */ + public static String getDescByCode(String code) { + for (CaseDocumentLogOptTypeEnum typeEnum : values()) { + if (typeEnum.getCode().equals(code)) { + return typeEnum.getDesc(); + } + } + return ""; + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..d0ca8c73 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java @@ -0,0 +1,66 @@ +package com.xboe.module.boecase.api; + +import com.xboe.core.api.ApiBaseController; +import com.xboe.core.JsonResponse; +import com.xboe.module.boecase.dto.CaseAiChatDto; +import com.xboe.module.boecase.service.ICaseAiChatService; +import com.xboe.module.boecase.vo.CaseAiMessageVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * AI对话管理API + */ +@Slf4j +@RestController +@RequestMapping(value = "/xboe/m/boe/case/ai") +public class CaseAiChatApi extends ApiBaseController { + + /** + * 聊天 + * @param caseAiChatDto + * @param response + * @return + */ + @Autowired + private ICaseAiChatService caseAiChatService; + + /** + * 聊天 + * @param caseAiChatDto + * @param response + * @return + */ + @PostMapping(value = "/chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter chat(@RequestBody CaseAiChatDto caseAiChatDto, + HttpServletResponse response) { + response.setContentType("text/event-stream"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + // 获取当前用户 + return caseAiChatService.chat(caseAiChatDto, getCurrent()); + } + + /** + * 根据conversationId查看会话内消息记录 + * @param conversationId 会话ID + * @return 消息记录列表 + */ + @GetMapping("/messages") + public JsonResponse> getConversationMessages(@RequestParam String conversationId) { + try { + List messages = caseAiChatService.getConversationMessages(conversationId); + return success(messages); + } catch (Exception e) { + log.error("查询会话消息记录异常", e); + return error("查询失败", e.getMessage()); + } + } +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseDocumentLogApi.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseDocumentLogApi.java new file mode 100644 index 00000000..826a59cc --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseDocumentLogApi.java @@ -0,0 +1,206 @@ +package com.xboe.module.boecase.api; + +import com.xboe.common.PageList; +import com.xboe.core.JsonResponse; +import com.xboe.core.api.ApiBaseController; +import com.xboe.core.log.AutoLog; +import com.xboe.module.boecase.dto.CaseDocumentLogQueryDto; +import com.xboe.module.boecase.service.ICaseDocumentLogService; +import com.xboe.module.boecase.service.ICaseKnowledgeService; +import com.xboe.module.boecase.vo.CaseDocumentLogVo; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +/** + * AI调用日志管理API + */ +@Slf4j +@RestController +@RequestMapping(value = "/xboe/m/boe/caseDocumentLog") +public class CaseDocumentLogApi extends ApiBaseController { + + @Resource + private ICaseDocumentLogService caseDocumentLogService; + + @Resource + private ICaseKnowledgeService caseKnowledgeService; + + /** + * AI调用日志分页查询 + * + * @param queryDto 查询条件 + * @return 分页结果 + */ + @PostMapping("/pageQuery") + @AutoLog(module = "AI调用日志", action = "分页查询", info = "AI调用日志分页查询") + public JsonResponse> pageQuery(@RequestBody CaseDocumentLogQueryDto queryDto) { + try { + PageList result = caseDocumentLogService.pageQuery( + queryDto.getPageIndex(), + queryDto.getPageSize(), + queryDto + ); + return success(result); + } catch (Exception e) { + log.error("AI调用日志分页查询失败", e); + return error("查询失败", e.getMessage()); + } + } + + /** + * 清空日志(根据筛选条件) + * + * @param queryDto 查询条件 + * @return 删除结果 + */ + @PostMapping("/clearLogs") + @AutoLog(module = "AI调用日志", action = "清空日志", info = "AI调用日志清空操作") + public JsonResponse clearLogs(@RequestBody CaseDocumentLogQueryDto queryDto) { + try { + int deletedCount = caseDocumentLogService.clearLogsByCondition(queryDto); + return success(deletedCount); + } catch (Exception e) { + log.error("AI调用日志清空失败", e); + return error("清空失败", e.getMessage()); + } + } + + /** + * 重试AI调用 + * + * @param request 重试请求参数 + * @return 重试结果 + */ + @PostMapping("/retry") + @AutoLog(module = "AI调用日志", action = "重试调用", info = "AI调用日志重试操作") + public JsonResponse retry(@RequestBody RetryRequest request) { + try { + boolean result = caseDocumentLogService.retryByLogId(request.getLogId()); + return success(result); + } catch (Exception e) { + log.error("AI调用重试失败", e); + return error("重试失败", e.getMessage()); + } + } + + /** + * 重试请求参数 + */ + public static class RetryRequest { + private String logId; + + public String getLogId() { + return logId; + } + + public void setLogId(String logId) { + this.logId = logId; + } + } + + /** + * 文档上传回调接口 + * + * @param request 回调请求参数 + * @return 回调结果 + */ + @PostMapping("/uploadCallback") + @AutoLog(module = "AI调用日志", action = "文档上传回调", info = "文档上传回调接口") + public CallbackResponse uploadCallback(@RequestBody CallbackRequest request) { + try { + log.info("收到文档上传回调,taskId: {}, fileStatus: {}, message: {}", + request.getTaskId(), request.getFileStatus(), request.getMessage()); + + boolean result = caseKnowledgeService.handleUploadCallback( + request.getTaskId(), + request.getMessage(), + request.getFileStatus() + ); + + CallbackResponse response = new CallbackResponse(); + response.setSuccess(result); + response.setCode(result ? 0 : -1); + response.setMessage(result ? "回调处理成功" : "回调处理失败"); + + return response; + } catch (Exception e) { + log.error("文档上传回调处理失败", e); + + CallbackResponse response = new CallbackResponse(); + response.setSuccess(false); + response.setCode(-1); + response.setMessage("回调处理异常: " + e.getMessage()); + + return response; + } + } + + /** + * 回调请求参数 + */ + public static class CallbackRequest { + private String taskId; + private String message; + private String fileStatus; + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getFileStatus() { + return fileStatus; + } + + public void setFileStatus(String fileStatus) { + this.fileStatus = fileStatus; + } + } + + /** + * 回调响应参数 + */ + public static class CallbackResponse { + private boolean success; + private int code; + private String message; + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + } + +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dao/CaseAiConversationsDao.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dao/CaseAiConversationsDao.java new file mode 100644 index 00000000..86305ffd --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dao/CaseAiConversationsDao.java @@ -0,0 +1,24 @@ +package com.xboe.module.boecase.dao; + +import com.xboe.core.orm.BaseDao; +import com.xboe.core.orm.FieldFilters; +import com.xboe.module.boecase.entity.CaseAiConversations; +import org.springframework.stereotype.Repository; + +/** + * 案例AI会话信息DAO + */ +@Repository +public class CaseAiConversationsDao extends BaseDao { + + /** + * 根据主键ID查询AI会话ID + * @param conversationId 主键ID + * @return AI会话ID + */ + public String findAiConversationIdById(String conversationId) { + CaseAiConversations conversation = this.getGenericDao().findOne(CaseAiConversations.class, + FieldFilters.eq("id", conversationId)); + return conversation != null ? conversation.getAiConversationId() : null; + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dao/CaseDocumentLogDao.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dao/CaseDocumentLogDao.java new file mode 100644 index 00000000..d283791a --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dao/CaseDocumentLogDao.java @@ -0,0 +1,24 @@ +package com.xboe.module.boecase.dao; + +import com.xboe.core.orm.BaseDao; +import com.xboe.module.boecase.entity.CaseDocumentLog; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Repository; + +/** + * AI调用日志DAO + */ +@Repository +@Slf4j +public class CaseDocumentLogDao extends BaseDao { + + /** + * 根据taskId查询文档日志 + * @param taskId 任务ID + * @return 文档日志 + */ + public CaseDocumentLog findByTaskId(String taskId) { + return this.getGenericDao().findOne(CaseDocumentLog.class, + FieldFilters.eq("taskId", taskId)); + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dto/CaseAiChatDto.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dto/CaseAiChatDto.java new file mode 100644 index 00000000..578fa80c --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dto/CaseAiChatDto.java @@ -0,0 +1,21 @@ +package com.xboe.module.boecase.dto; + +import lombok.Data; + +/** + * AI对话入参 + */ +@Data +public class CaseAiChatDto { + + /** + * 对话id + * 如果是新对话,传空 + */ + private String conversationId; + + /** + * 提问内容 + */ + private String query; +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dto/CaseDocumentLogQueryDto.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dto/CaseDocumentLogQueryDto.java new file mode 100644 index 00000000..e1b7110c --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/dto/CaseDocumentLogQueryDto.java @@ -0,0 +1,48 @@ +package com.xboe.module.boecase.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * AI调用日志查询条件DTO + */ +@Data +public class CaseDocumentLogQueryDto extends PageDto { + + /** + * 案例标题(模糊查询) + */ + private String caseTitle; + + /** + * 操作类型(create-新增,delete-删除,update-更改) + */ + private String optType; + + /** + * 调用时间开始 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime optTimeStart; + + /** + * 调用时间结束 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime optTimeEnd; + + /** + * 接口调用状态 + * 0-调用中, 1-调用成功, 2-调用失败 + */ + private Integer optStatus; + + /** + * 业务处理状态 + * 1-处理成功, 2-处理失败 + */ + private Integer caseStatus; +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/CaseAiConversations.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/CaseAiConversations.java new file mode 100644 index 00000000..056958eb --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/CaseAiConversations.java @@ -0,0 +1,38 @@ +package com.xboe.module.boecase.entity; + +import com.xboe.core.SysConstant; +import com.xboe.core.orm.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; + +/** + * 案例AI会话信息表 + */ +@Data +@Entity +@EqualsAndHashCode(callSuper = false) +@Table(name = SysConstant.TABLE_PRE + "case_ai_conversations") +public class CaseAiConversations extends BaseEntity { + + /** + * 会话ID(由AI平台提供) + */ + @Column(name = "ai_conversation_id", length = 100) + private String aiConversationId; + + /** + * 会话名称 + */ + @Column(name = "conversation_name", length = 200) + private String conversationName; + + /** + * 会话对应用户ID + */ + @Column(name = "conversation_user", length = 50) + private String conversationUser; +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/CaseDocumentLog.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/CaseDocumentLog.java new file mode 100644 index 00000000..f072205e --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/CaseDocumentLog.java @@ -0,0 +1,89 @@ +package com.xboe.module.boecase.entity; + +import com.xboe.core.SysConstant; +import com.xboe.core.orm.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.time.LocalDateTime; + +/** + * 案例文档日志信息表 + */ +@Data +@Entity +@EqualsAndHashCode(callSuper = false) +@Table(name = SysConstant.TABLE_PRE + "case_document_log") +public class CaseDocumentLog extends BaseEntity { + + /** + * 任务ID + */ + @Column(name = "task_id", length = 20) + private String taskId; + + /** + * 案例id + */ + @Column(name = "case_id", length = 20) + private String caseId; + + /** + * 案例标题 + */ + @Column(name = "case_title", length = 200) + private String caseTitle; + + /** + * 操作类型 + */ + @Column(name = "opt_type") + private String optType; + + /** + * 请求地址 + */ + @Column(name = "request_url", length = 500) + private String requestUrl; + + /** + * 请求参数 + */ + @Column(name = "request_body", length = 4000) + private String requestBody; + + /** + * 响应参数 + */ + @Column(name = "response_body", length = 4000) + private String responseBody; + + /** + * 调用时间 + */ + @Column(name = "opt_time") + private LocalDateTime optTime; + + /** + * 接口调用状态 + * 0-调用中, 1-调用成功, 2-调用失败 + */ + @Column(name = "opt_status") + private Integer optStatus; + + /** + * 业务处理状态 + * 1-处理成功, 2-处理失败 + */ + @Column(name = "case_status") + private Integer caseStatus; + + /** + * 执行时间(ms) + */ + @Column(name = "execute_duration") + private Long executeDuration; +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/properties/CaseAiProperties.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/properties/CaseAiProperties.java new file mode 100644 index 00000000..58265825 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/properties/CaseAiProperties.java @@ -0,0 +1,42 @@ +package com.xboe.module.boecase.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 案例专家AI相关配置项 + */ +@ConfigurationProperties(prefix = "xboe.case.ai") +@Data +public class CaseAiProperties { + + /** + * 接口地址 + */ + private String baseUrl; + + /** + * appKey + */ + private String appKey; + + /** + * appSecret + */ + private String secretKey; + + /** + * ai接口的apiCode + */ + private String aiApiCode; + + /** + * 案例知识库id + */ + private String caseKnowledgeId; + + /** + * 文档上传回调接口地址 + */ + private String fileUploadCallbackUrl; +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/IAiAccessTokenService.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/IAiAccessTokenService.java new file mode 100644 index 00000000..0f1caa7b --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/IAiAccessTokenService.java @@ -0,0 +1,13 @@ +package com.xboe.module.boecase.service; + +/** + * 获取accesstoken + */ +public interface IAiAccessTokenService { + + /** + * 获取accesstoken + * @return + */ + String getAccessToken(); +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseAiChatService.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseAiChatService.java new file mode 100644 index 00000000..2ccd6e3d --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseAiChatService.java @@ -0,0 +1,38 @@ +package com.xboe.module.boecase.service; + +import com.xboe.core.CurrentUser; +import com.xboe.module.boecase.dto.CaseAiChatDto; +import com.xboe.module.boecase.entity.CaseAiConversations; +import com.xboe.module.boecase.vo.CaseAiMessageVo; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.List; + +/** + * AI案例对话 + */ +public interface ICaseAiChatService { + + /** + * 聊天 + * @param caseAiChatDto + * @param currentUser + * @return + */ + SseEmitter chat(CaseAiChatDto caseAiChatDto, CurrentUser currentUser); + + /** + * 创建新的AI对话会话 + * @param userId 用户ID + * @param conversationName 对话名称 + * @return 创建的会话信息 + */ + CaseAiConversations createNewConversation(String userId, String conversationName); + + /** + * 根据conversationId查看会话内消息记录 + * @param conversationId 会话ID + * @return 消息记录列表 + */ + List getConversationMessages(String conversationId); +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseDocumentLogService.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseDocumentLogService.java new file mode 100644 index 00000000..58cfa45b --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseDocumentLogService.java @@ -0,0 +1,40 @@ +package com.xboe.module.boecase.service; + +import com.xboe.common.PageList; +import com.xboe.module.boecase.dto.CaseDocumentLogQueryDto; +import com.xboe.module.boecase.vo.CaseDocumentLogVo; + +/** + * AI调用日志Service接口 + */ +public interface ICaseDocumentLogService { + + /** + * 分页查询AI调用日志 + * + * @param pageIndex 页码 + * @param pageSize 每页大小 + * @param queryDto 查询条件 + * @return 分页结果 + */ + PageList pageQuery(int pageIndex, int pageSize, CaseDocumentLogQueryDto queryDto); + + /** + * 根据查询条件清空日志 + * 仅删除当前筛选条件下的日志记录,非筛选范围内的日志不受影响 + * + * @param queryDto 查询条件 + * @return 删除的记录数 + */ + int clearLogsByCondition(CaseDocumentLogQueryDto queryDto); + + /** + * 根据logId重试AI调用 + * 查询原始日志数据,重试执行后添加新的日志记录 + * + * @param logId 日志ID + * @return 是否成功 + */ + boolean retryByLogId(String logId); + +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseKnowledgeService.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseKnowledgeService.java new file mode 100644 index 00000000..68d3f591 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/ICaseKnowledgeService.java @@ -0,0 +1,41 @@ +package com.xboe.module.boecase.service; + +/** + * 案例-知识库 + */ +public interface ICaseKnowledgeService { + + /** + * 上传案例文档 + * + * @param caseId 案例ID + * @return 是否成功 + */ + boolean uploadCaseDocument(String caseId); + + /** + * 删除案例文档 + * + * @param caseId 案例ID + * @return 是否成功 + */ + boolean deleteCaseDocument(String caseId); + + /** + * 更新案例文档 + * + * @param caseId 案例ID + * @return 是否成功 + */ + boolean updateCaseDocument(String caseId); + + /** + * 处理文档上传回调 + * + * @param taskId 任务ID + * @param message 回调信息 + * @param fileStatus 文件状态(vectored: 成功, failed: 失败) + * @return 是否处理成功 + */ + boolean handleUploadCallback(String taskId, String message, String fileStatus); +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/AiAccessTokenServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/AiAccessTokenServiceImpl.java new file mode 100644 index 00000000..dab854ff --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/AiAccessTokenServiceImpl.java @@ -0,0 +1,88 @@ +package com.xboe.module.boecase.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.xboe.common.utils.StringUtil; +import com.xboe.module.boecase.properties.CaseAiProperties; +import com.xboe.module.boecase.service.IAiAccessTokenService; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; + +@EnableConfigurationProperties({CaseAiProperties.class}) +@Service +@Slf4j +public class AiAccessTokenServiceImpl implements IAiAccessTokenService { + + private static final String ACCESS_TOKEN_CACHE_KEY = "case_ai_access_token"; + + @Autowired + private CaseAiProperties caseAiProperties; + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Override + public String getAccessToken() { + // 1. 先从Redis缓存中获取 + String cachedToken = stringRedisTemplate.opsForValue().get(ACCESS_TOKEN_CACHE_KEY); + if (StringUtil.isNotBlank(cachedToken)) { + return cachedToken; + } + + // 2. 缓存中没有,重新获取 + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + String tokenUrl = caseAiProperties.getBaseUrl() + "/apigateway/secret/getAppAccessToken" + + "?appKey=" + URLEncoder.encode(caseAiProperties.getAppKey(), StandardCharsets.UTF_8.name()) + + "&secretKey=" + URLEncoder.encode(caseAiProperties.getSecretKey(), StandardCharsets.UTF_8.name()); + + HttpGet httpGet = new HttpGet(tokenUrl); + + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + + if (statusCode == 200) { + JSONObject result = JSON.parseObject(responseBody); + if (result.getIntValue("code") == 0 && result.getBooleanValue("success")) { + JSONObject data = result.getJSONObject("data"); + String accessToken = data.getString("accessToken"); + Integer expiresIn = data.getInteger("expiresIn"); + if (expiresIn == null) { + expiresIn = 7200; + } + + // 3. 存储到Redis,设置过期时间(提前5分钟过期) + int cacheSeconds = Math.max(expiresIn - 300, 60); + stringRedisTemplate.opsForValue().set(ACCESS_TOKEN_CACHE_KEY, accessToken, + cacheSeconds, TimeUnit.SECONDS); + + log.info("获取access_token成功,过期时间: {}秒", expiresIn); + return accessToken; + } else { + log.error("获取access_token失败,接口返回失败,response: {}", responseBody); + return null; + } + } else { + log.error("获取access_token失败,HTTP请求失败,status: {}, response: {}", + statusCode, responseBody); + return null; + } + } + } catch (Exception e) { + log.error("获取access_token异常", e); + return null; + } + } +} 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 new file mode 100644 index 00000000..5355007f --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseAiChatServiceImpl.java @@ -0,0 +1,602 @@ +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.module.boecase.dao.CaseAiConversationsDao; +import com.xboe.module.boecase.dao.CaseDocumentLogDao; +import com.xboe.module.boecase.dao.CasesDao; +import com.xboe.module.boecase.dto.CaseAiChatDto; +import com.xboe.module.boecase.entity.CaseAiConversations; +import com.xboe.module.boecase.entity.CaseDocumentLog; +import com.xboe.module.boecase.entity.Cases; +import com.xboe.module.boecase.properties.CaseAiProperties; +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 lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import okhttp3.sse.EventSource; +import okhttp3.sse.EventSourceListener; +import okhttp3.sse.EventSources; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchHits; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@EnableConfigurationProperties({CaseAiProperties.class}) +@Service +@Slf4j +public class CaseAiChatServiceImpl implements ICaseAiChatService { + + @Autowired + private CaseAiProperties caseAiProperties; + + @Autowired + private IAiAccessTokenService aiAccessTokenService; + + @Autowired + private CaseAiConversationsDao caseAiConversationsDao; + + @Autowired(required = false) + private RestHighLevelClient elasticsearchClient; + + @Autowired + private CaseDocumentLogDao caseDocumentLogDao; + + @Autowired + private CasesDao casesDao; + + @Override + public SseEmitter chat(CaseAiChatDto caseAiChatDto, CurrentUser currentUser) { + // 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(); + JSONObject chatParam = new JSONObject(); + chatParam.put("userId", userId); + JSONArray kIds = new JSONArray(); + kIds.add(kId); + chatParam.put("kIds", kIds); + chatParam.put("query", caseAiChatDto.getQuery()); + chatParam.put("conversationId", conversationId); + + // 4. 设置请求头 + String accessToken = aiAccessTokenService.getAccessToken(); + String apiCode = caseAiProperties.getAiApiCode(); + Request.Builder builder = new Request.Builder(); + builder.url(caseAiProperties.getBaseUrl() + "/apigateway/chat/knowledge/v1/chat/completions"); + builder.addHeader("access_token", accessToken); + builder.addHeader("X-AI-ApiCode", apiCode); + RequestBody bodyRequestBody = RequestBody.create(chatParam.toJSONString(), MediaType.parse("application/json")); + builder.post(bodyRequestBody); + Request request = builder.build(); + + // 5. 创建SSE响应器 + SseEmitter sseEmitter = new SseEmitter(); + + // 6. 用于收集对话数据的容器 + ConversationData conversationData = new ConversationData(); + conversationData.query = caseAiChatDto.getQuery(); + conversationData.conversationId = conversationId; + conversationData.userId = userId; + + // 7. 创建事件监听器 + EventSourceListener listener = new EventSourceListener() { + @Override + public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) { + log.info("调用接口 [{}] 接口开始监听", request.url()); + } + + @Override + public void onClosed(@NotNull EventSource eventSource) { + log.info("调用接口 [{}] 接口关闭", request.url()); + // 对话完成,保存到ES + saveConversationToES(conversationData); + sseEmitter.complete(); + } + + @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); + + try { + // 解析返回的数据 + JSONObject jsonData = JSONObject.parseObject(data); + if (jsonData.getBooleanValue("success") && jsonData.getIntValue("code") == 0) { + JSONObject responseData = jsonData.getJSONObject("data"); + Integer status = responseData.getInteger("status"); + + if (status != null) { + switch (status) { + case 0: // 返回引用文件 + // 处理文件引用并构建返给前端的数据 + JSONObject modifiedData = handleFileReferAndBuildResponse(responseData, conversationData); + if (modifiedData != null) { + // 发送修改后的数据给前端 + sseEmitter.send(modifiedData.toJSONString()); + return; // 早期返回,不发送原始数据 + } + break; + case 1: // 流式对话中 + String content = responseData.getString("content"); + if (content != null) { + conversationData.answer.append(content); + } + break; + case 2: // 回答完成 + // 不做特殊处理 + break; + case 3: // 返回建议 + handleSuggestions(responseData, conversationData); + break; + case 4: // 接口交互完成 + // 不做特殊处理 + break; + } + } + } + + // 发送给前端 + sseEmitter.send(data); + } catch (IOException e) { + log.error("调用接口处理监听数据时发生异常", e); + } catch (Exception e) { + log.error("解析EventStream数据异常", e); + try { + sseEmitter.send(data); // 即使解析失败也要发送原始数据 + } catch (IOException ioException) { + log.error("发送数据到前端失败", ioException); + } + } + } + + @Override + public void onFailure(@NotNull EventSource eventSource, @Nullable Throwable e, @Nullable Response response) { + log.error("调用接口 [{}] 接口异常", request.url(), e); + if (e != null) { + sseEmitter.completeWithError(e); + } else { + sseEmitter.completeWithError(new RuntimeException("调用接口异常, 异常未捕获")); + } + } + }; + + // 8. 执行HTTP请求 + OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(60, TimeUnit.SECONDS) + .writeTimeout(60, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .build(); + EventSource.Factory factory = EventSources.createFactory(client); + factory.newEventSource(request, listener); + + return sseEmitter; + } + + /** + * 获取或创建会话ID + */ + private String getOrCreateConversationId(CaseAiChatDto caseAiChatDto, CurrentUser currentUser) { + String conversationId = caseAiChatDto.getConversationId(); + + if (StringUtils.isEmpty(conversationId)) { + // 新会话,调用创建会话接口 + String conversationName = "AI案例咨询-" + LocalDateTime.now().toString(); + CaseAiConversations newConversation = createNewConversation(currentUser.getAccountId(), conversationName); + return newConversation.getAiConversationId(); + } else { + // 已存在会话,从数据库查询 + return caseAiConversationsDao.findAiConversationIdById(conversationId); + } + } + + @Override + public CaseAiConversations createNewConversation(String userId, String conversationName) { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + String url = caseAiProperties.getBaseUrl() + "/apigateway/knowledge/v1/conversation"; + HttpPost httpPost = new HttpPost(url); + + // 设置请求头 + String accessToken = aiAccessTokenService.getAccessToken(); + String apiCode = caseAiProperties.getAiApiCode(); + httpPost.setHeader("access_token", accessToken); + httpPost.setHeader("X-AI-ApiCode", apiCode); + httpPost.setHeader("Content-Type", "application/json"); + + // 设置请求体 + JSONObject requestBody = new JSONObject(); + requestBody.put("userId", userId); + requestBody.put("name", conversationName); + StringEntity entity = new StringEntity(requestBody.toJSONString(), StandardCharsets.UTF_8); + httpPost.setEntity(entity); + + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + + if (statusCode == 200) { + JSONObject result = JSONObject.parseObject(responseBody); + if (result.getIntValue("code") == 0 && result.getBooleanValue("success")) { + JSONObject data = result.getJSONObject("data"); + String aiConversationId = data.getString("id"); + String name = data.getString("name"); + + // 保存到数据库 + CaseAiConversations conversation = new CaseAiConversations(); + conversation.setAiConversationId(aiConversationId); + conversation.setConversationName(name); + conversation.setConversationUser(userId); + caseAiConversationsDao.save(conversation); + + log.info("创建AI会话成功,aiConversationId: {}, name: {}", aiConversationId, name); + return conversation; + } else { + log.error("创建AI会话失败,接口返回失败,response: {}", responseBody); + throw new RuntimeException("创建AI会话失败: " + result.getString("message")); + } + } else { + log.error("创建AI会话失败,HTTP请求失败,status: {}, response: {}", statusCode, responseBody); + throw new RuntimeException("创建AI会话失败,HTTP状态码: " + statusCode); + } + } + } catch (Exception e) { + log.error("创建AI会话异常", e); + throw new RuntimeException("创建AI会话异常", e); + } + } + + @Override + public List getConversationMessages(String conversationId) { + List messages = new ArrayList<>(); + + if (elasticsearchClient == null) { + log.warn("未配置Elasticsearch客户端,无法查询消息记录"); + return messages; + } + + try { + // 根据conversationId从数据库查询AI会话ID + String aiConversationId = caseAiConversationsDao.findAiConversationIdById(conversationId); + if (StringUtils.isEmpty(aiConversationId)) { + log.warn("未找到conversationId: {}对应的AI会话ID", conversationId); + return messages; + } + + // 从 ES 中查询消息记录 + SearchRequest searchRequest = new SearchRequest("ai_chat_messages"); // ES索引名,可以根据实际情况调整 + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.query(QueryBuilders.termQuery("conversationId", aiConversationId)); + searchSourceBuilder.size(1000); // 设置最大返回数量 + searchRequest.source(searchSourceBuilder); + + SearchResponse searchResponse = elasticsearchClient.search(searchRequest, RequestOptions.DEFAULT); + SearchHits hits = searchResponse.getHits(); + + for (SearchHit hit : hits) { + Map sourceMap = hit.getSourceAsMap(); + CaseAiMessageVo messageVo = parseMessageFromES(sourceMap); + if (messageVo != null) { + messages.add(messageVo); + } + } + + log.info("从 ES 中查询到 {} 条消息记录", messages.size()); + + } catch (Exception e) { + log.error("从 ES 查询会话消息记录异常", e); + } + + return messages; + } + + /** + * 从 ES 数据中解析消息对象 + * @param sourceMap ES数据 + * @return 消息对象 + */ + private CaseAiMessageVo parseMessageFromES(Map sourceMap) { + try { + CaseAiMessageVo messageVo = new CaseAiMessageVo(); + messageVo.setQuery((String) sourceMap.get("query")); + messageVo.setAnswer((String) sourceMap.get("answer")); + + // 解析 suggestions + Object suggestionsObj = sourceMap.get("suggestions"); + if (suggestionsObj instanceof List) { + messageVo.setSuggestions((List) suggestionsObj); + } + + // 解析 caseRefer + Object caseReferObj = sourceMap.get("caseRefer"); + if (caseReferObj instanceof List) { + List caseReferList = new ArrayList<>(); + List> caseReferMaps = (List>) caseReferObj; + + for (Map caseReferMap : caseReferMaps) { + CaseReferVo caseRefer = new CaseReferVo(); + caseRefer.setCaseId((String) caseReferMap.get("caseId")); + caseRefer.setTitle((String) caseReferMap.get("title")); + caseRefer.setAuthorName((String) caseReferMap.get("authorName")); + caseRefer.setContent((String) caseReferMap.get("content")); + + // 解析 keywords + Object keywordsObj = caseReferMap.get("keywords"); + if (keywordsObj instanceof List) { + caseRefer.setKeywords((List) keywordsObj); + } + + caseReferList.add(caseRefer); + } + messageVo.setCaseRefer(caseReferList); + } + + return messageVo; + } catch (Exception e) { + log.error("解析ES消息数据异常", e); + return null; + } + } + + /** + * 处理文件引用并构建返给前端的响应数据 + */ + private JSONObject handleFileReferAndBuildResponse(JSONObject responseData, ConversationData conversationData) { + try { + // 先处理文件引用,收集CaseReferVo数据 + List currentCaseRefers = new ArrayList<>(); + + JSONObject fileRefer = responseData.getJSONObject("fileRefer"); + if (fileRefer != null && fileRefer.containsKey("files")) { + JSONArray files = fileRefer.getJSONArray("files"); + for (int i = 0; i < files.size(); i++) { + JSONObject file = files.getJSONObject(i); + String docId = file.getString("docId"); + if (docId != null) { + // 根据docId从 case_document_log 表查询案例数据 + CaseReferVo caseRefer = getCaseReferByDocId(docId); + if (caseRefer != null) { + currentCaseRefers.add(caseRefer); + conversationData.caseRefers.add(caseRefer); // 也添加到总的收集器中 + } + } + } + } + + // 构建返给前端的数据结构 + JSONObject modifiedResponse = new JSONObject(); + modifiedResponse.put("success", true); + modifiedResponse.put("code", 0); + modifiedResponse.put("message", "业务处理成功"); + + JSONObject data = new JSONObject(); + data.put("status", 0); + data.put("content", responseData.getString("content")); + + // 添加处理后的案例引用数据 + JSONArray caseReferArray = new JSONArray(); + for (CaseReferVo caseRefer : currentCaseRefers) { + JSONObject caseReferObj = new JSONObject(); + caseReferObj.put("caseId", caseRefer.getCaseId()); + caseReferObj.put("title", caseRefer.getTitle()); + caseReferObj.put("authorName", caseRefer.getAuthorName()); + caseReferObj.put("keywords", caseRefer.getKeywords()); + caseReferObj.put("content", caseRefer.getContent()); + caseReferArray.add(caseReferObj); + } + + // 构建新的fileRefer结构,包含案例引用 + JSONObject newFileRefer = new JSONObject(); + newFileRefer.put("caseRefers", caseReferArray); + + // 保留原始的docs和files信息(如果需要) + if (fileRefer != null) { + if (fileRefer.containsKey("docs")) { + newFileRefer.put("docs", fileRefer.get("docs")); + } + if (fileRefer.containsKey("files")) { + newFileRefer.put("files", fileRefer.get("files")); + } + } + + data.put("fileRefer", newFileRefer); + data.put("suggestions", responseData.get("suggestions")); + + modifiedResponse.put("data", data); + + log.info("处理文件引用成功,返回 {} 个案例引用", currentCaseRefers.size()); + return modifiedResponse; + + } catch (Exception e) { + log.error("处理文件引用并构建响应数据异常", e); + return null; + } + } + + /** + * 处理文件引用(原方法,保留用于数据收集) + */ + private void handleFileRefer(JSONObject responseData, ConversationData conversationData) { + try { + JSONObject fileRefer = responseData.getJSONObject("fileRefer"); + if (fileRefer != null && fileRefer.containsKey("files")) { + JSONArray files = fileRefer.getJSONArray("files"); + for (int i = 0; i < files.size(); i++) { + JSONObject file = files.getJSONObject(i); + String docId = file.getString("docId"); + if (docId != null) { + // 根据docId从 case_document_log 表查询案例数据 + CaseReferVo caseRefer = getCaseReferByDocId(docId); + if (caseRefer != null) { + conversationData.caseRefers.add(caseRefer); + } + } + } + } + } catch (Exception e) { + log.error("处理文件引用异常", e); + } + } + + /** + * 处理建议 + */ + private void handleSuggestions(JSONObject responseData, ConversationData 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); + } + } + } + } catch (Exception e) { + log.error("处理建议异常", e); + } + } + + /** + * 根据docId查询案例引用信息 + */ + private CaseReferVo getCaseReferByDocId(String docId) { + try { + // 从 case_document_log 表查询案例信息(docId对应task_id) + CaseDocumentLog docLog = caseDocumentLogDao.findByTaskId(docId); + if (docLog == null) { + return null; + } + + // 根据 case_id 查询案例详情 + Cases caseEntity = casesDao.get(docLog.getCaseId()); + if (caseEntity == null) { + return null; + } + + // 构建 CaseReferVo + CaseReferVo caseRefer = new CaseReferVo(); + caseRefer.setCaseId(caseEntity.getId()); + caseRefer.setTitle(caseEntity.getTitle()); + caseRefer.setAuthorName(caseEntity.getAuthorName()); + caseRefer.setContent(caseEntity.getContent()); + + // 构建关键词列表 + List keywords = new ArrayList<>(); + if (caseEntity.getKeyword1() != null) keywords.add(caseEntity.getKeyword1()); + if (caseEntity.getKeyword2() != null) keywords.add(caseEntity.getKeyword2()); + if (caseEntity.getKeyword3() != null) keywords.add(caseEntity.getKeyword3()); + if (caseEntity.getKeyword4() != null) keywords.add(caseEntity.getKeyword4()); + if (caseEntity.getKeyword5() != null) keywords.add(caseEntity.getKeyword5()); + caseRefer.setKeywords(keywords); + + return caseRefer; + } catch (Exception e) { + log.error("根据docId查询案例引用信息异常", e); + return null; + } + } + + /** + * 保存对话记录到ES + */ + private void saveConversationToES(ConversationData conversationData) { + if (elasticsearchClient == null) { + log.warn("未配置Elasticsearch客户端,无法保存对话记录"); + return; + } + + 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("timestamp", LocalDateTime.now().toString()); + + // 构建 caseRefer 数据 + JSONArray caseReferArray = new JSONArray(); + for (CaseReferVo caseRefer : conversationData.caseRefers) { + JSONObject caseReferObj = new JSONObject(); + caseReferObj.put("caseId", caseRefer.getCaseId()); + caseReferObj.put("title", caseRefer.getTitle()); + caseReferObj.put("authorName", caseRefer.getAuthorName()); + caseReferObj.put("keywords", caseRefer.getKeywords()); + caseReferObj.put("content", caseRefer.getContent()); + caseReferArray.add(caseReferObj); + } + esData.put("caseRefer", caseReferArray); + + // 添加建议 + esData.put("suggestions", conversationData.suggestions); + + // 保存到ES + IndexRequest indexRequest = new IndexRequest("ai_chat_messages"); + indexRequest.source(esData.toJSONString(), XContentType.JSON); + + IndexResponse indexResponse = elasticsearchClient.index(indexRequest, RequestOptions.DEFAULT); + log.info("保存对话记录到ES成功,文档ID: {}", indexResponse.getId()); + + } catch (Exception e) { + log.error("保存对话记录到ES异常", e); + } + } + + /** + * 对话数据容器 + */ + 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; + } +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseDocumentLogServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseDocumentLogServiceImpl.java new file mode 100644 index 00000000..eebcfd7b --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseDocumentLogServiceImpl.java @@ -0,0 +1,280 @@ +package com.xboe.module.boecase.service.impl; + +import com.xboe.common.utils.StringUtil; +import com.xboe.common.utils.IDGenerator; +import com.xboe.common.OrderCondition; +import com.xboe.common.PageList; +import com.xboe.core.orm.FieldFilters; +import com.xboe.core.orm.IFieldFilter; +import com.xboe.core.orm.LikeMatchMode; +import com.xboe.module.boecase.dao.CaseDocumentLogDao; +import com.xboe.module.boecase.dto.CaseDocumentLogQueryDto; +import com.xboe.module.boecase.entity.CaseDocumentLog; +import com.xboe.module.boecase.service.ICaseDocumentLogService; +import com.xboe.module.boecase.vo.CaseDocumentLogVo; +import com.xboe.enums.CaseDocumentLogOptTypeEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * AI调用日志Service实现类 + */ +@Slf4j +@Service +@Transactional +public class CaseDocumentLogServiceImpl implements ICaseDocumentLogService { + + @Resource + private CaseDocumentLogDao caseDocumentLogDao; + + @Override + public PageList pageQuery(int pageIndex, int pageSize, CaseDocumentLogQueryDto queryDto) { + // 构建查询条件 + List filters = new ArrayList<>(); + + // 删除标识过滤 + filters.add(FieldFilters.eq("deleted", false)); + + // 案例标题模糊查询 + if (StringUtil.isNotBlank(queryDto.getCaseTitle())) { + filters.add(FieldFilters.like("caseTitle", LikeMatchMode.ANYWHERE, queryDto.getCaseTitle())); + } + + // 操作类型精确查询 + if (StringUtil.isNotBlank(queryDto.getOptType())) { + filters.add(FieldFilters.eq("optType", queryDto.getOptType())); + } + + // 调用时间区间查询 + if (queryDto.getOptTimeStart() != null) { + filters.add(FieldFilters.ge("optTime", queryDto.getOptTimeStart())); + } + if (queryDto.getOptTimeEnd() != null) { + filters.add(FieldFilters.le("optTime", queryDto.getOptTimeEnd())); + } + + // 接口调用状态 + if (queryDto.getOptStatus() != null) { + filters.add(FieldFilters.eq("optStatus", queryDto.getOptStatus())); + } + + // 业务处理状态 + if (queryDto.getCaseStatus() != null) { + filters.add(FieldFilters.eq("caseStatus", queryDto.getCaseStatus())); + } + + // 按创建时间降序排序 + OrderCondition order = OrderCondition.desc("sysCreateTime"); + + // 执行分页查询 + PageList pageResult = caseDocumentLogDao.getGenericDao() + .findPage(pageIndex, pageSize, CaseDocumentLog.class, filters, order); + + // 转换为VO对象 + List voList = pageResult.getList().stream() + .map(this::convertToVo) + .collect(Collectors.toList()); + + // 构建返回结果 + PageList result = new PageList<>(); + result.setList(voList); + result.setCount(pageResult.getCount()); + result.setPageSize(pageResult.getPageSize()); + + return result; + } + + @Override + public int clearLogsByCondition(CaseDocumentLogQueryDto queryDto) { + // 构建查询条件(与分页查询相同的逻辑) + List filters = new ArrayList<>(); + + // 删除标识过滤 + filters.add(FieldFilters.eq("deleted", false)); + + // 案例标题模糊查询 + if (StringUtil.isNotBlank(queryDto.getCaseTitle())) { + filters.add(FieldFilters.like("caseTitle", LikeMatchMode.ANYWHERE, queryDto.getCaseTitle())); + } + + // 操作类型精确查询 + if (StringUtil.isNotBlank(queryDto.getOptType())) { + filters.add(FieldFilters.eq("optType", queryDto.getOptType())); + } + + // 调用时间区间查询 + if (queryDto.getOptTimeStart() != null) { + filters.add(FieldFilters.ge("optTime", queryDto.getOptTimeStart())); + } + if (queryDto.getOptTimeEnd() != null) { + filters.add(FieldFilters.le("optTime", queryDto.getOptTimeEnd())); + } + + // 接口调用状态 + if (queryDto.getOptStatus() != null) { + filters.add(FieldFilters.eq("optStatus", queryDto.getOptStatus())); + } + + // 业务处理状态 + if (queryDto.getCaseStatus() != null) { + filters.add(FieldFilters.eq("caseStatus", queryDto.getCaseStatus())); + } + + // 查询符合条件的所有记录 + List logsToDelete = caseDocumentLogDao.getGenericDao() + .findList(CaseDocumentLog.class, (IFieldFilter) filters); + + if (logsToDelete.isEmpty()) { + return 0; + } + + // 批量设置删除标识为true(逻辑删除) + int deletedCount = 0; + for (CaseDocumentLog log : logsToDelete) { + log.setDeleted(true); + caseDocumentLogDao.update(log); + deletedCount++; + } + + log.info("清空日志操作完成,共删除{}\u6761记录", deletedCount); + return deletedCount; + } + + @Override + public boolean retryByLogId(String logId) { + if (StringUtil.isBlank(logId)) { + log.error("重试失败,logId不能为空"); + return false; + } + + // 1. 根据logId查询原始日志数据 + CaseDocumentLog originalLog = caseDocumentLogDao.get(logId); + if (originalLog == null || originalLog.getDeleted()) { + log.error("重试失败,未找到有效的日志记录,logId: {}", logId); + return false; + } + + log.info("开始重试AI调用,原始日志ID: {}, 案例标题: {}, 操作类型: {}", + logId, originalLog.getCaseTitle(), originalLog.getOptType()); + + // 2. 创建新的日志记录 + CaseDocumentLog newLog = new CaseDocumentLog(); + newLog.setId(IDGenerator.generate()); + newLog.setTaskId(originalLog.getTaskId()); + newLog.setCaseId(originalLog.getCaseId()); + newLog.setCaseTitle(originalLog.getCaseTitle()); + newLog.setOptType(originalLog.getOptType()); + newLog.setRequestUrl(originalLog.getRequestUrl()); + newLog.setRequestBody(originalLog.getRequestBody()); + newLog.setOptTime(LocalDateTime.now()); + + // 3. 执行AI调用重试逻辑 + boolean retrySuccess = false; + long startTime = System.currentTimeMillis(); + + try { + // TODO: 在这里实现具体的AI调用重试逻辑 + // 模拟执行结果(实际开发时需要替换为真实的AI调用逻辑) + log.info("正在执行AI调用重试..."); + + // 这里应该有真实的AI调用逻辑 + // 例如: + // String response = aiService.callAI(originalLog.getRequestBody()); + // newLog.setResponseBody(response); + + // 模拟成功的响应 + newLog.setResponseBody("{\"success\": true, \"message\": \"重试成功\"}"); + newLog.setOptStatus(1); // 1-调用成功 + newLog.setCaseStatus(1); // 1-处理成功 + retrySuccess = true; + + log.info("AI调用重试成功"); + + } catch (Exception e) { + log.error("AI调用重试失败", e); + newLog.setResponseBody("{\"success\": false, \"error\": \"" + e.getMessage() + "\"}"); + newLog.setOptStatus(2); // 2-调用失败 + newLog.setCaseStatus(2); // 2-处理失败 + retrySuccess = false; + } finally { + // 4. 记录执行时间 + long endTime = System.currentTimeMillis(); + newLog.setExecuteDuration(endTime - startTime); + + // 5. 保存新的日志记录 + try { + caseDocumentLogDao.save(newLog); + log.info("重试日志记录保存成功,新日志ID: {}, 执行状态: {}", + newLog.getId(), retrySuccess ? "成功" : "失败"); + } catch (Exception e) { + log.error("保存重试日志记录失败", e); + return false; + } + } + + return retrySuccess; + } + + /** + * 实体转换为VO + */ + private CaseDocumentLogVo convertToVo(CaseDocumentLog entity) { + CaseDocumentLogVo vo = new CaseDocumentLogVo(); + BeanUtils.copyProperties(entity, vo); + + // 操作类型转换为中文描述 + vo.setOptType(CaseDocumentLogOptTypeEnum.getDescByCode(entity.getOptType())); + + // 接口调用状态转换 + vo.setOptStatusText(getOptStatusText(entity.getOptStatus())); + + // 业务处理状态转换 + vo.setCaseStatusText(getCaseStatusText(entity.getCaseStatus())); + + return vo; + } + + /** + * 获取接口调用状态描述 + */ + private String getOptStatusText(Integer optStatus) { + if (optStatus == null) { + return ""; + } + switch (optStatus) { + case 0: + return "调用中"; + case 1: + return "调用成功"; + case 2: + return "调用失败"; + default: + return ""; + } + } + + /** + * 获取业务处理状态描述 + */ + private String getCaseStatusText(Integer caseStatus) { + if (caseStatus == null) { + return ""; + } + switch (caseStatus) { + case 1: + return "处理成功"; + case 2: + return "处理失败"; + default: + return ""; + } + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseKnowledgeServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseKnowledgeServiceImpl.java new file mode 100644 index 00000000..f1520ba0 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseKnowledgeServiceImpl.java @@ -0,0 +1,687 @@ +package com.xboe.module.boecase.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.xboe.common.utils.IDGenerator; +import com.xboe.common.utils.StringUtil; +import com.xboe.common.OrderCondition; +import com.xboe.core.orm.FieldFilters; +import com.xboe.enums.CaseDocumentLogCaseStatusEnum; +import com.xboe.enums.CaseDocumentLogOptStatusEnum; +import com.xboe.module.boecase.dao.CaseDocumentLogDao; +import com.xboe.module.boecase.dao.CasesDao; +import com.xboe.module.boecase.entity.CaseDocumentLog; +import com.xboe.module.boecase.entity.Cases; +import com.xboe.module.boecase.properties.CaseAiProperties; +import com.xboe.module.boecase.service.IAiAccessTokenService; +import com.xboe.module.boecase.service.ICaseKnowledgeService; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.io.File; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 案例-知识库Service实现类 + */ +@Slf4j +@Service +@Transactional +@EnableConfigurationProperties({CaseAiProperties.class}) +public class CaseKnowledgeServiceImpl implements ICaseKnowledgeService { + + @Autowired + private CaseAiProperties caseAiProperties; + + @Resource + private CasesDao casesDao; + + @Resource + private CaseDocumentLogDao caseDocumentLogDao; + + @Autowired + private IAiAccessTokenService aiAccessTokenService; + + private static final String ACCESS_TOKEN_CACHE_KEY = "case:ai:access_token"; + + @Override + public boolean uploadCaseDocument(String caseId) { + if (StringUtil.isBlank(caseId)) { + log.error("上传案例文档失败,案例ID不能为空"); + return false; + } + + // 1. 查询案例信息 + Cases caseEntity = casesDao.findOne(FieldFilters.eq("id", caseId)); + if (caseEntity == null || caseEntity.getDeleted()) { + log.error("上传案例文档失败,未找到有效的案例记录,caseId: {}", caseId); + return false; + } + + // 2. 检查文件路径 + if (StringUtil.isBlank(caseEntity.getFilePath())) { + log.error("上传案例文档失败,案例文件路径为空,caseId: {}", caseId); + return false; + } + + File file = new File(caseEntity.getFilePath()); + if (!file.exists()) { + log.error("上传案例文档失败,案例文件不存在,filePath: {}", caseEntity.getFilePath()); + return false; + } + + // 3. 获取access_token + String accessToken = aiAccessTokenService.getAccessToken(); + if (StringUtil.isBlank(accessToken)) { + log.error("上传案例文档失败,获取access_token失败"); + return false; + } + + // 4. 构建上传参数 + String fileName = caseEntity.getFileName(); + if (StringUtil.isBlank(fileName)) { + fileName = file.getName(); + } + + String fileType = getFileType(fileName); + String userId = getCurrentUserId(); + String uploadUrl = caseAiProperties.getBaseUrl() + "/apigateway/knowledge/v1/file/upload"; + + // 5. 重试逻辑:最多3次机会 + int maxRetries = 3; + for (int attempt = 1; attempt <= maxRetries; attempt++) { + log.info("上传案例文档第{}次尝试,caseId: {}", attempt, caseId); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpPost httpPost = new HttpPost(uploadUrl); + httpPost.setHeader("X-AI-ApiCode", caseAiProperties.getAiApiCode()); + httpPost.setHeader("access_token", accessToken); + + // 构建multipart/form-data请求体 + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.addBinaryBody("file", file); + builder.addTextBody("userId", userId, ContentType.TEXT_PLAIN); + builder.addTextBody("kId", caseAiProperties.getCaseKnowledgeId(), ContentType.TEXT_PLAIN); + builder.addTextBody("fileName", fileName, ContentType.TEXT_PLAIN); + builder.addTextBody("fileType", fileType, ContentType.TEXT_PLAIN); + builder.addTextBody("parseType", "AUTO", ContentType.TEXT_PLAIN); + builder.addTextBody("callbackUrl", caseAiProperties.getFileUploadCallbackUrl(), ContentType.TEXT_PLAIN); + + HttpEntity multipart = builder.build(); + httpPost.setEntity(multipart); + + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + + if (statusCode == 200) { + JSONObject result = JSON.parseObject(responseBody); + if (result.getBooleanValue("success")) { + // 业务处理成功 + JSONObject data = result.getJSONObject("data"); + String taskId = data.getString("taskId"); + + // 保存成功的CaseDocumentLog记录 + saveCaseDocumentLog(caseId, caseEntity.getTitle(), "create", uploadUrl, + "上传案例文档", responseBody, CaseDocumentLogOptStatusEnum.SUCCESS.getCode(), CaseDocumentLogCaseStatusEnum.SUCCESS.getCode(), taskId); + + log.info("上传案例文档成功,caseId: {}, taskId: {}, 尝试次数: {}", caseId, taskId, attempt); + return true; + } else { + // 业务处理失败,不重试 + log.error("上传案例文档业务处理失败,不重试,response: {}", responseBody); + saveCaseDocumentLog(caseId, caseEntity.getTitle(), "create", uploadUrl, + "上传案例文档", responseBody, CaseDocumentLogOptStatusEnum.SUCCESS.getCode(), CaseDocumentLogCaseStatusEnum.FAILED.getCode(), null); + return false; + } + } else { + // 接口调用失败 + log.warn("上传案例文档接口调用失败,第{}次尝试,status: {}, response: {}", attempt, statusCode, responseBody); + if (attempt == maxRetries) { + // 最后一次尝试仍然失败 + saveCaseDocumentLog(caseId, caseEntity.getTitle(), "create", uploadUrl, + "上传案例文档", responseBody, CaseDocumentLogOptStatusEnum.FAILED.getCode(), CaseDocumentLogCaseStatusEnum.FAILED.getCode(), null); + return false; + } + // 继续下一次重试 + } + } + } catch (Exception e) { + // 接口调用异常 + log.warn("上传案例文档接口调用异常,第{}次尝试", attempt, e); + if (attempt == maxRetries) { + // 最后一次尝试仍然异常 + log.error("上传案例文档最终失败,已重试{}次", maxRetries); + saveCaseDocumentLog(caseId, caseEntity.getTitle(), "create", uploadUrl, + "上传案例文档", "接口调用异常: " + e.getMessage(), CaseDocumentLogOptStatusEnum.FAILED.getCode(), CaseDocumentLogCaseStatusEnum.FAILED.getCode(), null); + return false; + } + // 继续下一次重试 + } + } + + return false; + } + + @Override + public boolean deleteCaseDocument(String caseId) { + if (StringUtil.isBlank(caseId)) { + log.error("删除案例文档失败,案例ID不能为空"); + return false; + } + + // 1. 查询案例信息 + Cases caseEntity = casesDao.findOne(FieldFilters.eq("id", caseId)); + if (caseEntity == null) { + log.error("删除案例文档失败,未找到案例记录,caseId: {}", caseId); + return false; + } + + // 2. 根据案例ID查询最新一条CaseDocumentLog数据 + List logList = caseDocumentLogDao.getGenericDao() + .findList(CaseDocumentLog.class, + FieldFilters.eq("caseId", caseId), + OrderCondition.desc("sysCreateTime")); + + if (logList.isEmpty()) { + log.error("删除案例文档失败,未找到相关的日志记录,caseId: {}", caseId); + return false; + } + + CaseDocumentLog latestLog = logList.get(0); + String taskId = latestLog.getTaskId(); + if (StringUtil.isBlank(taskId)) { + log.error("删除案例文档失败,日志记录中taskId为空,caseId: {}", caseId); + return false; + } + + // 3. 获取access_token + String accessToken = aiAccessTokenService.getAccessToken(); + if (StringUtil.isBlank(accessToken)) { + log.error("删除案例文档失败,获取access_token失败"); + return false; + } + + String deleteUrl = caseAiProperties.getBaseUrl() + "/apigateway/knowledge/v1/file/delete"; + + // 4. 重试逻辑:最多3次机会 + int maxRetries = 3; + for (int attempt = 1; attempt <= maxRetries; attempt++) { + log.info("删除案例文档第{}次尝试,caseId: {}, taskId: {}", attempt, caseId, taskId); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpDelete httpDelete = new HttpDelete(deleteUrl); + httpDelete.setHeader("X-AI-ApiCode", caseAiProperties.getAiApiCode()); + httpDelete.setHeader("access_token", accessToken); + httpDelete.setHeader("Content-Type", "application/x-www-form-urlencoded"); + + // 构建请求参数 + String params = "kId=" + URLEncoder.encode(caseAiProperties.getCaseKnowledgeId(), StandardCharsets.UTF_8.name()) + + "&taskIds=" + URLEncoder.encode(taskId, StandardCharsets.UTF_8.name()); + + StringEntity entity = new StringEntity(params, StandardCharsets.UTF_8); + entity.setContentType("application/x-www-form-urlencoded"); + httpDelete.setEntity(entity); + + try (CloseableHttpResponse response = httpClient.execute(httpDelete)) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + + if (statusCode == 200) { + JSONObject result = JSON.parseObject(responseBody); + if (result.getBooleanValue("success")) { + // 接口调用成功,检查业务处理结果 + JSONObject data = result.getJSONObject("data"); + Boolean deleteSuccess = data.getBoolean(taskId); + + int optStatus = CaseDocumentLogOptStatusEnum.SUCCESS.getCode(); + int caseStatus = (deleteSuccess != null && deleteSuccess) ? + CaseDocumentLogCaseStatusEnum.SUCCESS.getCode() : CaseDocumentLogCaseStatusEnum.FAILED.getCode(); + + saveCaseDocumentLog(caseId, caseEntity.getTitle(), "delete", deleteUrl, + "删除案例文档", responseBody, optStatus, caseStatus, null); + + if (deleteSuccess != null && deleteSuccess) { + log.info("删除案例文档成功,caseId: {}, taskId: {}, 尝试次数: {}", caseId, taskId, attempt); + return true; + } else { + // 业务处理失败,不重试 + log.error("删除案例文档业务处理失败,不重试,caseId: {}, taskId: {}", caseId, taskId); + return false; + } + } else { + // 业务处理失败,不重试 + log.error("删除案例文档业务处理失败,不重试,response: {}", responseBody); + saveCaseDocumentLog(caseId, caseEntity.getTitle(), "delete", deleteUrl, + "删除案例文档", responseBody, CaseDocumentLogOptStatusEnum.SUCCESS.getCode(), CaseDocumentLogCaseStatusEnum.FAILED.getCode(), null); + return false; + } + } else { + // 接口调用失败 + log.warn("删除案例文档接口调用失败,第{}次尝试,status: {}, response: {}", attempt, statusCode, responseBody); + if (attempt == maxRetries) { + // 最后一次尝试仍然失败 + saveCaseDocumentLog(caseId, caseEntity.getTitle(), "delete", deleteUrl, + "删除案例文档", responseBody, CaseDocumentLogOptStatusEnum.FAILED.getCode(), CaseDocumentLogCaseStatusEnum.FAILED.getCode(), null); + return false; + } + // 继续下一次重试 + } + } + } catch (Exception e) { + // 接口调用异常 + log.warn("删除案例文档接口调用异常,第{}次尝试", attempt, e); + if (attempt == maxRetries) { + // 最后一次尝试仍然异常 + log.error("删除案例文档最终失败,已重试{}次", maxRetries); + saveCaseDocumentLog(caseId, caseEntity.getTitle(), "delete", deleteUrl, + "删除案例文档", "接口调用异常: " + e.getMessage(), CaseDocumentLogOptStatusEnum.FAILED.getCode(), CaseDocumentLogCaseStatusEnum.FAILED.getCode(), null); + return false; + } + // 继续下一次重试 + } + } + + return false; + } + + @Override + public boolean updateCaseDocument(String caseId) { + if (StringUtil.isBlank(caseId)) { + log.error("更新案例文档失败,案例ID不能为空"); + return false; + } + + // 1. 查询案例信息 + Cases caseEntity = casesDao.findOne(FieldFilters.eq("id", caseId)); + if (caseEntity == null) { + log.error("更新案例文档失败,未找到案例记录,caseId: {}", caseId); + return false; + } + + log.info("开始更新案例文档,caseId: {}", caseId); + + // 获取access_token + String accessToken = aiAccessTokenService.getAccessToken(); + if (StringUtil.isBlank(accessToken)) { + log.error("更新案例文档失败,获取access_token失败"); + saveCaseDocumentLog(caseId, caseEntity.getTitle(), "update", "", + "更新案例文档", "获取access_token失败", CaseDocumentLogOptStatusEnum.FAILED.getCode(), CaseDocumentLogCaseStatusEnum.FAILED.getCode(), null); + return false; + } + + try { + // 2. 先调用删除接口 + boolean deleteSuccess = callDeleteInterface(caseId, caseEntity, accessToken); + if (!deleteSuccess) { + log.error("更新案例文档失败,删除接口调用失败,不继续执行上传操作,caseId: {}", caseId); + saveCaseDocumentLog(caseId, caseEntity.getTitle(), "update", "", + "更新案例文档", "删除接口:失败,上传接口:未执行", CaseDocumentLogOptStatusEnum.FAILED.getCode(), CaseDocumentLogCaseStatusEnum.FAILED.getCode(), null); + return false; + } + + // 3. 删除成功后,再调用上传接口 + String taskId = callUploadInterface(caseId, caseEntity, accessToken); + boolean uploadSuccess = StringUtil.isNotBlank(taskId); + + // 4. 根据结果保存一条CaseDocumentLog数据 + int optStatus = uploadSuccess ? CaseDocumentLogOptStatusEnum.SUCCESS.getCode() : CaseDocumentLogOptStatusEnum.FAILED.getCode(); + int caseStatus = uploadSuccess ? CaseDocumentLogCaseStatusEnum.SUCCESS.getCode() : CaseDocumentLogCaseStatusEnum.FAILED.getCode(); + String result = String.format("删除接口:成功,上传接口:%s", + uploadSuccess ? "成功" : "失败"); + + saveCaseDocumentLog(caseId, caseEntity.getTitle(), "update", "", + "更新案例文档", result, optStatus, caseStatus, taskId); + + if (uploadSuccess) { + log.info("更新案例文档成功,caseId: {}, taskId: {}", caseId, taskId); + } else { + log.error("更新案例文档失败,上传接口调用失败,caseId: {}", caseId); + } + + return uploadSuccess; + + } catch (Exception e) { + log.error("更新案例文档异常", e); + saveCaseDocumentLog(caseId, caseEntity.getTitle(), "update", "", + "更新案例文档", "更新异常: " + e.getMessage(), CaseDocumentLogOptStatusEnum.FAILED.getCode(), CaseDocumentLogCaseStatusEnum.FAILED.getCode(), null); + return false; + } + } + + @Override + public boolean handleUploadCallback(String taskId, String message, String fileStatus) { + if (StringUtil.isBlank(taskId)) { + log.error("处理上传回调失败,taskId不能为空"); + return false; + } + + if (StringUtil.isBlank(fileStatus)) { + log.error("处理上传回调失败,fileStatus不能为空,taskId: {}", taskId); + return false; + } + + try { + // 1. 根据taskId查询最新一条CaseDocumentLog数据 + List logList = caseDocumentLogDao.getGenericDao() + .findList(CaseDocumentLog.class, + FieldFilters.eq("taskId", taskId), + OrderCondition.desc("sysCreateTime")); + + if (logList.isEmpty()) { + log.error("处理上传回调失败,未找到对应的日志记录,taskId: {}", taskId); + return false; + } + + CaseDocumentLog latestLog = logList.get(0); + log.info("找到对应的日志记录,logId: {}, taskId: {}, 原状态 - optStatus: {}, caseStatus: {}", + latestLog.getId(), taskId, latestLog.getOptStatus(), latestLog.getCaseStatus()); + + // 2. 根据fileStatus更新状态 + int newOptStatus; + int newCaseStatus; + + if ("vectored".equals(fileStatus)) { + // 文档上传成功(向量化成功) + newOptStatus = CaseDocumentLogOptStatusEnum.SUCCESS.getCode(); + newCaseStatus = CaseDocumentLogCaseStatusEnum.SUCCESS.getCode(); + log.info("文档上传成功(向量化成功),taskId: {}", taskId); + } else if ("failed".equals(fileStatus)) { + // 业务处理失败 + newOptStatus = CaseDocumentLogOptStatusEnum.SUCCESS.getCode(); // 接口调用成功 + newCaseStatus = CaseDocumentLogCaseStatusEnum.FAILED.getCode(); // 业务处理失败 + log.warn("文档上传业务处理失败,taskId: {}, message: {}", taskId, message); + } else { + log.error("未知的fileStatus: {}, taskId: {}", fileStatus, taskId); + return false; + } + + // 3. 更新日志记录状态 + latestLog.setOptStatus(newOptStatus); + latestLog.setCaseStatus(newCaseStatus); + latestLog.setResponseBody(StringUtil.isNotBlank(message) ? message : latestLog.getResponseBody()); + latestLog.setSysUpdateTime(LocalDateTime.now()); + + caseDocumentLogDao.save(latestLog); + + log.info("更新日志记录成功,logId: {}, taskId: {}, 新状态 - optStatus: {}, caseStatus: {}", + latestLog.getId(), taskId, newOptStatus, newCaseStatus); + + return true; + + } catch (Exception e) { + log.error("处理上传回调异常,taskId: {}", taskId, e); + return false; + } + } + + /** + * 根据文件名获取文件类型 + */ + private String getFileType(String fileName) { + if (StringUtil.isBlank(fileName)) { + return "txt"; + } + + String extension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase(); + switch (extension) { + case "doc": + case "docx": + return "word"; + case "pdf": + return "pdf"; + case "md": + return "md"; + case "txt": + return "txt"; + case "xls": + case "xlsx": + return "excel"; + case "xml": + return "xml"; + case "ppt": + case "pptx": + return "ppt"; + default: + return "txt"; + } + } + + /** + * 获取当前用户ID + */ + private String getCurrentUserId() { + // TODO: 实现获取当前登录用户ID的逻辑 + // 这里需要根据实际的用户认证系统来实现 + return "defaultUserId"; + } + + /** + * 调用删除接口(仅调用接口,不记录日志) + */ + private boolean callDeleteInterface(String caseId, Cases caseEntity, String accessToken) { + // 1. 根据案例ID查询最新一条CaseDocumentLog数据 + List logList = caseDocumentLogDao.getGenericDao() + .findList(CaseDocumentLog.class, + FieldFilters.eq("caseId", caseId), + OrderCondition.desc("sysCreateTime")); + + if (logList.isEmpty()) { + log.warn("调用删除接口时未找到相关的日志记录,caseId: {}", caseId); + return false; + } + + CaseDocumentLog latestLog = logList.get(0); + String taskId = latestLog.getTaskId(); + if (StringUtil.isBlank(taskId)) { + log.warn("调用删除接口时日志记录中taskId为空,caseId: {}", caseId); + return false; + } + + String deleteUrl = caseAiProperties.getBaseUrl() + "/apigateway/knowledge/v1/file/delete"; + + // 2. 重试逻辑:最多3次机会 + int maxRetries = 3; + for (int attempt = 1; attempt <= maxRetries; attempt++) { + log.info("调用删除接口第{}次尝试,caseId: {}, taskId: {}", attempt, caseId, taskId); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpDelete httpDelete = new HttpDelete(deleteUrl); + httpDelete.setHeader("X-AI-ApiCode", caseAiProperties.getAiApiCode()); + httpDelete.setHeader("access_token", accessToken); + httpDelete.setHeader("Content-Type", "application/x-www-form-urlencoded"); + + // 构建请求参数 + String params = "kId=" + URLEncoder.encode(caseAiProperties.getCaseKnowledgeId(), StandardCharsets.UTF_8.name()) + + "&taskIds=" + URLEncoder.encode(taskId, StandardCharsets.UTF_8.name()); + + StringEntity entity = new StringEntity(params, StandardCharsets.UTF_8); + entity.setContentType("application/x-www-form-urlencoded"); + httpDelete.setEntity(entity); + + try (CloseableHttpResponse response = httpClient.execute(httpDelete)) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + + if (statusCode == 200) { + JSONObject result = JSON.parseObject(responseBody); + if (result.getBooleanValue("success")) { + JSONObject data = result.getJSONObject("data"); + Boolean deleteSuccess = data.getBoolean(taskId); + + if (deleteSuccess != null && deleteSuccess) { + log.info("调用删除接口成功,caseId: {}, taskId: {}, 尝试次数: {}", caseId, taskId, attempt); + return true; + } else { + // 业务处理失败,不重试 + log.warn("调用删除接口业务处理失败,不重试,caseId: {}, taskId: {}", caseId, taskId); + return false; + } + } else { + // 业务处理失败,不重试 + log.warn("调用删除接口业务处理失败,不重试,response: {}", responseBody); + return false; + } + } else { + // 接口调用失败 + log.warn("调用删除接口失败,第{}次尝试,status: {}, response: {}", attempt, statusCode, responseBody); + if (attempt == maxRetries) { + log.warn("调用删除接口最终失败,已重试{}次", maxRetries); + return false; + } + // 继续下一次重试 + } + } + } catch (Exception e) { + // 接口调用异常 + log.warn("调用删除接口异常,第{}次尝试", attempt, e); + if (attempt == maxRetries) { + log.warn("调用删除接口最终失败,已重试{}次", maxRetries); + return false; + } + // 继续下一次重试 + } + } + + return false; + } + + /** + * 调用上传接口(仅调用接口,不记录日志) + */ + private String callUploadInterface(String caseId, Cases caseEntity, String accessToken) { + // 1. 检查文件路径 + if (StringUtil.isBlank(caseEntity.getFilePath())) { + log.warn("调用上传接口失败,案例文件路径为空,caseId: {}", caseId); + return null; + } + + File file = new File(caseEntity.getFilePath()); + if (!file.exists()) { + log.warn("调用上传接口失败,案例文件不存在,filePath: {}", caseEntity.getFilePath()); + return null; + } + + // 2. 构建上传参数 + String fileName = caseEntity.getFileName(); + if (StringUtil.isBlank(fileName)) { + fileName = file.getName(); + } + + String fileType = getFileType(fileName); + String userId = getCurrentUserId(); + String uploadUrl = caseAiProperties.getBaseUrl() + "/apigateway/knowledge/v1/file/upload"; + + // 3. 重试逻辑:最多3次机会 + int maxRetries = 3; + for (int attempt = 1; attempt <= maxRetries; attempt++) { + log.info("调用上传接口第{}次尝试,caseId: {}", attempt, caseId); + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpPost httpPost = new HttpPost(uploadUrl); + httpPost.setHeader("X-AI-ApiCode", caseAiProperties.getAiApiCode()); + httpPost.setHeader("access_token", accessToken); + + // 构建multipart/form-data请求体 + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.addBinaryBody("file", file); + builder.addTextBody("userId", userId, ContentType.TEXT_PLAIN); + builder.addTextBody("kId", caseAiProperties.getCaseKnowledgeId(), ContentType.TEXT_PLAIN); + builder.addTextBody("fileName", fileName, ContentType.TEXT_PLAIN); + builder.addTextBody("fileType", fileType, ContentType.TEXT_PLAIN); + builder.addTextBody("parseType", "AUTO", ContentType.TEXT_PLAIN); + builder.addTextBody("callbackUrl", caseAiProperties.getFileUploadCallbackUrl(), ContentType.TEXT_PLAIN); + + HttpEntity multipart = builder.build(); + httpPost.setEntity(multipart); + + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + int statusCode = response.getStatusLine().getStatusCode(); + String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + + if (statusCode == 200) { + JSONObject result = JSON.parseObject(responseBody); + if (result.getBooleanValue("success")) { + JSONObject data = result.getJSONObject("data"); + String taskId = data.getString("taskId"); + + log.info("调用上传接口成功,caseId: {}, taskId: {}, 尝试次数: {}", caseId, taskId, attempt); + return taskId; + } else { + // 业务处理失败,不重试 + log.warn("调用上传接口业务处理失败,不重试,response: {}", responseBody); + return null; + } + } else { + // 接口调用失败 + log.warn("调用上传接口失败,第{}次尝试,status: {}, response: {}", attempt, statusCode, responseBody); + if (attempt == maxRetries) { + log.warn("调用上传接口最终失败,已重试{}次", maxRetries); + return null; + } + // 继续下一次重试 + } + } + } catch (Exception e) { + // 接口调用异常 + log.warn("调用上传接口异常,第{}次尝试", attempt, e); + if (attempt == maxRetries) { + log.warn("调用上传接口最终失败,已重试{}次", maxRetries); + return null; + } + // 继续下一次重试 + } + } + + return null; + } + + /** + * 保存CaseDocumentLog记录 + */ + private void saveCaseDocumentLog(String caseId, String caseTitle, String optType, + String requestUrl, String requestBody, String responseBody, + Integer optStatus, Integer caseStatus, String taskId) { + try { + CaseDocumentLog caseDocumentLog = new CaseDocumentLog(); + caseDocumentLog.setId(IDGenerator.generate()); + caseDocumentLog.setTaskId(taskId); + caseDocumentLog.setCaseId(caseId); + caseDocumentLog.setCaseTitle(caseTitle); + caseDocumentLog.setOptType(optType); + caseDocumentLog.setRequestUrl(requestUrl); + caseDocumentLog.setRequestBody(requestBody); + caseDocumentLog.setResponseBody(responseBody); + caseDocumentLog.setOptTime(LocalDateTime.now()); + caseDocumentLog.setOptStatus(optStatus); + caseDocumentLog.setCaseStatus(caseStatus); + caseDocumentLog.setExecuteDuration(0L); + + caseDocumentLogDao.save(caseDocumentLog); + log.info("保存CaseDocumentLog成功,logId: {}", caseDocumentLog.getId()); + } catch (Exception e) { + log.error("保存CaseDocumentLog失败", e); + } + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiConversationVo.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiConversationVo.java new file mode 100644 index 00000000..d99dacf5 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiConversationVo.java @@ -0,0 +1,10 @@ +package com.xboe.module.boecase.vo; + +import lombok.Data; + +/** + * AI会话记录 + */ +@Data +public class CaseAiConversationVo { +} 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 new file mode 100644 index 00000000..dea1d7c4 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiMessageVo.java @@ -0,0 +1,32 @@ +package com.xboe.module.boecase.vo; + +import lombok.Data; + +import java.util.List; + +/** + * AI会话消息记录VO + */ +@Data +public class CaseAiMessageVo { + + /** + * 用户提问内容 + */ + private String query; + + /** + * AI回答内容 + */ + private String answer; + + /** + * 案例引用列表 + */ + private List caseRefer; + + /** + * 建议列表 + */ + private List suggestions; +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseDocumentLogVo.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseDocumentLogVo.java new file mode 100644 index 00000000..c448d81b --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseDocumentLogVo.java @@ -0,0 +1,76 @@ +package com.xboe.module.boecase.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * AI调用日志列表展示VO + */ +@Data +public class CaseDocumentLogVo { + + /** + * 日志ID + */ + private String id; + + /** + * 案例标题 + */ + private String caseTitle; + + /** + * 操作类型 + */ + private String optType; + + /** + * 调用接口名称 + */ + private String requestUrl; + + /** + * 调用时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime optTime; + + /** + * 请求参数 + */ + private String requestBody; + + /** + * 响应参数 + */ + private String responseBody; + + /** + * 接口调用结果 + * 0-调用中, 1-调用成功, 2-调用失败 + */ + private Integer optStatus; + + /** + * 接口调用结果描述 + */ + private String optStatusText; + + /** + * 业务处理结果 + * 1-处理成功, 2-处理失败 + */ + private Integer caseStatus; + + /** + * 业务处理结果描述 + */ + private String caseStatusText; + + /** + * 执行时间(ms) + */ + private Long executeDuration; +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseReferVo.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseReferVo.java new file mode 100644 index 00000000..5961f596 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseReferVo.java @@ -0,0 +1,37 @@ +package com.xboe.module.boecase.vo; + +import lombok.Data; + +import java.util.List; + +/** + * 案例引用信息VO + */ +@Data +public class CaseReferVo { + + /** + * 案例ID + */ + private String caseId; + + /** + * 案例标题 + */ + private String title; + + /** + * 作者姓名 + */ + private String authorName; + + /** + * 关键词列表 + */ + private List keywords; + + /** + * 案例内容 + */ + private String content; +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/resources/application-dev.yml b/servers/boe-server-all/src/main/resources/application-dev.yml index ce14272e..c175e2f3 100644 --- a/servers/boe-server-all/src/main/resources/application-dev.yml +++ b/servers/boe-server-all/src/main/resources/application-dev.yml @@ -79,6 +79,14 @@ xboe: image: course: default: http://192.168.0.253/pc/images/bgimg/course.png + case: + ai: + base-url: http://10.10.181.114:30003 + app-key: 6e9be45319184ac793aa127c362b0f0b + secret-key: db4d24279e3d6dbf1524af42cd0bedd2 + ai-api-code: 30800 + case-knowledge-id: de2e006e-82fb-4ace-8813-f25c316be4ff + file-upload-callback-url: http://192.168.0.253:9090/xboe/m/boe/caseDocumentLog/uploadCallback xxl: job: accessToken: 65ddc683-22f5-83b4-de3a-3c97a0a29af0 diff --git a/servers/boe-server-all/src/main/resources/application-pre.yml b/servers/boe-server-all/src/main/resources/application-pre.yml index f8fa90ce..629f3db0 100644 --- a/servers/boe-server-all/src/main/resources/application-pre.yml +++ b/servers/boe-server-all/src/main/resources/application-pre.yml @@ -111,6 +111,14 @@ xboe: image: course: default: http://10.251.132.75/pc/images/bgimg/course.png + case: + ai: + base-url: http://10.10.181.114:30003 + app-key: 6e9be45319184ac793aa127c362b0f0b + secret-key: db4d24279e3d6dbf1524af42cd0bedd2 + ai-api-code: 30800 + case-knowledge-id: de2e006e-82fb-4ace-8813-f25c316be4ff + file-upload-callback-url: http://192.168.0.253:9090/xboe/m/boe/caseDocumentLog/uploadCallback jasypt: encryptor: algorithm: PBEWithMD5AndDES diff --git a/servers/boe-server-all/src/main/resources/application-prod.yml b/servers/boe-server-all/src/main/resources/application-prod.yml index 8d6a8177..61261a3a 100644 --- a/servers/boe-server-all/src/main/resources/application-prod.yml +++ b/servers/boe-server-all/src/main/resources/application-prod.yml @@ -77,6 +77,14 @@ xboe: image: course: default: https://u.boe.com/pc/images/bgimg/course.png + case: + ai: + base-url: http://10.10.181.114:30003 + app-key: 6e9be45319184ac793aa127c362b0f0b + secret-key: db4d24279e3d6dbf1524af42cd0bedd2 + ai-api-code: 30800 + case-knowledge-id: de2e006e-82fb-4ace-8813-f25c316be4ff + file-upload-callback-url: http://192.168.0.253:9090/xboe/m/boe/caseDocumentLog/uploadCallback xxl: job: accessToken: 65ddc683-22f5-83b4-de3a-3c97a0a29af0 diff --git a/servers/boe-server-all/src/main/resources/application-test.yml b/servers/boe-server-all/src/main/resources/application-test.yml index 7872413a..4b4d430c 100644 --- a/servers/boe-server-all/src/main/resources/application-test.yml +++ b/servers/boe-server-all/src/main/resources/application-test.yml @@ -111,6 +111,14 @@ xboe: image: course: default: https://u-pre.boe.com/pc/images/bgimg/course.png + case: + ai: + base-url: http://10.10.181.114:30003 + app-key: 6e9be45319184ac793aa127c362b0f0b + secret-key: db4d24279e3d6dbf1524af42cd0bedd2 + ai-api-code: 30800 + case-knowledge-id: de2e006e-82fb-4ace-8813-f25c316be4ff + file-upload-callback-url: http://192.168.0.253:9090/xboe/m/boe/caseDocumentLog/uploadCallback jasypt: encryptor: algorithm: PBEWithMD5AndDES