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