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