From ea5a55deffa6d33048a1f7e03b71424c8add5e31 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 08 五月 2026 14:51:52 +0800
Subject: [PATCH] feat(ai): 实现采购多文件分析附件存储与历史回显功能
---
src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 files changed, 163 insertions(+), 9 deletions(-)
diff --git a/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java b/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
index 2bb0625..08dc357 100644
--- a/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
+++ b/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
@@ -11,8 +11,10 @@
import com.ruoyi.ai.service.AiChatSessionService;
import com.ruoyi.ai.service.AiFileTextExtractor;
import com.ruoyi.ai.store.MongoChatMemoryStore;
+import com.ruoyi.basic.dto.StorageBlobVO;
import com.ruoyi.basic.mapper.SupplierManageMapper;
import com.ruoyi.basic.pojo.SupplierManage;
+import com.ruoyi.basic.service.StorageBlobService;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
@@ -50,10 +52,14 @@
import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;
+import java.io.ByteArrayInputStream;
+import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Base64;
+import java.util.Arrays;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@@ -63,9 +69,11 @@
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
+import java.nio.file.Files;
@Tag(name = "閲囪喘鏅鸿兘浣�")
@RestController
@@ -87,6 +95,7 @@
private final IPurchaseLedgerService purchaseLedgerService;
private final IPaymentRegistrationService paymentRegistrationService;
private final PurchaseReturnOrdersService purchaseReturnOrdersService;
+ private final StorageBlobService storageBlobService;
private final SupplierManageMapper supplierManageMapper;
private final StreamingChatLanguageModel purchaseVisionStreamingChatModel;
@@ -96,12 +105,13 @@
MongoChatMemoryStore mongoChatMemoryStore,
AiChatSessionService aiChatSessionService,
AiFileTextExtractor aiFileTextExtractor,
- ObjectMapper objectMapper,
- IPurchaseLedgerService purchaseLedgerService,
- IPaymentRegistrationService paymentRegistrationService,
- PurchaseReturnOrdersService purchaseReturnOrdersService,
- SupplierManageMapper supplierManageMapper,
- @Qualifier("purchaseVisionStreamingChatModel") StreamingChatLanguageModel purchaseVisionStreamingChatModel) {
+ ObjectMapper objectMapper,
+ IPurchaseLedgerService purchaseLedgerService,
+ IPaymentRegistrationService paymentRegistrationService,
+ PurchaseReturnOrdersService purchaseReturnOrdersService,
+ StorageBlobService storageBlobService,
+ SupplierManageMapper supplierManageMapper,
+ @Qualifier("purchaseVisionStreamingChatModel") StreamingChatLanguageModel purchaseVisionStreamingChatModel) {
this.purchaseAgent = purchaseAgent;
this.purchaseIntentExecutor = purchaseIntentExecutor;
this.aiSessionUserContext = aiSessionUserContext;
@@ -112,6 +122,7 @@
this.purchaseLedgerService = purchaseLedgerService;
this.paymentRegistrationService = paymentRegistrationService;
this.purchaseReturnOrdersService = purchaseReturnOrdersService;
+ this.storageBlobService = storageBlobService;
this.supplierManageMapper = supplierManageMapper;
this.purchaseVisionStreamingChatModel = purchaseVisionStreamingChatModel;
}
@@ -172,6 +183,19 @@
? message
: "璇峰垎鏋愯繖浜涢噰璐枃浠讹紝鎻愬彇鍙敤浜庝笟鍔″鐞嗙殑鏁版嵁锛屽苟鏁寸悊鎴愬緟瀹㈡埛纭鐨勬牸寮�";
+ List<String> filePaths;
+ try {
+ List<StorageBlobVO> uploadedFiles = storageBlobService.upload(copyFilesForUpload(files), true);
+ filePaths = resolveFileAccessPaths(uploadedFiles);
+ } catch (Exception ex) {
+ return Flux.just("鏂囦欢涓婁紶澶辫触");
+ }
+ try {
+ mongoChatMemoryStore.appendAnalyzeFileContext(finalMemoryId, finalMessage, filePaths);
+ } catch (Exception ex) {
+ return Flux.just("浼氳瘽鏂囦欢淇℃伅淇濆瓨澶辫触");
+ }
+
String fileContent;
try {
fileContent = buildMultiFileContent(files);
@@ -189,7 +213,7 @@
aiChatSessionService.touchSession(finalMemoryId, loginUser, "閲囪喘澶氭枃浠跺垎鏋�: " + finalMessage);
if (containsImageFile(files)) {
- return chatWithPurchaseVisionModel(finalMemoryId, userPrompt, files)
+ return chatWithPurchaseVisionModel(finalMemoryId, finalMessage, userPrompt, files)
.doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
.doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
}
@@ -282,8 +306,121 @@
return false;
}
- private Flux<String> chatWithPurchaseVisionModel(String memoryId, String userPrompt, MultipartFile[] files) {
+ private List<String> resolveFileAccessPaths(List<StorageBlobVO> uploadedFiles) {
+ if (StringUtils.isEmpty(uploadedFiles)) {
+ return Collections.emptyList();
+ }
+ List<String> filePaths = new ArrayList<>();
+ for (StorageBlobVO uploadedFile : uploadedFiles) {
+ if (uploadedFile == null) {
+ continue;
+ }
+ String selectedPath;
+ if (shouldUsePreviewPath(uploadedFile)) {
+ selectedPath = StringUtils.hasText(uploadedFile.getPreviewURL())
+ ? uploadedFile.getPreviewURL()
+ : uploadedFile.getDownloadURL();
+ } else {
+ selectedPath = StringUtils.hasText(uploadedFile.getDownloadURL())
+ ? uploadedFile.getDownloadURL()
+ : uploadedFile.getPreviewURL();
+ }
+ if (StringUtils.hasText(selectedPath)) {
+ filePaths.add(selectedPath);
+ }
+ }
+ return filePaths;
+ }
+
+ private boolean shouldUsePreviewPath(StorageBlobVO uploadedFile) {
+ String contentType = uploadedFile.getContentType();
+ if (StringUtils.hasText(contentType)) {
+ String normalized = contentType.toLowerCase(Locale.ROOT);
+ if (normalized.startsWith("image/") || "application/pdf".equals(normalized)) {
+ return true;
+ }
+ }
+ String filename = uploadedFile.getOriginalFilename();
+ if (!StringUtils.hasText(filename) || !filename.contains(".")) {
+ return false;
+ }
+ String ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase(Locale.ROOT);
+ return StringUtils.inStringIgnoreCase(ext, "png", "jpg", "jpeg", "webp", "bmp", "pdf");
+ }
+
+ private List<MultipartFile> copyFilesForUpload(MultipartFile[] files) throws IOException {
+ List<MultipartFile> copies = new ArrayList<>();
+ for (MultipartFile file : files) {
+ copies.add(new InMemoryMultipartFile(
+ file.getName(),
+ file.getOriginalFilename(),
+ file.getContentType(),
+ file.getBytes()
+ ));
+ }
+ return copies;
+ }
+
+ private static final class InMemoryMultipartFile implements MultipartFile {
+ private final String name;
+ private final String originalFilename;
+ private final String contentType;
+ private final byte[] bytes;
+
+ private InMemoryMultipartFile(String name, String originalFilename, String contentType, byte[] bytes) {
+ this.name = name;
+ this.originalFilename = originalFilename;
+ this.contentType = contentType;
+ this.bytes = bytes == null ? new byte[0] : bytes;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getOriginalFilename() {
+ return originalFilename;
+ }
+
+ @Override
+ public String getContentType() {
+ return contentType;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return bytes.length == 0;
+ }
+
+ @Override
+ public long getSize() {
+ return bytes.length;
+ }
+
+ @Override
+ public byte[] getBytes() {
+ return bytes.clone();
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ @Override
+ public void transferTo(File dest) throws IOException, IllegalStateException {
+ Files.write(dest.toPath(), bytes);
+ }
+ }
+
+ private Flux<String> chatWithPurchaseVisionModel(String memoryId,
+ String userMessage,
+ String userPrompt,
+ MultipartFile[] files) {
return Flux.create(sink -> {
+ StringBuilder assistantReply = new StringBuilder();
try {
List<Content> contents = new ArrayList<>();
contents.add(TextContent.from(userPrompt));
@@ -302,14 +439,21 @@
SystemMessage.from("浣犳槸閲囪喘涓氬姟鏂囦欢鍒嗘瀽鍔╂墜銆傝浠庢枃鏈拰鍥剧墖涓瘑鍒噰璐彴璐︺�侀噰璐骇鍝佹槑缁嗐�佷粯娆炬垨閫�璐т俊鎭紝鍙緭鍑哄悎娉� JSON銆�"),
UserMessage.from(contents)
);
+ safeAppendMessages(memoryId, List.of(UserMessage.from("閲囪喘澶氭枃浠跺垎鏋�: " + userMessage)));
purchaseVisionStreamingChatModel.chat(messages, new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(String partialResponse) {
- sink.next(partialResponse);
+ if (partialResponse != null) {
+ assistantReply.append(partialResponse);
+ sink.next(partialResponse);
+ }
}
@Override
public void onCompleteResponse(ChatResponse completeResponse) {
+ if (StringUtils.hasText(assistantReply.toString())) {
+ safeAppendMessages(memoryId, List.of(AiMessage.from(assistantReply.toString())));
+ }
sink.complete();
}
@@ -325,6 +469,16 @@
});
}
+ private void safeAppendMessages(String memoryId, List<ChatMessage> messages) {
+ if (!StringUtils.hasText(memoryId) || StringUtils.isEmpty(messages)) {
+ return;
+ }
+ try {
+ mongoChatMemoryStore.appendMessages(memoryId, messages);
+ } catch (Exception ignored) {
+ }
+ }
+
private String resolveImageMimeType(MultipartFile file) {
String contentType = file.getContentType();
if (StringUtils.hasText(contentType) && contentType.startsWith("image/")) {
--
Gitblit v1.9.3