6 天以前 ea5a55deffa6d33048a1f7e03b71424c8add5e31
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;
@@ -100,6 +109,7 @@
                                IPurchaseLedgerService purchaseLedgerService,
                                IPaymentRegistrationService paymentRegistrationService,
                                PurchaseReturnOrdersService purchaseReturnOrdersService,
                                 StorageBlobService storageBlobService,
                                SupplierManageMapper supplierManageMapper,
                                @Qualifier("purchaseVisionStreamingChatModel") StreamingChatLanguageModel purchaseVisionStreamingChatModel) {
        this.purchaseAgent = purchaseAgent;
@@ -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) {
                        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/")) {