From 748ec8c07203448a5814ef0590f29794b8aceb3e Mon Sep 17 00:00:00 2001 From: "liu.zixi" Date: Mon, 3 Nov 2025 13:49:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A1=88=E4=BE=8B=E4=B8=93=E5=AE=B6=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E5=A2=9E=E5=8A=A0=E6=97=B6=E9=95=BF=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E4=BB=BB=E5=8A=A1;=20=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E6=96=87=E6=A1=A3=E6=97=B6=E5=A2=9E=E5=8A=A0metadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/boecase/api/CaseAiChatApi.java | 59 ++++++- .../entity/AiChatConversationData.java | 25 ++- .../boecase/properties/CaseAiProperties.java | 10 ++ .../boecase/service/ICaseAiChatService.java | 10 +- .../service/IElasticSearchIndexService.java | 3 - .../service/impl/CaseAiChatServiceImpl.java | 130 ++++++++++++++- .../impl/CaseKnowledgeServiceImpl.java | 150 ++++++++++++++++++ .../impl/ElasticSearchIndexServiceImpl.java | 10 +- .../boecase/task/CaseAiChatDataTask.java | 38 +++++ .../module/boecase/vo/CaseAiMessageVo.java | 11 ++ .../boecase/vo/ConversationExcelVo.java | 31 ++++ .../src/main/resources/application-prod.yml | 3 +- .../src/main/resources/application-test.yml | 1 + 13 files changed, 455 insertions(+), 26 deletions(-) create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/task/CaseAiChatDataTask.java create mode 100644 servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/ConversationExcelVo.java diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java index 8b436724..64706599 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/api/CaseAiChatApi.java @@ -8,6 +8,8 @@ import com.xboe.module.boecase.service.ICaseAiChatService; import com.xboe.module.boecase.service.ICaseAiPermissionService; import com.xboe.module.boecase.service.IElasticSearchIndexService; import com.xboe.module.boecase.vo.CaseAiMessageVo; +import com.xboe.module.excel.ExportsExcelSenderUtil; +import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; @@ -15,7 +17,12 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; /** @@ -73,6 +80,44 @@ public class CaseAiChatApi extends ApiBaseController { } } + /** + * 导出会话记录为Excel + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param response HTTP响应 + */ + @GetMapping("/export-conversations") + public void downloadConversationExcel(@RequestParam String startTime, + @RequestParam String endTime, + HttpServletResponse response) { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + LocalDateTime start = LocalDateTime.parse(startTime, formatter); + LocalDateTime end = LocalDateTime.parse(endTime, formatter); + + // TODO: 这里需要修改为实际返回数据的方法 + caseAiChatService.downloadConversationExcel(start, end); + + response.setContentType("application/vnd.ms-excel"); + response.setHeader("Content-Disposition", "attachment; filename=conversations.xls"); + + // 示例数据,实际应该从Service获取 + LinkedHashMap headers = new LinkedHashMap<>(); + headers.put("会话ID", "conversationId"); + headers.put("会话名称", "conversationName"); + headers.put("用户", "user"); + headers.put("开始时间", "startTime"); + headers.put("会话时长", "duration"); + + List dataList = new ArrayList<>(); + // 这里应该填充实际数据 + + ExportsExcelSenderUtil.export(headers, dataList, response.getOutputStream(), "yyyy-MM-dd HH:mm:ss"); + } catch (Exception e) { + log.error("导出会话记录为Excel异常", e); + } + } + /** * 判断当前登录用户是否显示"案例专家"功能入口 * @return 是否显示功能入口 @@ -123,4 +168,16 @@ public class CaseAiChatApi extends ApiBaseController { } return error("创建失败"); } -} + + /** + * 用于Excel导出的VO类 + */ + @Data + static class ConversationExcelVo { + private String conversationId; + private String conversationName; + private String user; + private String startTime; + private String duration; + } +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/AiChatConversationData.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/AiChatConversationData.java index 61d0a97c..d62b056b 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/AiChatConversationData.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/entity/AiChatConversationData.java @@ -54,28 +54,27 @@ public class AiChatConversationData { */ private String userName; + /** + * 消息时间戳 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime startTime; + /** * 消息时间戳 */ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime timestamp; + /** + * 聊天时长(秒) + */ + private Integer durationSeconds; + // ================== 构造函数 ================== public AiChatConversationData() { - this.timestamp = LocalDateTime.now(); - } - - public AiChatConversationData(String conversationId, String query, String answer, - List caseRefers, List suggestions, - String userId) { - this.conversationId = conversationId; - this.query = query; - this.answer = new StringBuilder(answer != null ? answer : ""); - this.caseRefers = caseRefers != null ? caseRefers : new ArrayList<>(); - this.suggestions = suggestions != null ? suggestions : new ArrayList<>(); - this.userId = userId; - this.timestamp = LocalDateTime.now(); + this.startTime = LocalDateTime.now(); } // ================== 便捷方法 ================== 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 index 9f390443..4802f813 100644 --- 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 @@ -72,4 +72,14 @@ public class CaseAiProperties { * AI处理失败告警邮件收件人列表 */ private List alertEmailRecipients; + + /** + * 是否发送AI对话记录到邮箱 + */ + private boolean aiChatDataSendEmail; + + /** + * AI对话记录保存根路径 + */ + private String aiChatRootPath; } \ No newline at end of file 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 index 2ccd6e3d..6f840458 100644 --- 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 @@ -6,6 +6,7 @@ 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.time.LocalDateTime; import java.util.List; /** @@ -35,4 +36,11 @@ public interface ICaseAiChatService { * @return 消息记录列表 */ List getConversationMessages(String conversationId); -} + + /** + * 导出会话记录为Excel + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + void downloadConversationExcel(LocalDateTime startTime, LocalDateTime endTime); +} \ No newline at end of file diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/IElasticSearchIndexService.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/IElasticSearchIndexService.java index 1f389eaa..ea1d4207 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/IElasticSearchIndexService.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/IElasticSearchIndexService.java @@ -12,20 +12,17 @@ public interface IElasticSearchIndexService { /** * 查看索引是否存在 - * @param indexName * @return */ boolean checkIndexExists(); /** * 创建索引 - * @param indexName */ boolean createIndex(); /** * 删除索引 - * @param indexName * @return */ boolean deleteIndex(); diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseAiChatServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseAiChatServiceImpl.java index f302d71d..ef24d226 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseAiChatServiceImpl.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/CaseAiChatServiceImpl.java @@ -3,6 +3,7 @@ package com.xboe.module.boecase.service.impl; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.xboe.core.CurrentUser; +import com.xboe.core.orm.FieldFilters; import com.xboe.enums.CaseAiChatStatusEnum; import com.xboe.module.boecase.dao.CaseAiConversationsDao; import com.xboe.module.boecase.dao.CaseDocumentLogDao; @@ -18,8 +19,10 @@ import com.xboe.module.boecase.service.IElasticSearchIndexService; import com.xboe.module.boecase.vo.CaseAiMessageVo; import com.xboe.module.boecase.vo.CaseReferVo; import com.xboe.module.boecase.entity.AiChatConversationData; +import com.xboe.module.boecase.vo.ConversationExcelVo; import com.xboe.system.organization.vo.OrgSimpleVo; import com.xboe.system.user.service.IUserService; +import lombok.Data; import lombok.extern.slf4j.Slf4j; import okhttp3.*; import okhttp3.sse.EventSource; @@ -32,6 +35,11 @@ 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.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchRequest; @@ -53,8 +61,13 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import java.io.File; import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; @@ -314,7 +327,117 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { } return elasticSearchIndexService.queryData(conversationId); } - + + @Override + public void downloadConversationExcel(LocalDateTime startTime, LocalDateTime endTime) { + // 1. 根据startTime和endTime,查询在这个时间区间内的CaseAiConversations数据 + List conversations = caseAiConversationsDao.getGenericDao().findList( + CaseAiConversations.class, + FieldFilters.ge("sysCreateTime", startTime), + FieldFilters.le("sysCreateTime", endTime) + ); + + // 准备Excel数据 + List excelDataList = new ArrayList<>(); + + // 2. 遍历这组数据,根据aiConversationId从es中查询数据(可调用getConversationMessages()方法) + for (CaseAiConversations conversation : conversations) { + String aiConversationId = conversation.getAiConversationId(); + String conversationName = conversation.getConversationName(); + String conversationUser = conversation.getConversationUser(); + + List messages = getConversationMessages(aiConversationId); + + // 计算会话时长 + long duration = 0; // 默认为0,如果需要精确计算,需要从消息中提取时间信息 + + // 3. 写入Excel,包括每个会话的用户,会话标题,会话内的问答记录,每次对话时长等 + ConversationExcelVo excelData = new ConversationExcelVo(); + excelData.setConversationId(aiConversationId); + excelData.setConversationName(conversationName); + excelData.setUser(conversationUser); + excelData.setMessages(messages); + + excelDataList.add(excelData); + } + + // 写入Excel文件 + Workbook workbook = new XSSFWorkbook(); + Sheet sheet = workbook.createSheet("AI会话数据"); + // 标题行 + Row headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue("会话ID"); + headerRow.createCell(1).setCellValue("会话名称"); + headerRow.createCell(2).setCellValue("用户"); + headerRow.createCell(3).setCellValue("提问"); + headerRow.createCell(4).setCellValue("回答"); + headerRow.createCell(5).setCellValue("开始时间"); + headerRow.createCell(6).setCellValue("问答时长(秒)"); + + // 内容行 + if (!excelDataList.isEmpty()) { + int rowNum = 1; // 从第二行开始写入数据 + for (ConversationExcelVo excelData : excelDataList) { + List messages = excelData.getMessages(); + + if (messages != null && !messages.isEmpty()) { + // 记录起始行号,用于后续合并单元格 + int startRow = rowNum; + + // 遍历每个消息 + for (CaseAiMessageVo message : messages) { + Row row = sheet.createRow(rowNum++); + // 填充每行数据 + row.createCell(0).setCellValue(excelData.getConversationId()); + row.createCell(1).setCellValue(excelData.getConversationName()); + row.createCell(2).setCellValue(excelData.getUser()); + row.createCell(3).setCellValue(message.getQuery() != null ? message.getQuery() : ""); + row.createCell(4).setCellValue(message.getAnswer() != null ? message.getAnswer() : ""); + row.createCell(5).setCellValue(""); // 开始时间字段暂留空 + row.createCell(6).setCellValue(message.getDurationSeconds() != null ? message.getDurationSeconds() : 0); + } + + // 合并单元格(会话ID、会话名称、用户三列) + // 参数说明:起始行号,结束行号,起始列号,结束列号 + if (rowNum > startRow + 1) { // 只有当有多行时才合并 + sheet.addMergedRegion(new CellRangeAddress(startRow, rowNum - 1, 0, 0)); + sheet.addMergedRegion(new CellRangeAddress(startRow, rowNum - 1, 1, 1)); + sheet.addMergedRegion(new CellRangeAddress(startRow, rowNum - 1, 2, 2)); + } + } else { + // 如果没有消息,则仍然创建一行显示基本信息 + Row row = sheet.createRow(rowNum++); + row.createCell(0).setCellValue(excelData.getConversationId()); + row.createCell(1).setCellValue(excelData.getConversationName()); + row.createCell(2).setCellValue(excelData.getUser()); + } + } + } + // 3. 创建Excel文件并保存 + if (caseAiProperties.isAiChatDataSendEmail()) { + // TODO 发送邮件附件 + } else { + // 保存文件 + String dirPath = caseAiProperties.getAiChatRootPath() + File.separator + startTime.format(DateTimeFormatter.ofPattern("yyyyMM")); + Path dir = Paths.get(dirPath); + if (!Files.exists(dir)) { + try { + Files.createDirectories(dir); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + String fileName = "AI会话数据-" + startTime.format(DateTimeFormatter.ofPattern("yyyyMMdd")) + "-" + System.currentTimeMillis() + ".xlsx"; + Path filePath = dir.resolve(fileName); + try (OutputStream out = Files.newOutputStream(filePath)) { + workbook.write(out); + out.flush(); + } catch (IOException e) { + log.error("保存文件错误", e); + } + } + } + /** * 从 ES 数据中解析消息对象 * @param sourceMap ES数据 @@ -554,9 +677,4 @@ public class CaseAiChatServiceImpl implements ICaseAiChatService { sseEmitter.completeWithError(e); } } - - /** - * 对话数据容器 - */ - // ConversationData 已移动到独立的Entity类:AiChatConversationData } 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 index 119cf72a..b549297d 100644 --- 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 @@ -17,11 +17,15 @@ import com.xboe.enums.CaseDocumentLogRunStatusEnum; import com.xboe.module.assistance.service.ISmtpEmailService; import com.xboe.module.boecase.dao.CaseDocumentLogDao; import com.xboe.module.boecase.dao.CasesDao; +import com.xboe.module.boecase.dao.CasesMajorTypeDao; import com.xboe.module.boecase.entity.CaseDocumentLog; import com.xboe.module.boecase.entity.Cases; +import com.xboe.module.boecase.entity.CasesMajorType; import com.xboe.module.boecase.properties.CaseAiProperties; import com.xboe.module.boecase.service.IAiAccessTokenService; import com.xboe.module.boecase.service.ICaseKnowledgeService; +import com.xboe.module.dict.entity.DictItem; +import com.xboe.module.dict.service.ISysDictionaryService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntity; @@ -44,6 +48,8 @@ import java.io.File; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; /** * 案例-知识库Service实现类 @@ -63,9 +69,15 @@ public class CaseKnowledgeServiceImpl implements ICaseKnowledgeService { @Resource private CaseDocumentLogDao caseDocumentLogDao; + @Resource + private CasesMajorTypeDao casesMajorTypeDao; + @Resource private XFileUploader fileUploader; + @Autowired + private ISysDictionaryService sysDictionaryService; + @Autowired private IAiAccessTokenService aiAccessTokenService; @@ -145,6 +157,52 @@ public class CaseKnowledgeServiceImpl implements ICaseKnowledgeService { builder.addTextBody("fileType", fileType, ContentType.TEXT_PLAIN); requestBody.put("fileType", fileType); builder.addTextBody("parseType", "AUTO", ContentType.TEXT_PLAIN); + // metadata + JSONObject fileMetaData = new JSONObject(); + fileMetaData.put("标题", cases.getTitle()); + fileMetaData.put("作者", cases.getAuthorName()); + fileMetaData.put("年份", String.valueOf(cases.getSysCreateTime().getYear())); + fileMetaData.put("摘要", cases.getSummary()); + // 组织领域:orgDomainParent + String orgDomainParent = cases.getOrgDomainParent(); + List orgDomainParentItems = sysDictionaryService.findByKey("org_domain"); + Optional orgDomainParentItem = orgDomainParentItems.stream() + .filter(item -> StringUtils.equals(orgDomainParent, item.getCode())) + .findFirst(); + if (orgDomainParentItem.isPresent()) { + StringBuilder sb = new StringBuilder(); + sb.append(orgDomainParentItem.get().getName()); + if (StringUtils.isNotBlank(cases.getOrgDomainParent2())) { + Optional orgDomainParent2Item = orgDomainParentItems.stream() + .filter(item -> StringUtils.equals(cases.getOrgDomainParent2(), item.getCode())) + .findFirst(); + orgDomainParent2Item.ifPresent(dictItem -> sb.append(" - ").append(dictItem.getName())); + if (StringUtils.isNotBlank(cases.getOrgDomainParent3())) { + Optional orgDomainParent3Item = orgDomainParentItems.stream() + .filter(item -> StringUtils.equals(cases.getOrgDomainParent3(), item.getCode())) + .findFirst(); + orgDomainParent3Item.ifPresent(dictItem -> sb.append(" - ").append(dictItem.getName())); + } + } + fileMetaData.put("组织领域", sb.toString()); + } + // 分类:majorIds + List cmtList = casesMajorTypeDao.findList(FieldFilters.eq("caseId", cases.getId())); + if (cmtList != null && !cmtList.isEmpty()) { + List majorIds = cmtList.stream().map(CasesMajorType::getMajorId).collect(Collectors.toList()); + + List majorItems = sysDictionaryService.findByKey("major_type"); + if (majorItems != null && !majorItems.isEmpty()) { + List majorNames = majorItems.stream() + .filter(item -> majorIds.contains(item.getCode())) + .map(DictItem::getName) + .collect(Collectors.toList()); + fileMetaData.put("组织领域", String.join(", ", majorNames)); + } + } + + builder.addTextBody("fileMetaData", fileMetaData.toJSONString(), ContentType.TEXT_PLAIN); + requestBody.put("fileMetaData", fileMetaData); // 由于接口权限,目前采用不回调,而是通过批处理的方式,处理文件状态 if (caseAiProperties.isFileUploadUseCallback()) { builder.addTextBody("callbackUrl", caseAiProperties.getFileUploadCallbackUrl(), ContentType.TEXT_PLAIN); @@ -432,6 +490,52 @@ public class CaseKnowledgeServiceImpl implements ICaseKnowledgeService { builder.addTextBody("fileType", fileType, ContentType.TEXT_PLAIN); requestBody.put("fileType", fileType); builder.addTextBody("parseType", "AUTO", ContentType.TEXT_PLAIN); + // metadata + JSONObject fileMetaData = new JSONObject(); + fileMetaData.put("标题", cases.getTitle()); + fileMetaData.put("作者", cases.getAuthorName()); + fileMetaData.put("年份", String.valueOf(cases.getSysCreateTime().getYear())); + fileMetaData.put("摘要", cases.getSummary()); + // 组织领域:orgDomainParent + String orgDomainParent = cases.getOrgDomainParent(); + List orgDomainParentItems = sysDictionaryService.findByKey("org_domain"); + Optional orgDomainParentItem = orgDomainParentItems.stream() + .filter(item -> StringUtils.equals(orgDomainParent, item.getCode())) + .findFirst(); + if (orgDomainParentItem.isPresent()) { + StringBuilder sb = new StringBuilder(); + sb.append(orgDomainParentItem.get().getName()); + if (StringUtils.isNotBlank(cases.getOrgDomainParent2())) { + Optional orgDomainParent2Item = orgDomainParentItems.stream() + .filter(item -> StringUtils.equals(cases.getOrgDomainParent2(), item.getCode())) + .findFirst(); + orgDomainParent2Item.ifPresent(dictItem -> sb.append(" - ").append(dictItem.getName())); + if (StringUtils.isNotBlank(cases.getOrgDomainParent3())) { + Optional orgDomainParent3Item = orgDomainParentItems.stream() + .filter(item -> StringUtils.equals(cases.getOrgDomainParent3(), item.getCode())) + .findFirst(); + orgDomainParent3Item.ifPresent(dictItem -> sb.append(" - ").append(dictItem.getName())); + } + } + fileMetaData.put("组织领域", sb.toString()); + } + // 分类:majorIds + List cmtList = casesMajorTypeDao.findList(FieldFilters.eq("caseId", cases.getId())); + if (cmtList != null && !cmtList.isEmpty()) { + List majorIds = cmtList.stream().map(CasesMajorType::getMajorId).collect(Collectors.toList()); + + List majorItems = sysDictionaryService.findByKey("major_type"); + if (majorItems != null && !majorItems.isEmpty()) { + List majorNames = majorItems.stream() + .filter(item -> majorIds.contains(item.getCode())) + .map(DictItem::getName) + .collect(Collectors.toList()); + fileMetaData.put("组织领域", String.join(", ", majorNames)); + } + } + + builder.addTextBody("fileMetaData", fileMetaData.toJSONString(), ContentType.TEXT_PLAIN); + requestBody.put("fileMetaData", fileMetaData); // 由于接口权限,目前采用不回调,而是通过批处理的方式,处理文件状态 if (caseAiProperties.isFileUploadUseCallback()) { builder.addTextBody("callbackUrl", caseAiProperties.getFileUploadCallbackUrl(), ContentType.TEXT_PLAIN); @@ -696,6 +800,52 @@ public class CaseKnowledgeServiceImpl implements ICaseKnowledgeService { builder.addTextBody("fileType", fileType, ContentType.TEXT_PLAIN); requestBody.put("fileType", fileType); builder.addTextBody("parseType", "AUTO", ContentType.TEXT_PLAIN); + // metadata + JSONObject fileMetaData = new JSONObject(); + fileMetaData.put("标题", cases.getTitle()); + fileMetaData.put("作者", cases.getAuthorName()); + fileMetaData.put("年份", String.valueOf(cases.getSysCreateTime().getYear())); + fileMetaData.put("摘要", cases.getSummary()); + // 组织领域:orgDomainParent + String orgDomainParent = cases.getOrgDomainParent(); + List orgDomainParentItems = sysDictionaryService.findByKey("org_domain"); + Optional orgDomainParentItem = orgDomainParentItems.stream() + .filter(item -> StringUtils.equals(orgDomainParent, item.getCode())) + .findFirst(); + if (orgDomainParentItem.isPresent()) { + StringBuilder sb = new StringBuilder(); + sb.append(orgDomainParentItem.get().getName()); + if (StringUtils.isNotBlank(cases.getOrgDomainParent2())) { + Optional orgDomainParent2Item = orgDomainParentItems.stream() + .filter(item -> StringUtils.equals(cases.getOrgDomainParent2(), item.getCode())) + .findFirst(); + orgDomainParent2Item.ifPresent(dictItem -> sb.append(" - ").append(dictItem.getName())); + if (StringUtils.isNotBlank(cases.getOrgDomainParent3())) { + Optional orgDomainParent3Item = orgDomainParentItems.stream() + .filter(item -> StringUtils.equals(cases.getOrgDomainParent3(), item.getCode())) + .findFirst(); + orgDomainParent3Item.ifPresent(dictItem -> sb.append(" - ").append(dictItem.getName())); + } + } + fileMetaData.put("组织领域", sb.toString()); + } + // 分类:majorIds + List cmtList = casesMajorTypeDao.findList(FieldFilters.eq("caseId", cases.getId())); + if (cmtList != null && !cmtList.isEmpty()) { + List majorIds = cmtList.stream().map(CasesMajorType::getMajorId).collect(Collectors.toList()); + + List majorItems = sysDictionaryService.findByKey("major_type"); + if (majorItems != null && !majorItems.isEmpty()) { + List majorNames = majorItems.stream() + .filter(item -> majorIds.contains(item.getCode())) + .map(DictItem::getName) + .collect(Collectors.toList()); + fileMetaData.put("组织领域", String.join(", ", majorNames)); + } + } + + builder.addTextBody("fileMetaData", fileMetaData.toJSONString(), ContentType.TEXT_PLAIN); + requestBody.put("fileMetaData", fileMetaData); // 由于接口权限,目前采用不回调,而是通过批处理的方式,处理文件状态 if (caseAiProperties.isFileUploadUseCallback()) { builder.addTextBody("callbackUrl", caseAiProperties.getFileUploadCallbackUrl(), ContentType.TEXT_PLAIN); diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/ElasticSearchIndexServiceImpl.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/ElasticSearchIndexServiceImpl.java index 2dd12638..717c7045 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/ElasticSearchIndexServiceImpl.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/service/impl/ElasticSearchIndexServiceImpl.java @@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.io.IOException; +import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @@ -134,7 +135,11 @@ public class ElasticSearchIndexServiceImpl implements IElasticSearchIndexService esData.put("answer", conversationData.getAnswerAsString()); esData.put("conversationId", conversationData.getConversationId()); esData.put("userId", conversationData.getUserId()); - esData.put("timestamp", LocalDateTime.now().toString()); + // 持续时间 + LocalDateTime now = LocalDateTime.now(); + esData.put("startTime", conversationData.getStartTime().toString()); + esData.put("timestamp", now.toString()); + esData.put("durationSeconds", Duration.between(conversationData.getStartTime(), now).getSeconds()); // 构建 caseRefer 数据 JSONArray caseReferArray = new JSONArray(); @@ -206,6 +211,9 @@ public class ElasticSearchIndexServiceImpl implements IElasticSearchIndexService CaseAiMessageVo messageVo = new CaseAiMessageVo(); messageVo.setQuery((String) sourceMap.get("query")); messageVo.setAnswer((String) sourceMap.get("answer")); + String startTimeStr = (String) sourceMap.get("startTime"); + messageVo.setStartTime(LocalDateTime.parse(startTimeStr)); + messageVo.setDurationSeconds((Long) sourceMap.get("durationSeconds")); // 解析 suggestions Object suggestionsObj = sourceMap.get("suggestions"); diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/task/CaseAiChatDataTask.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/task/CaseAiChatDataTask.java new file mode 100644 index 00000000..3ffda0e8 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/task/CaseAiChatDataTask.java @@ -0,0 +1,38 @@ +package com.xboe.module.boecase.task; + +import com.xboe.module.boecase.service.ICaseAiChatService; +import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.YearMonth; + +@Component +@Slf4j +public class CaseAiChatDataTask { + + @Autowired + private ICaseAiChatService caseAiChatService; + + + /** + * 查询上月聊天数据并下载 + * cron: 0/10 * * * * ? + */ + @XxlJob("chatDataExcelDownloadJob") + public void chatDataExcelDownload() { + LocalDateTime now = LocalDateTime.now(); + // 取上个月的1号00:00:00到上个月最后一天的23:59:59 + YearMonth lastMonth = YearMonth.from(now).minusMonths(1); + LocalDateTime startTime = now.minusMonths(1) + .withDayOfMonth(1) + .withHour(0) + .withMinute(0) + .withSecond(0); + LocalDateTime endTime = lastMonth.atEndOfMonth().atTime(23, 59, 59); + // 执行 + caseAiChatService.downloadConversationExcel(startTime, endTime); + } +} diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiMessageVo.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiMessageVo.java index dea1d7c4..df56fc14 100644 --- a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiMessageVo.java +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/CaseAiMessageVo.java @@ -2,6 +2,7 @@ package com.xboe.module.boecase.vo; import lombok.Data; +import java.time.LocalDateTime; import java.util.List; /** @@ -20,6 +21,16 @@ public class CaseAiMessageVo { */ private String answer; + /** + * 会话开始时间 + */ + private LocalDateTime startTime; + + /** + * 会话时长(秒) + */ + private Long durationSeconds; + /** * 案例引用列表 */ diff --git a/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/ConversationExcelVo.java b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/ConversationExcelVo.java new file mode 100644 index 00000000..9a79c3b8 --- /dev/null +++ b/servers/boe-server-all/src/main/java/com/xboe/module/boecase/vo/ConversationExcelVo.java @@ -0,0 +1,31 @@ +package com.xboe.module.boecase.vo; + +import lombok.Data; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 会话Excel导出VO + */ +@Data +public class ConversationExcelVo { + /** + * 会话ID + */ + private String conversationId; + + /** + * 会话名称 + */ + private String conversationName; + + /** + * 用户 + */ + private String user; + + /** + * 问答记录 + */ + private List messages; +} \ No newline at end of file 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 d61101aa..6173f3c7 100644 --- a/servers/boe-server-all/src/main/resources/application-prod.yml +++ b/servers/boe-server-all/src/main/resources/application-prod.yml @@ -84,7 +84,7 @@ xboe: secret-key: db4d24279e3d6dbf1524af42cd0bedd2 ai-api-code: 30800 chat-api-code: 32065 - case-knowledge-id: de2e006e-82fb-4ace-8813-f25c316be4ff + case-knowledge-id: 92a0e117-b1f0-4eb7-a4b3-c79fb18f2ede file-upload-callback-url: http://10.251.113.95:9090/xboe/m/boe/caseDocumentLog/uploadCallback use-white-list: true white-user-code-list: @@ -112,6 +112,7 @@ xboe: - chengmeng@boe.com.cn - liyubing@boe.com.cn - lijian-hq@boe.com.cn + ai-chat-root-path: /home/www/elearning/upload/ai/chat 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 b469287f..79880dc2 100644 --- a/servers/boe-server-all/src/main/resources/application-test.yml +++ b/servers/boe-server-all/src/main/resources/application-test.yml @@ -144,6 +144,7 @@ xboe: - "11339772" alert-email-recipients: - chengmeng@boe.com.cn + ai-chat-root-path: /home/www/elearning/upload/ai/chat jasypt: encryptor: algorithm: PBEWithMD5AndDES