From b6060477204af8038c2f7bff385a64de708dc03d Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 08 五月 2026 15:59:23 +0800
Subject: [PATCH] refactor(purchase-ai): 重构采购AI控制器以使用服务层
---
src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java | 1009 -----------------------------
src/main/java/com/ruoyi/ai/service/PurchaseAiService.java | 1031 ++++++++++++++++++++++++++++++
2 files changed, 1,045 insertions(+), 995 deletions(-)
diff --git a/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java b/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
index 08dc357..158ea61 100644
--- a/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
+++ b/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
@@ -1,162 +1,41 @@
package com.ruoyi.ai.controller;
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.ruoyi.ai.assistant.PurchaseAgent;
-import com.ruoyi.ai.assistant.PurchaseIntentExecutor;
import com.ruoyi.ai.bean.ChatForm;
import com.ruoyi.ai.bean.PurchaseAiConfirmRequest;
-import com.ruoyi.ai.context.AiSessionUserContext;
-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.ai.service.PurchaseAiService;
import com.ruoyi.common.utils.SecurityUtils;
-import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
-import com.ruoyi.purchase.dto.PurchaseLedgerDto;
-import com.ruoyi.purchase.dto.PurchaseReturnOrderDto;
-import com.ruoyi.purchase.pojo.PaymentRegistration;
-import com.ruoyi.purchase.service.IPaymentRegistrationService;
-import com.ruoyi.purchase.service.IPurchaseLedgerService;
-import com.ruoyi.purchase.service.PurchaseReturnOrdersService;
-import com.ruoyi.sales.pojo.SalesLedgerProduct;
-import dev.langchain4j.data.image.Image;
-import dev.langchain4j.data.message.AiMessage;
-import dev.langchain4j.data.message.ChatMessage;
-import dev.langchain4j.data.message.Content;
-import dev.langchain4j.data.message.ImageContent;
-import dev.langchain4j.data.message.SystemMessage;
-import dev.langchain4j.data.message.TextContent;
-import dev.langchain4j.data.message.UserMessage;
-import dev.langchain4j.model.chat.StreamingChatLanguageModel;
-import dev.langchain4j.model.chat.response.ChatResponse;
-import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
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;
-import java.time.format.DateTimeParseException;
-import java.util.ArrayList;
-import java.util.Collections;
-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
@RequestMapping("/purchase-ai")
public class PurchaseAiController extends BaseController {
- private static final String PURCHASE_FILE_ANALYZE_MEMORY_PREFIX = "purchase-file-analyze::";
- private static final int MAX_FILE_COUNT = 10;
- private static final int MAX_SINGLE_FILE_TEXT_LENGTH = 8000;
- private static final int MAX_TOTAL_FILE_TEXT_LENGTH = 30000;
+ private final PurchaseAiService purchaseAiService;
- private final PurchaseAgent purchaseAgent;
- private final PurchaseIntentExecutor purchaseIntentExecutor;
- private final AiSessionUserContext aiSessionUserContext;
- private final MongoChatMemoryStore mongoChatMemoryStore;
- private final AiChatSessionService aiChatSessionService;
- private final AiFileTextExtractor aiFileTextExtractor;
- private final ObjectMapper objectMapper;
- private final IPurchaseLedgerService purchaseLedgerService;
- private final IPaymentRegistrationService paymentRegistrationService;
- private final PurchaseReturnOrdersService purchaseReturnOrdersService;
- private final StorageBlobService storageBlobService;
- private final SupplierManageMapper supplierManageMapper;
- private final StreamingChatLanguageModel purchaseVisionStreamingChatModel;
-
- public PurchaseAiController(PurchaseAgent purchaseAgent,
- PurchaseIntentExecutor purchaseIntentExecutor,
- AiSessionUserContext aiSessionUserContext,
- MongoChatMemoryStore mongoChatMemoryStore,
- AiChatSessionService aiChatSessionService,
- AiFileTextExtractor aiFileTextExtractor,
- 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;
- this.mongoChatMemoryStore = mongoChatMemoryStore;
- this.aiChatSessionService = aiChatSessionService;
- this.aiFileTextExtractor = aiFileTextExtractor;
- this.objectMapper = objectMapper;
- this.purchaseLedgerService = purchaseLedgerService;
- this.paymentRegistrationService = paymentRegistrationService;
- this.purchaseReturnOrdersService = purchaseReturnOrdersService;
- this.storageBlobService = storageBlobService;
- this.supplierManageMapper = supplierManageMapper;
- this.purchaseVisionStreamingChatModel = purchaseVisionStreamingChatModel;
+ public PurchaseAiController(PurchaseAiService purchaseAiService) {
+ this.purchaseAiService = purchaseAiService;
}
@Operation(summary = "閲囪喘瀵硅瘽")
@PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
public Flux<String> chat(@RequestBody ChatForm chatForm) {
- if (!StringUtils.hasText(chatForm.getMemoryId())) {
- return Flux.just("memoryId涓嶈兘涓虹┖");
- }
- if (!StringUtils.hasText(chatForm.getMessage())) {
- return Flux.just("message涓嶈兘涓虹┖");
- }
-
LoginUser loginUser = SecurityUtils.getLoginUser();
- String memoryId = chatForm.getMemoryId();
- String userMessage = chatForm.getMessage();
-
- aiSessionUserContext.bind(memoryId, loginUser);
- aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
-
- String directResponse = purchaseIntentExecutor.tryExecute(memoryId, userMessage);
- if (StringUtils.isNotEmpty(directResponse)) {
- mongoChatMemoryStore.appendMessages(
- memoryId,
- List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
- );
- aiChatSessionService.refreshSessionStats(memoryId, loginUser);
- return Flux.just(directResponse);
- }
-
- return purchaseAgent.chat(memoryId, userMessage)
- .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
- .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
+ return purchaseAiService.chat(chatForm, loginUser);
}
@Operation(summary = "閲囪喘澶氭枃浠跺垎鏋�")
@@ -164,894 +43,34 @@
public Flux<String> analyzeFiles(@RequestParam("files") MultipartFile[] files,
@RequestParam(value = "message", required = false) String message,
@RequestParam(value = "memoryId", required = false) String memoryId) {
- if (files == null || files.length == 0) {
- return Flux.just("files涓嶈兘涓虹┖");
- }
- if (files.length > MAX_FILE_COUNT) {
- return Flux.just("涓�娆℃渶澶氬垎鏋�" + MAX_FILE_COUNT + "涓枃浠�");
- }
-
- String rawMemoryId = StringUtils.hasText(memoryId) ? memoryId : UUID.randomUUID().toString();
- String finalMemoryId = rawMemoryId.startsWith(PURCHASE_FILE_ANALYZE_MEMORY_PREFIX)
- ? rawMemoryId
- : PURCHASE_FILE_ANALYZE_MEMORY_PREFIX + rawMemoryId;
-
LoginUser loginUser = SecurityUtils.getLoginUser();
- aiSessionUserContext.bind(finalMemoryId, loginUser);
-
- String finalMessage = StringUtils.hasText(message)
- ? 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);
- } catch (IllegalArgumentException ex) {
- return Flux.just(ex.getMessage());
- } catch (IOException ex) {
- return Flux.just("鏂囦欢璇诲彇澶辫触");
- }
-
- if (!StringUtils.hasText(fileContent)) {
- return Flux.just("鏈彁鍙栧埌鏈夋晥鏂囦欢鍐呭");
- }
-
- String userPrompt = buildPurchaseFileAnalyzePrompt(finalMessage, fileContent);
- aiChatSessionService.touchSession(finalMemoryId, loginUser, "閲囪喘澶氭枃浠跺垎鏋�: " + finalMessage);
-
- if (containsImageFile(files)) {
- return chatWithPurchaseVisionModel(finalMemoryId, finalMessage, userPrompt, files)
- .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
- .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
- }
-
- return Flux.defer(() -> purchaseAgent.chat(finalMemoryId, userPrompt))
- .onErrorResume(NoSuchElementException.class, ex -> {
- mongoChatMemoryStore.deleteMessages(finalMemoryId);
- return purchaseAgent.chat(finalMemoryId, userPrompt);
- })
- .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
- .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
+ return purchaseAiService.analyzeFiles(files, message, memoryId, loginUser);
}
@Operation(summary = "閲囪喘澶氭枃浠跺垎鏋愮‘璁ゅ鐞�")
@PostMapping("/analyze-files/confirm")
public AjaxResult confirmAnalyzeResult(@RequestBody PurchaseAiConfirmRequest request) {
- if (request == null || !StringUtils.hasText(request.getBusinessType())) {
- return AjaxResult.error("businessType涓嶈兘涓虹┖");
- }
- if (request.getPayload() == null || request.getPayload().isEmpty()) {
- return AjaxResult.error("payload涓嶈兘涓虹┖");
- }
-
- try {
- String businessType = request.getBusinessType().trim();
- return switch (businessType) {
- case "purchase_ledger" -> processPurchaseLedger(request.getPayload());
- case "payment_registration" -> processPaymentRegistration(request.getPayload());
- case "purchase_return_order" -> processPurchaseReturnOrder(request.getPayload());
- default -> AjaxResult.error("鏆備笉鏀寔璇ヤ笟鍔$被鍨�: " + businessType);
- };
- } catch (Exception ex) {
- return AjaxResult.error(toCustomerMessage(ex));
- }
+ return purchaseAiService.confirmAnalyzeResult(request);
}
@Operation(summary = "閲囪喘浼氳瘽鍒楄〃")
@GetMapping("/history/sessions")
public AjaxResult listSessions() {
- return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser()));
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ return success(purchaseAiService.listSessions(loginUser));
}
@Operation(summary = "閲囪喘浼氳瘽娑堟伅")
@GetMapping("/history/messages/{memoryId}")
public AjaxResult listMessages(@PathVariable String memoryId) {
- return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser()));
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ return success(purchaseAiService.listMessages(memoryId, loginUser));
}
@Operation(summary = "鍒犻櫎閲囪喘浼氳瘽")
@DeleteMapping("/history/{memoryId}")
public AjaxResult deleteSession(@PathVariable String memoryId) {
- aiSessionUserContext.remove(memoryId);
- return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
- }
-
- private String buildMultiFileContent(MultipartFile[] files) throws IOException {
- StringBuilder builder = new StringBuilder();
- int totalLength = 0;
- for (MultipartFile file : files) {
- String text = aiFileTextExtractor.extractText(file);
- if (!StringUtils.hasText(text)) {
- continue;
- }
- String limitedText = text.length() > MAX_SINGLE_FILE_TEXT_LENGTH
- ? text.substring(0, MAX_SINGLE_FILE_TEXT_LENGTH)
- : text;
- if (totalLength + limitedText.length() > MAX_TOTAL_FILE_TEXT_LENGTH) {
- int remain = MAX_TOTAL_FILE_TEXT_LENGTH - totalLength;
- if (remain <= 0) {
- break;
- }
- limitedText = limitedText.substring(0, remain);
- }
- builder.append("\n--- 鏂囦欢: ")
- .append(file.getOriginalFilename())
- .append(" ---\n")
- .append(limitedText)
- .append('\n');
- totalLength += limitedText.length();
- }
- return builder.toString();
- }
-
- private boolean containsImageFile(MultipartFile[] files) {
- for (MultipartFile file : files) {
- if (aiFileTextExtractor.isImageFile(file)) {
- return true;
- }
- }
- return false;
- }
-
- 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));
- for (MultipartFile file : files) {
- if (!aiFileTextExtractor.isImageFile(file)) {
- continue;
- }
- contents.add(TextContent.from("涓嬮潰杩欏紶鍥剧墖鏂囦欢鍚嶏細" + file.getOriginalFilename()));
- contents.add(ImageContent.from(Image.builder()
- .base64Data(Base64.getEncoder().encodeToString(file.getBytes()))
- .mimeType(resolveImageMimeType(file))
- .build()));
- }
-
- List<ChatMessage> messages = List.of(
- 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();
- }
-
- @Override
- public void onError(Throwable error) {
- sink.error(error);
- }
- });
- } catch (Exception ex) {
- sink.next("鍥剧墖鏂囦欢璇诲彇澶辫触锛岃纭鍥剧墖鏍煎紡涓� png銆乯pg銆乯peg銆亀ebp 鎴� bmp锛屼笖澶у皬涓嶈秴杩�10MB");
- sink.complete();
- }
- });
- }
-
- 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/")) {
- return contentType;
- }
- String filename = file.getOriginalFilename();
- String ext = "";
- if (StringUtils.hasText(filename) && filename.contains(".")) {
- ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
- }
- return switch (ext) {
- case "jpg", "jpeg" -> "image/jpeg";
- case "webp" -> "image/webp";
- case "bmp" -> "image/bmp";
- default -> "image/png";
- };
- }
-
- private String buildPurchaseFileAnalyzePrompt(String message, String fileContent) {
- return """
- 浣犳槸閲囪喘涓氬姟鏂囦欢鍒嗘瀽鍔╂墜銆傝涓ユ牸鏍规嵁鐢ㄦ埛涓婁紶鐨勫涓枃浠跺拰鐢ㄦ埛瑕佹眰鎻愬彇閲囪喘涓氬姟鏁版嵁銆�
-
- 鐢ㄦ埛瑕佹眰:
- %s
-
- 杈撳嚭瑕佹眰:
- 1. 鍙緭鍑哄悎娉� JSON锛屼笉瑕� Markdown锛屼笉瑕侀澶栬В閲娿��
- 2. JSON 椤跺眰瀛楁鍥哄畾涓�:
- - success: boolean
- - businessType: purchase_ledger | payment_registration | purchase_return_order | unknown
- - action: confirm_required
- - description: 涓枃璇存槑
- - confidence: 0鍒�1鐨勫皬鏁�
- - missingFields: 缂哄け瀛楁涓枃鍚嶇О鏁扮粍锛岄潰鍚戝鎴峰睍绀猴紝涓嶈杈撳嚭鑻辨枃瀛楁鍚�
- - warnings: 椋庨櫓鎻愮ず鏁扮粍
- - payload: 寰呭鎴风‘璁ょ殑鏁版嵁锛屽瓧娈靛悕蹇呴』浣跨敤鍚庣 DTO 瀛楁鍚�
- - preview: 缁欏鎴风‘璁ょ敤鐨勪腑鏂囨憳瑕佹暟缁�
- 3. 濡傛灉鍙垽鏂负閲囪喘鍙拌处锛宐usinessType 浣跨敤 purchase_ledger锛宲ayload.purchaseLedgers 涓洪噰璐鍗�/閲囪喘鍙拌处鏁扮粍:
- - purchaseLedgers: 閲囪喘璁㈠崟/閲囪喘鍙拌处鏁扮粍锛屾瘡鏉¤褰曞瓧娈靛悕蹇呴』涓� PurchaseLedgerDto 淇濇寔涓�鑷�
- - 浜у搧鏄庣粏蹇呴』鏀惧湪姣忔潯閲囪喘鍙拌处璁板綍鐨� productData 瀛楁涓紝productData 绫诲瀷涓� List<SalesLedgerProduct>
- - 涓嶈浼樺厛浣跨敤 payload 椤跺眰 productData锛涢《灞� productData 浠呬綔涓烘棫鏍煎紡鍏煎
- - 鏂囦欢閲岀殑鈥滈噰璐崟鍙封�濆氨鏄�滈噰璐悎鍚屽彿鈥濓紝缁熶竴鏄犲皠涓� purchaseContractNumber
- - 鏂囦欢閲岀殑鈥滈攢鍞崟鍙封�濆氨鏄�滈攢鍞悎鍚屽彿鈥濓紝缁熶竴鏄犲皠涓� salesContractNo
- - 鎵�鏈夋棩鏈熷瓧娈靛繀椤讳娇鐢� yyyy-MM-dd锛屼緥濡� 2026-04-30锛涗笉瑕佽緭鍑� 4/30/26銆�2026/4/30銆�2026骞�4鏈�30鏃� 鎴栧甫鏃跺垎绉掔殑鏍煎紡
- - 閲囪喘鍙拌处涓嶉渶瑕佸湪 payload 涓紶瀹℃壒浜猴紝涓嶈杈撳嚭 approveUserIds銆乤pproverId
- - missingFields 鍙~鍐欎笟鍔″繀濉絾鏃犳硶璇嗗埆鐨勫瓧娈碉紝涓嶈鎶� PurchaseLedgerDto 鐨勬墍鏈夌┖瀛楁閮藉垪涓虹己澶憋紱缂哄け椤瑰繀椤诲啓涓枃锛屼緥濡傗�滀緵搴斿晢鍚嶇О鈥濃�滃惈绋庡崟浠封�濓紝涓嶈鍐� supplierId銆乼axInclusiveUnitPrice
- - 閲囪喘鍙拌处涓昏〃蹇呭~瀛楁浠呮寜杩欎簺鍒ゆ柇: purchaseContractNumber銆乻upplierName 鎴� supplierId
- - productData 姣忔潯浜у搧蹇呭~瀛楁: productCategory銆乻pecificationModel銆乽nit銆乹uantity銆乼axInclusiveUnitPrice 鎴� taxInclusiveTotalPrice锛涘鏋滃彧鏈夊惈绋庢�讳环鍜屾暟閲忥紝蹇呴』璁$畻 taxInclusiveUnitPrice锛涘鏋滃彧鏈夊惈绋庡崟浠峰拰鏁伴噺锛屽繀椤昏绠� taxInclusiveTotalPrice
- - 浜у搧瀛楁鎸夐噰璐鍏ユ帴鍙� PurchaseLedgerProductImportDto 瀵归綈: 閲囪喘鍗曞彿銆佷骇鍝佸ぇ绫汇�佽鏍煎瀷鍙枫�佸崟浣嶃�佹暟閲忋�佺◣鐜囥�佸惈绋庡崟浠枫�佸惈绋庢�讳环銆佸彂绁ㄧ被鍨嬨�佹槸鍚﹁川妫�
- - 閲囪喘浜у搧 type 鍥哄畾涓� 2
- - purchaseLedgers 姣忔潯璁板綍鍙娇鐢ㄨ繖浜� PurchaseLedgerDto 瀛楁鍚�:
- entryDateStart, entryDateEnd, id, purchaseContractNumber, supplierId, supplierName, isWhite, recorderId, recorderName, salesContractNo, salesContractNoId, projectName, entryDate, executionDate, remarks, attachmentMaterials, createdAt, updatedAt, salesLedgerId, hasChildren, Type, productData, tempFileIds, SalesLedgerFiles, phoneNumber, businessPersonId, productId, productModelId, invoiceNumber, invoiceAmount, ticketRegistrationId, contractAmount, receiptPaymentAmount, unReceiptPaymentAmount, type, paymentMethod, approvalStatus, templateName
- - productData 姣忔潯浜у搧鍙娇鐢ㄨ繖浜� SalesLedgerProduct 瀛楁鍚�:
- productCategory, specificationModel, unit, quantity, taxRate, taxInclusiveUnitPrice, taxInclusiveTotalPrice, taxExclusiveTotalPrice, invoiceType, productId, productModelId, isChecked, type
- 4. 濡傛灉鍙垽鏂负浠樻鐧昏锛宐usinessType 浣跨敤 payment_registration锛宲ayload.records 涓轰粯娆剧櫥璁版暟缁勶紝瀛楁灏介噺鍖呭惈 purchaseLedgerId銆乻alesLedgerProductId銆乧urrentPaymentAmount銆乸aymentMethod銆乸aymentDate銆�
- 5. 濡傛灉鍙垽鏂负閲囪喘閫�璐э紝businessType 浣跨敤 purchase_return_order锛宲ayload 鎸� PurchaseReturnOrderDto 缁勭粐锛屾槑缁嗘斁 purchaseReturnOrderProductsDtos銆�
- 6. 缂哄皯涓氬姟澶勭悊蹇呴』瀛楁鏃讹紝涓嶈缂栭�� ID锛屾妸瀛楁鏀惧叆 missingFields锛屽苟浠嶈繑鍥炲彲纭鐨勮崏绋挎暟鎹��
- 7. 鎵�鏈変腑鏂囧唴瀹圭洿鎺ヤ繚鐣欙紝涓嶈杞箟鎴� Unicode銆�
-
- 鏂囦欢鍐呭:
- %s
- """.formatted(message, fileContent);
- }
-
- private AjaxResult processPurchaseLedger(Map<String, Object> payload) throws Exception {
- if (payload.containsKey("purchaseLedgers")) {
- return processPurchaseLedgerBatch(payload);
- }
-
- Map<String, Object> normalizedPayload = normalizePurchaseLedgerMap(payload);
- PurchaseLedgerDto dto = objectMapper.convertValue(normalizedPayload, PurchaseLedgerDto.class);
- AjaxResult ledgerResult = validatePurchaseLedger(dto, 0);
- if (ledgerResult != null) {
- return ledgerResult;
- }
- AjaxResult supplierResult = fillSupplierIdByName(dto);
- if (supplierResult != null) {
- return supplierResult;
- }
- AjaxResult productResult = validatePurchaseProducts(dto.getProductData(), 0);
- if (productResult != null) {
- return productResult;
- }
- int result = purchaseLedgerService.addOrEditPurchase(dto);
- return AjaxResult.success("閲囪喘鍙拌处宸插鐞�", result);
- }
-
- private AjaxResult processPurchaseLedgerBatch(Map<String, Object> payload) throws Exception {
- List<Map<String, Object>> purchaseLedgers = toMapList(payload.get("purchaseLedgers"));
- if (purchaseLedgers.isEmpty()) {
- return AjaxResult.error("purchaseLedgers涓嶈兘涓虹┖");
- }
-
- List<Map<String, Object>> topLevelProductData = toMapList(payload.get("productData"));
- List<Map<String, Object>> results = new ArrayList<>();
- for (int i = 0; i < purchaseLedgers.size(); i++) {
- Map<String, Object> ledgerMap = normalizePurchaseLedgerMap(purchaseLedgers.get(i));
- PurchaseLedgerDto dto = objectMapper.convertValue(ledgerMap, PurchaseLedgerDto.class);
- AjaxResult ledgerResult = validatePurchaseLedger(dto, i);
- if (ledgerResult != null) {
- return ledgerResult;
- }
- AjaxResult supplierResult = fillSupplierIdByName(dto);
- if (supplierResult != null) {
- return supplierResult;
- }
-
- List<SalesLedgerProduct> products = dto.getProductData();
- if (products == null || products.isEmpty()) {
- products = matchProductsForLedger(ledgerMap, dto, topLevelProductData, purchaseLedgers.size() == 1);
- dto.setProductData(products);
- }
- AjaxResult productResult = validatePurchaseProducts(products, i);
- if (productResult != null) {
- return productResult;
- }
- int result = purchaseLedgerService.addOrEditPurchase(dto);
-
- Map<String, Object> item = new LinkedHashMap<>();
- item.put("index", i);
- item.put("purchaseContractNumber", dto.getPurchaseContractNumber());
- item.put("supplierId", dto.getSupplierId());
- item.put("supplierName", dto.getSupplierName());
- item.put("productCount", products.size());
- item.put("result", result);
- results.add(item);
- }
- return AjaxResult.success("閲囪喘鍙拌处宸叉壒閲忓鐞�", results);
- }
-
- private List<SalesLedgerProduct> matchProductsForLedger(Map<String, Object> ledgerMap,
- PurchaseLedgerDto dto,
- List<Map<String, Object>> productData,
- boolean onlyOneLedger) {
- List<SalesLedgerProduct> products = new ArrayList<>();
- for (Map<String, Object> productMap : productData) {
- if (onlyOneLedger || productBelongsToLedger(productMap, ledgerMap, dto)) {
- products.add(objectMapper.convertValue(normalizeSalesLedgerProductMap(productMap), SalesLedgerProduct.class));
- }
- }
- return products;
- }
-
- private boolean productBelongsToLedger(Map<String, Object> productMap, Map<String, Object> ledgerMap, PurchaseLedgerDto dto) {
- Long productPurchaseLedgerId = longValue(productMap, "purchaseLedgerId", "purchaseId", "閲囪喘璁㈠崟id", "閲囪喘鍙拌处id");
- if (productPurchaseLedgerId != null && dto.getId() != null && productPurchaseLedgerId.equals(dto.getId())) {
- return true;
- }
-
- Long productSalesLedgerId = longValue(productMap, "salesLedgerId");
- if (productSalesLedgerId != null && dto.getId() != null && productSalesLedgerId.equals(dto.getId())) {
- return true;
- }
-
- String productContractNo = stringValue(productMap, "purchaseContractNumber", "purchaseContractNo", "閲囪喘鍚堝悓鍙�", "閲囪喘鍗曞彿", "閲囪喘璁㈠崟鍙�");
- if (StringUtils.hasText(productContractNo)
- && StringUtils.hasText(dto.getPurchaseContractNumber())
- && productContractNo.trim().equals(dto.getPurchaseContractNumber().trim())) {
- return true;
- }
-
- String ledgerContractNo = stringValue(ledgerMap, "purchaseContractNumber", "purchaseContractNo", "閲囪喘鍚堝悓鍙�", "閲囪喘鍗曞彿", "閲囪喘璁㈠崟鍙�");
- if (StringUtils.hasText(productContractNo)
- && StringUtils.hasText(ledgerContractNo)
- && productContractNo.trim().equals(ledgerContractNo.trim())) {
- return true;
- }
-
- String productSalesContractNo = stringValue(productMap, "salesContractNo", "salesContractNumber", "閿�鍞悎鍚屽彿", "閿�鍞崟鍙�", "閿�鍞鍗曞彿");
- if (StringUtils.hasText(productSalesContractNo)
- && StringUtils.hasText(dto.getSalesContractNo())
- && productSalesContractNo.trim().equals(dto.getSalesContractNo().trim())) {
- return true;
- }
-
- String ledgerSalesContractNo = stringValue(ledgerMap, "salesContractNo", "salesContractNumber", "閿�鍞悎鍚屽彿", "閿�鍞崟鍙�", "閿�鍞鍗曞彿");
- if (StringUtils.hasText(productSalesContractNo)
- && StringUtils.hasText(ledgerSalesContractNo)
- && productSalesContractNo.trim().equals(ledgerSalesContractNo.trim())) {
- return true;
- }
-
- String productSupplierName = stringValue(productMap, "supplierName", "渚涘簲鍟嗗悕绉�");
- return StringUtils.hasText(productSupplierName)
- && StringUtils.hasText(dto.getSupplierName())
- && productSupplierName.trim().equals(dto.getSupplierName().trim());
- }
-
- private Map<String, Object> normalizePurchaseLedgerMap(Map<String, Object> source) {
- Map<String, Object> target = new LinkedHashMap<>();
- copyPurchaseLedgerDtoFields(source, target);
- putDtoFieldIfPresent(source, target, "entryDateStart", "褰曞叆寮�濮嬫棩鏈�", "褰曞叆鏃ユ湡寮�濮�");
- putDtoFieldIfPresent(source, target, "entryDateEnd", "褰曞叆缁撴潫鏃ユ湡", "褰曞叆鏃ユ湡缁撴潫");
- putDtoFieldIfPresent(source, target, "id", "閲囪喘鍙拌处id", "閲囪喘璁㈠崟id", "涓婚敭");
- putDtoFieldIfPresent(source, target, "purchaseContractNumber", "purchaseContractNo", "閲囪喘鍚堝悓鍙�", "閲囪喘鍗曞彿", "閲囪喘璁㈠崟鍙�");
- putDtoFieldIfPresent(source, target, "supplierId", "渚涘簲鍟唅d", "渚涘簲鍟咺D", "渚涘簲鍟嗗悕绉癷d", "渚涘簲鍟嗗悕绉癐D");
- putDtoFieldIfPresent(source, target, "supplierName", "渚涘簲鍟�", "渚涘簲鍟嗗悕绉�");
- putDtoFieldIfPresent(source, target, "isWhite", "鏄惁鐧藉悕鍗�");
- putDtoFieldIfPresent(source, target, "recorderId", "褰曞叆浜篿d", "褰曞叆浜篒D", "褰曞叆浜哄鍚峣d", "褰曞叆浜哄鍚岻D");
- putDtoFieldIfPresent(source, target, "recorderName", "褰曞叆浜�", "褰曞叆浜哄鍚�");
- putDtoFieldIfPresent(source, target, "salesContractNo", "salesContractNumber", "閿�鍞悎鍚屽彿", "閿�鍞崟鍙�", "閿�鍞鍗曞彿");
- putDtoFieldIfPresent(source, target, "salesContractNoId", "閿�鍞悎鍚屽彿id", "閿�鍞悎鍚屽彿ID", "閿�鍞崟鍙穒d", "閿�鍞崟鍙稩D");
- putDtoFieldIfPresent(source, target, "projectName", "椤圭洰", "椤圭洰鍚嶇О");
- putDtoFieldIfPresent(source, target, "entryDate", "褰曞叆鏃ユ湡");
- putDtoFieldIfPresent(source, target, "executionDate", "绛捐鏃ユ湡", "鍚堝悓绛捐鏃ユ湡");
- putDtoFieldIfPresent(source, target, "remarks", "澶囨敞", "璇存槑");
- putDtoFieldIfPresent(source, target, "attachmentMaterials", "闄勪欢鏉愭枡", "闄勪欢鏉愭枡璺緞鎴栧悕绉�");
- putDtoFieldIfPresent(source, target, "createdAt", "鍒涘缓鏃堕棿", "璁板綍鍒涘缓鏃堕棿");
- putDtoFieldIfPresent(source, target, "updatedAt", "鏇存柊鏃堕棿", "璁板綍鏈�鍚庢洿鏂版椂闂�");
- putDtoFieldIfPresent(source, target, "salesLedgerId", "閿�鍞彴璐d", "閿�鍞彴璐D", "鍏宠仈閿�鍞彴璐︿富琛ㄤ富閿�");
- putDtoFieldIfPresent(source, target, "hasChildren", "鏄惁鏈夊瓙绾�", "鏄惁鏈夋槑缁�");
- putDtoFieldIfPresent(source, target, "Type", "鍙拌处绫诲瀷", "涓氬姟绫诲瀷");
- putDtoFieldIfPresent(source, target, "productData", "products", "浜у搧鏄庣粏", "閲囪喘浜у搧鏄庣粏");
- putDtoFieldIfPresent(source, target, "tempFileIds", "涓存椂鏂囦欢id", "涓存椂鏂囦欢ID", "涓存椂鏂囦欢ids");
- putDtoFieldIfPresent(source, target, "SalesLedgerFiles", "闄勪欢鍒楄〃", "閿�鍞彴璐﹂檮浠�");
- putDtoFieldIfPresent(source, target, "phoneNumber", "涓氬姟鍛樻墜鏈哄彿", "鎵嬫満鍙�");
- putDtoFieldIfPresent(source, target, "businessPersonId", "涓氬姟鍛榠d", "涓氬姟鍛業D");
- putDtoFieldIfPresent(source, target, "productId", "浜у搧id", "浜у搧ID");
- putDtoFieldIfPresent(source, target, "productModelId", "浜у搧瑙勬牸id", "浜у搧瑙勬牸ID");
- putDtoFieldIfPresent(source, target, "invoiceNumber", "鍙戠エ鍙�", "鍙戠エ鍙风爜");
- putDtoFieldIfPresent(source, target, "invoiceAmount", "鍙戠エ閲戦", "鍙戠エ閲戦锛堝厓锛�");
- putDtoFieldIfPresent(source, target, "ticketRegistrationId", "鏉ョエ鐧昏id", "鏉ョエ鐧昏ID");
- putDtoFieldIfPresent(source, target, "contractAmount", "鍚堝悓閲戦", "鍚堝悓閲戦锛堜骇鍝佸惈绋庢�讳环锛�");
- putDtoFieldIfPresent(source, target, "receiptPaymentAmount", "鏉ョエ閲戦", "宸叉潵绁ㄩ噾棰�", "宸叉潵绁ㄩ噾棰�(鍏�)");
- putDtoFieldIfPresent(source, target, "unReceiptPaymentAmount", "鏈潵绁ㄩ噾棰�", "鏈潵绁ㄩ噾棰�(鍏�)");
- putDtoFieldIfPresent(source, target, "type", "鏂囦欢绫诲瀷");
- putDtoFieldIfPresent(source, target, "paymentMethod", "浠樻鏂瑰紡");
- putDtoFieldIfPresent(source, target, "approvalStatus", "瀹℃壒鐘舵��");
- putDtoFieldIfPresent(source, target, "templateName", "妯℃澘鍚嶇О");
- target.remove("approveUserIds");
- target.remove("approverId");
- normalizeNestedProductData(target);
- attachImportStyleProductData(source, target);
- if (target.get("type") == null) {
- target.put("type", 2);
- }
- target.putIfAbsent("entryDate", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
- normalizePurchaseLedgerDateFields(target);
- return target;
- }
-
- private void attachImportStyleProductData(Map<String, Object> source, Map<String, Object> target) {
- if (target.get("productData") != null) {
- return;
- }
- Map<String, Object> productMap = normalizeSalesLedgerProductMap(source);
- if (hasImportStyleProductData(productMap)) {
- target.put("productData", List.of(productMap));
- }
- }
-
- private boolean hasImportStyleProductData(Map<String, Object> productMap) {
- return hasMapText(productMap, "productCategory")
- || hasMapText(productMap, "specificationModel")
- || productMap.get("quantity") != null
- || productMap.get("taxInclusiveUnitPrice") != null
- || productMap.get("taxInclusiveTotalPrice") != null;
- }
-
- private boolean hasMapText(Map<String, Object> map, String key) {
- Object value = map.get(key);
- return value != null && StringUtils.hasText(String.valueOf(value));
- }
-
- private void normalizeNestedProductData(Map<String, Object> target) {
- Object productDataValue = target.get("productData");
- if (productDataValue == null) {
- return;
- }
- List<Map<String, Object>> productMaps = toMapList(productDataValue);
- List<Map<String, Object>> normalizedProducts = new ArrayList<>();
- for (Map<String, Object> productMap : productMaps) {
- normalizedProducts.add(normalizeSalesLedgerProductMap(productMap));
- }
- target.put("productData", normalizedProducts);
- }
-
- private Map<String, Object> normalizeSalesLedgerProductMap(Map<String, Object> source) {
- Map<String, Object> target = new LinkedHashMap<>();
- copySalesLedgerProductFields(source, target);
- putDtoFieldIfPresent(source, target, "productCategory", "浜у搧澶х被", "浜у搧鍚嶇О", "浜у搧", "鍝佸悕", "鐗╂枡鍚嶇О");
- putDtoFieldIfPresent(source, target, "specificationModel", "瑙勬牸鍨嬪彿", "鍨嬪彿", "瑙勬牸", "浜у搧瑙勬牸");
- putDtoFieldIfPresent(source, target, "unit", "鍗曚綅");
- putDtoFieldIfPresent(source, target, "quantity", "鏁伴噺", "閲囪喘鏁伴噺");
- putDtoFieldIfPresent(source, target, "taxRate", "绋庣巼");
- putDtoFieldIfPresent(source, target, "taxInclusiveUnitPrice", "鍚◣鍗曚环", "鍗曚环", "閲囪喘鍗曚环", "鍚◣浠锋牸");
- putDtoFieldIfPresent(source, target, "taxInclusiveTotalPrice", "鍚◣鎬讳环", "鎬讳环", "閲囪喘閲戦", "閲戦", "鍚堝悓閲戦");
- putDtoFieldIfPresent(source, target, "taxExclusiveTotalPrice", "涓嶅惈绋庢�讳环");
- putDtoFieldIfPresent(source, target, "invoiceType", "鍙戠エ绫诲瀷", "鍙戠エ绫诲埆");
- putDtoFieldIfPresent(source, target, "productId", "浜у搧id", "浜у搧ID");
- putDtoFieldIfPresent(source, target, "productModelId", "浜у搧瑙勬牸id", "浜у搧瑙勬牸ID", "鍨嬪彿id", "鍨嬪彿ID");
- putDtoFieldIfPresent(source, target, "isChecked", "鏄惁璐ㄦ", "鏄惁璐ㄦ楠�", "璐ㄦ");
- putDtoFieldIfPresent(source, target, "type", "鍙拌处绫诲瀷");
- normalizeProductAmounts(target);
- target.putIfAbsent("type", 2);
- return target;
- }
-
- private void copySalesLedgerProductFields(Map<String, Object> source, Map<String, Object> target) {
- String[] productFields = {
- "id", "salesLedgerId", "warnNum", "productCategory", "specificationModel", "unit",
- "speculativeTradingName", "quantity", "minStock", "taxRate", "taxInclusiveUnitPrice",
- "taxInclusiveTotalPrice", "taxExclusiveTotalPrice", "invoiceType", "type", "ticketsNum",
- "ticketsAmount", "futureTickets", "futureTicketsAmount", "invoiceNum", "noInvoiceNum",
- "invoiceAmount", "noInvoiceAmount", "productId", "productModelId", "register", "registerDate",
- "approveStatus", "pendingInvoiceTotal", "invoiceTotal", "pendingTicketsTotal", "ticketsTotal",
- "isChecked", "isProduction"
- };
- for (String field : productFields) {
- if (source.containsKey(field)) {
- target.put(field, source.get(field));
- }
- }
- }
-
- private void normalizeProductAmounts(Map<String, Object> target) {
- BigDecimal quantity = decimalValue(target.get("quantity"));
- BigDecimal unitPrice = decimalValue(target.get("taxInclusiveUnitPrice"));
- BigDecimal totalPrice = decimalValue(target.get("taxInclusiveTotalPrice"));
- if (unitPrice == null && totalPrice != null && quantity != null && quantity.compareTo(BigDecimal.ZERO) != 0) {
- target.put("taxInclusiveUnitPrice", totalPrice.divide(quantity, 6, RoundingMode.HALF_UP));
- }
- if (totalPrice == null && unitPrice != null && quantity != null) {
- target.put("taxInclusiveTotalPrice", unitPrice.multiply(quantity));
- }
- BigDecimal taxRate = decimalValue(target.get("taxRate"));
- totalPrice = decimalValue(target.get("taxInclusiveTotalPrice"));
- if (target.get("taxExclusiveTotalPrice") == null && totalPrice != null && taxRate != null) {
- BigDecimal divisor = BigDecimal.ONE.add(taxRate.divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP));
- target.put("taxExclusiveTotalPrice", totalPrice.divide(divisor, 2, RoundingMode.HALF_UP));
- }
- }
-
- private AjaxResult validatePurchaseProducts(List<SalesLedgerProduct> products, int ledgerIndex) {
- if (products == null || products.isEmpty()) {
- return null;
- }
- for (int i = 0; i < products.size(); i++) {
- SalesLedgerProduct product = products.get(i);
- String prefix = "绗�" + (ledgerIndex + 1) + "涓噰璐彴璐︾殑绗�" + (i + 1) + "鏉′骇鍝�";
- if (!StringUtils.hasText(product.getProductCategory())) {
- return AjaxResult.error(prefix + "缂哄皯浜у搧鍚嶇О锛岃琛ュ厖鍚庡啀纭");
- }
- if (!StringUtils.hasText(product.getSpecificationModel())) {
- return AjaxResult.error(prefix + "缂哄皯瑙勬牸鍨嬪彿锛岃琛ュ厖鍚庡啀纭");
- }
- if (!StringUtils.hasText(product.getUnit())) {
- return AjaxResult.error(prefix + "缂哄皯鍗曚綅锛岃琛ュ厖鍚庡啀纭");
- }
- if (product.getQuantity() == null) {
- return AjaxResult.error(prefix + "缂哄皯鏁伴噺");
- }
- if (product.getTaxInclusiveUnitPrice() == null) {
- return AjaxResult.error(prefix + "缂哄皯鍚◣鍗曚环锛岃琛ュ厖鍚庡啀纭");
- }
- if (product.getTaxInclusiveTotalPrice() == null) {
- return AjaxResult.error(prefix + "缂哄皯鍚◣鎬讳环锛岃琛ュ厖鍚庡啀纭");
- }
- }
- return null;
- }
-
- private AjaxResult validatePurchaseLedger(PurchaseLedgerDto dto, int ledgerIndex) {
- String prefix = "绗�" + (ledgerIndex + 1) + "涓噰璐彴璐�";
- if (!StringUtils.hasText(dto.getPurchaseContractNumber())) {
- return AjaxResult.error(prefix + "缂哄皯閲囪喘鍚堝悓鍙凤紝璇疯ˉ鍏呭悗鍐嶇‘璁�");
- }
- if (dto.getSupplierId() == null && !StringUtils.hasText(dto.getSupplierName())) {
- return AjaxResult.error(prefix + "缂哄皯渚涘簲鍟嗗悕绉帮紝璇疯ˉ鍏呭悗鍐嶇‘璁�");
- }
- return null;
- }
-
- private void normalizePurchaseLedgerDateFields(Map<String, Object> target) {
- normalizeDateField(target, "entryDate");
- normalizeDateField(target, "executionDate");
- normalizeDateField(target, "createdAt");
- normalizeDateField(target, "updatedAt");
- }
-
- private void normalizeDateField(Map<String, Object> target, String fieldName) {
- Object value = target.get(fieldName);
- if (value == null) {
- return;
- }
- String normalizedDate = normalizeDateValue(value);
- if (StringUtils.hasText(normalizedDate)) {
- target.put(fieldName, normalizedDate);
- }
- }
-
- private String normalizeDateValue(Object value) {
- if (value instanceof Date date) {
- return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
- }
- if (value instanceof Number number) {
- return LocalDate.of(1899, 12, 30)
- .plusDays(number.longValue())
- .format(DateTimeFormatter.ISO_LOCAL_DATE);
- }
-
- String text = String.valueOf(value).trim();
- if (!StringUtils.hasText(text)) {
- return null;
- }
- if (text.length() >= 10 && text.charAt(4) == '-' && text.charAt(7) == '-') {
- return text.substring(0, 10);
- }
-
- String normalizedText = text.replace("骞�", "-")
- .replace("鏈�", "-")
- .replace("鏃�", "")
- .replace(".", "-")
- .replace("/", "-")
- .trim();
- DateTimeFormatter[] formatters = {
- DateTimeFormatter.ofPattern("yyyy-M-d"),
- DateTimeFormatter.ofPattern("M-d-yyyy"),
- DateTimeFormatter.ofPattern("M-d-yy")
- };
- for (DateTimeFormatter formatter : formatters) {
- try {
- return LocalDate.parse(normalizedText, formatter).format(DateTimeFormatter.ISO_LOCAL_DATE);
- } catch (DateTimeParseException ignored) {
- // Try the next supported input pattern.
- }
- }
- return text;
- }
-
- private void copyPurchaseLedgerDtoFields(Map<String, Object> source, Map<String, Object> target) {
- String[] dtoFields = {
- "entryDateStart", "entryDateEnd", "id", "purchaseContractNumber",
- "supplierId", "supplierName", "isWhite", "recorderId", "recorderName", "salesContractNo",
- "salesContractNoId", "projectName", "entryDate", "executionDate", "remarks", "attachmentMaterials",
- "createdAt", "updatedAt", "salesLedgerId", "hasChildren", "Type", "productData", "tempFileIds",
- "SalesLedgerFiles", "phoneNumber", "businessPersonId", "productId", "productModelId", "invoiceNumber",
- "invoiceAmount", "ticketRegistrationId", "contractAmount", "receiptPaymentAmount",
- "unReceiptPaymentAmount", "type", "paymentMethod", "approvalStatus", "templateName"
- };
- for (String field : dtoFields) {
- if (source.containsKey(field)) {
- target.put(field, source.get(field));
- }
- }
- }
-
- private void putDtoFieldIfPresent(Map<String, Object> source, Map<String, Object> target, String dtoField, String... aliases) {
- if (target.containsKey(dtoField) && target.get(dtoField) != null) {
- return;
- }
- for (String alias : aliases) {
- Object value = source.get(alias);
- if (value != null && StringUtils.hasText(String.valueOf(value))) {
- target.put(dtoField, value);
- return;
- }
- }
- }
-
- private List<Map<String, Object>> toMapList(Object value) {
- if (value == null) {
- return List.of();
- }
- return objectMapper.convertValue(value, new TypeReference<List<Map<String, Object>>>() {
- });
- }
-
- private String stringValue(Map<String, Object> map, String... keys) {
- for (String key : keys) {
- Object value = map.get(key);
- if (value != null && StringUtils.hasText(String.valueOf(value))) {
- return String.valueOf(value);
- }
- }
- return null;
- }
-
- private Long longValue(Map<String, Object> map, String... keys) {
- String value = stringValue(map, keys);
- if (!StringUtils.hasText(value)) {
- return null;
- }
- try {
- return Long.parseLong(value.trim());
- } catch (NumberFormatException ignored) {
- return null;
- }
- }
-
- private BigDecimal decimalValue(Object value) {
- if (value == null) {
- return null;
- }
- if (value instanceof BigDecimal decimal) {
- return decimal;
- }
- if (value instanceof Number number) {
- return new BigDecimal(String.valueOf(number));
- }
- String text = String.valueOf(value)
- .replace(",", "")
- .replace("锛�", "")
- .replace("鍏�", "")
- .replace("锟�", "")
- .trim();
- if (!StringUtils.hasText(text)) {
- return null;
- }
- try {
- return new BigDecimal(text);
- } catch (NumberFormatException ignored) {
- return null;
- }
- }
-
- private String toCustomerMessage(Exception ex) {
- String message = ex.getMessage();
- if (!StringUtils.hasText(message)) {
- return "澶勭悊澶辫触锛岃妫�鏌ョ‘璁ゆ暟鎹悗閲嶈瘯";
- }
- if (message.contains("tax_inclusive_unit_price")) {
- return "澶勭悊澶辫触锛氫骇鍝佹槑缁嗙己灏戝惈绋庡崟浠凤紝璇疯ˉ鍏呭悗鍐嶇‘璁�";
- }
- if (message.contains("tax_inclusive_total_price")) {
- return "澶勭悊澶辫触锛氫骇鍝佹槑缁嗙己灏戝惈绋庢�讳环锛岃琛ュ厖鍚庡啀纭";
- }
- if (message.contains("entryDate")) {
- return "澶勭悊澶辫触锛氬綍鍏ユ棩鏈熸牸寮忎笉姝g‘锛岃浣跨敤 yyyy-MM-dd锛屼緥濡� 2026-04-30";
- }
- if (message.contains("supplier")) {
- return "澶勭悊澶辫触锛氫緵搴斿晢淇℃伅涓嶅畬鏁达紝璇风‘璁や緵搴斿晢鍚嶇О鎴栦緵搴斿晢ID";
- }
- if (message.contains("SQL") || message.contains("java.") || message.contains("Exception")) {
- return "澶勭悊澶辫触锛氱‘璁ゆ暟鎹笉瀹屾暣鎴栨牸寮忎笉姝g‘锛岃妫�鏌ュ繀濉瓧娈靛悗閲嶈瘯";
- }
- return "澶勭悊澶辫触锛�" + message;
- }
-
- private AjaxResult fillSupplierIdByName(PurchaseLedgerDto dto) {
- if (dto.getSupplierId() != null) {
- return null;
- }
- if (!StringUtils.hasText(dto.getSupplierName())) {
- return AjaxResult.error("渚涘簲鍟咺D涓嶈兘涓虹┖锛涙湭璇嗗埆鍒颁緵搴斿晢鍚嶇О锛屾棤娉曡嚜鍔ㄥ尮閰嶄緵搴斿晢ID");
- }
-
- SupplierManage supplier = supplierManageMapper.selectOne(new LambdaQueryWrapper<SupplierManage>()
- .eq(SupplierManage::getSupplierName, dto.getSupplierName().trim())
- .last("limit 1"));
- if (supplier == null) {
- return AjaxResult.error("鏈壘鍒颁緵搴斿晢锛�" + dto.getSupplierName() + "锛岃鍏堢淮鎶や緵搴斿晢鎴栨墜鍔ㄩ�夋嫨渚涘簲鍟咺D");
- }
- dto.setSupplierId(supplier.getId());
- return null;
- }
-
- private AjaxResult processPaymentRegistration(Map<String, Object> payload) {
- Object recordsValue = payload.get("records");
- List<PaymentRegistration> records;
- if (recordsValue == null) {
- records = Collections.singletonList(objectMapper.convertValue(payload, PaymentRegistration.class));
- } else {
- records = objectMapper.convertValue(recordsValue, new TypeReference<List<PaymentRegistration>>() {
- });
- }
- int result = paymentRegistrationService.insertPaymentRegistration(records);
- return AjaxResult.success("浠樻鐧昏宸插鐞�", result);
- }
-
- private AjaxResult processPurchaseReturnOrder(Map<String, Object> payload) {
- PurchaseReturnOrderDto dto = objectMapper.convertValue(payload, PurchaseReturnOrderDto.class);
- Boolean result = purchaseReturnOrdersService.add(dto);
- return AjaxResult.success("閲囪喘閫�璐у崟宸插鐞�", result);
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ return toAjax(purchaseAiService.deleteSession(memoryId, loginUser));
}
}
diff --git a/src/main/java/com/ruoyi/ai/service/PurchaseAiService.java b/src/main/java/com/ruoyi/ai/service/PurchaseAiService.java
new file mode 100644
index 0000000..0f24d64
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/service/PurchaseAiService.java
@@ -0,0 +1,1031 @@
+package com.ruoyi.ai.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.ai.assistant.PurchaseAgent;
+import com.ruoyi.ai.assistant.PurchaseIntentExecutor;
+import com.ruoyi.ai.bean.ChatForm;
+import com.ruoyi.ai.bean.PurchaseAiConfirmRequest;
+import com.ruoyi.ai.context.AiSessionUserContext;
+import com.ruoyi.ai.dto.AiChatMessageDto;
+import com.ruoyi.ai.dto.AiChatSessionDto;
+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.StringUtils;
+import com.ruoyi.framework.security.LoginUser;
+import com.ruoyi.framework.web.domain.AjaxResult;
+import com.ruoyi.purchase.dto.PurchaseLedgerDto;
+import com.ruoyi.purchase.dto.PurchaseReturnOrderDto;
+import com.ruoyi.purchase.pojo.PaymentRegistration;
+import com.ruoyi.purchase.service.IPaymentRegistrationService;
+import com.ruoyi.purchase.service.IPurchaseLedgerService;
+import com.ruoyi.purchase.service.PurchaseReturnOrdersService;
+import com.ruoyi.sales.pojo.SalesLedgerProduct;
+import dev.langchain4j.data.image.Image;
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.Content;
+import dev.langchain4j.data.message.ImageContent;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.TextContent;
+import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.model.chat.StreamingChatLanguageModel;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+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;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+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;
+
+@Service
+public class PurchaseAiService {
+
+ private static final String PURCHASE_FILE_ANALYZE_MEMORY_PREFIX = "purchase-file-analyze::";
+ private static final int MAX_FILE_COUNT = 10;
+ private static final int MAX_SINGLE_FILE_TEXT_LENGTH = 8000;
+ private static final int MAX_TOTAL_FILE_TEXT_LENGTH = 30000;
+
+ private final PurchaseAgent purchaseAgent;
+ private final PurchaseIntentExecutor purchaseIntentExecutor;
+ private final AiSessionUserContext aiSessionUserContext;
+ private final MongoChatMemoryStore mongoChatMemoryStore;
+ private final AiChatSessionService aiChatSessionService;
+ private final AiFileTextExtractor aiFileTextExtractor;
+ private final ObjectMapper objectMapper;
+ private final IPurchaseLedgerService purchaseLedgerService;
+ private final IPaymentRegistrationService paymentRegistrationService;
+ private final PurchaseReturnOrdersService purchaseReturnOrdersService;
+ private final StorageBlobService storageBlobService;
+ private final SupplierManageMapper supplierManageMapper;
+ private final StreamingChatLanguageModel purchaseVisionStreamingChatModel;
+
+ public PurchaseAiService(PurchaseAgent purchaseAgent,
+ PurchaseIntentExecutor purchaseIntentExecutor,
+ AiSessionUserContext aiSessionUserContext,
+ MongoChatMemoryStore mongoChatMemoryStore,
+ AiChatSessionService aiChatSessionService,
+ AiFileTextExtractor aiFileTextExtractor,
+ 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;
+ this.mongoChatMemoryStore = mongoChatMemoryStore;
+ this.aiChatSessionService = aiChatSessionService;
+ this.aiFileTextExtractor = aiFileTextExtractor;
+ this.objectMapper = objectMapper;
+ this.purchaseLedgerService = purchaseLedgerService;
+ this.paymentRegistrationService = paymentRegistrationService;
+ this.purchaseReturnOrdersService = purchaseReturnOrdersService;
+ this.storageBlobService = storageBlobService;
+ this.supplierManageMapper = supplierManageMapper;
+ this.purchaseVisionStreamingChatModel = purchaseVisionStreamingChatModel;
+ }
+
+ public Flux<String> chat(ChatForm chatForm, LoginUser loginUser) {
+ if (!StringUtils.hasText(chatForm.getMemoryId())) {
+ return Flux.just("memoryId涓嶈兘涓虹┖");
+ }
+ if (!StringUtils.hasText(chatForm.getMessage())) {
+ return Flux.just("message涓嶈兘涓虹┖");
+ }
+
+ String memoryId = chatForm.getMemoryId();
+ String userMessage = chatForm.getMessage();
+
+ aiSessionUserContext.bind(memoryId, loginUser);
+ aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
+
+ String directResponse = purchaseIntentExecutor.tryExecute(memoryId, userMessage);
+ if (StringUtils.isNotEmpty(directResponse)) {
+ mongoChatMemoryStore.appendMessages(
+ memoryId,
+ List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
+ );
+ aiChatSessionService.refreshSessionStats(memoryId, loginUser);
+ return Flux.just(directResponse);
+ }
+
+ return purchaseAgent.chat(memoryId, userMessage)
+ .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
+ .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
+ }
+
+ public Flux<String> analyzeFiles(MultipartFile[] files,
+ String message,
+ String memoryId,
+ LoginUser loginUser) {
+ if (files == null || files.length == 0) {
+ return Flux.just("files涓嶈兘涓虹┖");
+ }
+ if (files.length > MAX_FILE_COUNT) {
+ return Flux.just("涓�娆℃渶澶氬垎鏋�" + MAX_FILE_COUNT + "涓枃浠�");
+ }
+
+ String rawMemoryId = StringUtils.hasText(memoryId) ? memoryId : UUID.randomUUID().toString();
+ String finalMemoryId = rawMemoryId.startsWith(PURCHASE_FILE_ANALYZE_MEMORY_PREFIX)
+ ? rawMemoryId
+ : PURCHASE_FILE_ANALYZE_MEMORY_PREFIX + rawMemoryId;
+
+ aiSessionUserContext.bind(finalMemoryId, loginUser);
+
+ String finalMessage = StringUtils.hasText(message)
+ ? 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);
+ } catch (IllegalArgumentException ex) {
+ return Flux.just(ex.getMessage());
+ } catch (IOException ex) {
+ return Flux.just("鏂囦欢璇诲彇澶辫触");
+ }
+
+ if (!StringUtils.hasText(fileContent)) {
+ return Flux.just("鏈彁鍙栧埌鏈夋晥鏂囦欢鍐呭");
+ }
+
+ String userPrompt = buildPurchaseFileAnalyzePrompt(finalMessage, fileContent);
+ aiChatSessionService.touchSession(finalMemoryId, loginUser, "閲囪喘澶氭枃浠跺垎鏋�: " + finalMessage);
+
+ if (containsImageFile(files)) {
+ return chatWithPurchaseVisionModel(finalMemoryId, finalMessage, userPrompt, files)
+ .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
+ .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
+ }
+
+ return Flux.defer(() -> purchaseAgent.chat(finalMemoryId, userPrompt))
+ .onErrorResume(NoSuchElementException.class, ex -> {
+ mongoChatMemoryStore.deleteMessages(finalMemoryId);
+ return purchaseAgent.chat(finalMemoryId, userPrompt);
+ })
+ .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
+ .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
+ }
+
+ public AjaxResult confirmAnalyzeResult(PurchaseAiConfirmRequest request) {
+ if (request == null || !StringUtils.hasText(request.getBusinessType())) {
+ return AjaxResult.error("businessType涓嶈兘涓虹┖");
+ }
+ if (request.getPayload() == null || request.getPayload().isEmpty()) {
+ return AjaxResult.error("payload涓嶈兘涓虹┖");
+ }
+
+ try {
+ String businessType = request.getBusinessType().trim();
+ return switch (businessType) {
+ case "purchase_ledger" -> processPurchaseLedger(request.getPayload());
+ case "payment_registration" -> processPaymentRegistration(request.getPayload());
+ case "purchase_return_order" -> processPurchaseReturnOrder(request.getPayload());
+ default -> AjaxResult.error("鏆備笉鏀寔璇ヤ笟鍔$被鍨�: " + businessType);
+ };
+ } catch (Exception ex) {
+ return AjaxResult.error(toCustomerMessage(ex));
+ }
+ }
+
+ public List<AiChatSessionDto> listSessions(LoginUser loginUser) {
+ return aiChatSessionService.listCurrentUserSessions(loginUser);
+ }
+
+ public List<AiChatMessageDto> listMessages(String memoryId, LoginUser loginUser) {
+ return aiChatSessionService.listCurrentUserMessages(memoryId, loginUser);
+ }
+
+ public boolean deleteSession(String memoryId, LoginUser loginUser) {
+ aiSessionUserContext.remove(memoryId);
+ return aiChatSessionService.deleteCurrentUserSession(memoryId, loginUser);
+ }
+
+ private String buildMultiFileContent(MultipartFile[] files) throws IOException {
+ StringBuilder builder = new StringBuilder();
+ int totalLength = 0;
+ for (MultipartFile file : files) {
+ String text = aiFileTextExtractor.extractText(file);
+ if (!StringUtils.hasText(text)) {
+ continue;
+ }
+ String limitedText = text.length() > MAX_SINGLE_FILE_TEXT_LENGTH
+ ? text.substring(0, MAX_SINGLE_FILE_TEXT_LENGTH)
+ : text;
+ if (totalLength + limitedText.length() > MAX_TOTAL_FILE_TEXT_LENGTH) {
+ int remain = MAX_TOTAL_FILE_TEXT_LENGTH - totalLength;
+ if (remain <= 0) {
+ break;
+ }
+ limitedText = limitedText.substring(0, remain);
+ }
+ builder.append("\n--- 鏂囦欢: ")
+ .append(file.getOriginalFilename())
+ .append(" ---\n")
+ .append(limitedText)
+ .append('\n');
+ totalLength += limitedText.length();
+ }
+ return builder.toString();
+ }
+
+ private boolean containsImageFile(MultipartFile[] files) {
+ for (MultipartFile file : files) {
+ if (aiFileTextExtractor.isImageFile(file)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ 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));
+ for (MultipartFile file : files) {
+ if (!aiFileTextExtractor.isImageFile(file)) {
+ continue;
+ }
+ contents.add(TextContent.from("涓嬮潰杩欏紶鍥剧墖鏂囦欢鍚嶏細" + file.getOriginalFilename()));
+ contents.add(ImageContent.from(Image.builder()
+ .base64Data(Base64.getEncoder().encodeToString(file.getBytes()))
+ .mimeType(resolveImageMimeType(file))
+ .build()));
+ }
+
+ List<ChatMessage> messages = List.of(
+ 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();
+ }
+
+ @Override
+ public void onError(Throwable error) {
+ sink.error(error);
+ }
+ });
+ } catch (Exception ex) {
+ sink.next("鍥剧墖鏂囦欢璇诲彇澶辫触锛岃纭鍥剧墖鏍煎紡涓� png銆乯pg銆乯peg銆亀ebp 鎴� bmp锛屼笖澶у皬涓嶈秴杩�10MB");
+ sink.complete();
+ }
+ });
+ }
+
+ 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/")) {
+ return contentType;
+ }
+ String filename = file.getOriginalFilename();
+ String ext = "";
+ if (StringUtils.hasText(filename) && filename.contains(".")) {
+ ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
+ }
+ return switch (ext) {
+ case "jpg", "jpeg" -> "image/jpeg";
+ case "webp" -> "image/webp";
+ case "bmp" -> "image/bmp";
+ default -> "image/png";
+ };
+ }
+
+ private String buildPurchaseFileAnalyzePrompt(String message, String fileContent) {
+ return """
+ 浣犳槸閲囪喘涓氬姟鏂囦欢鍒嗘瀽鍔╂墜銆傝涓ユ牸鏍规嵁鐢ㄦ埛涓婁紶鐨勫涓枃浠跺拰鐢ㄦ埛瑕佹眰鎻愬彇閲囪喘涓氬姟鏁版嵁銆�
+
+ 鐢ㄦ埛瑕佹眰:
+ %s
+
+ 杈撳嚭瑕佹眰:
+ 1. 鍙緭鍑哄悎娉� JSON锛屼笉瑕� Markdown锛屼笉瑕侀澶栬В閲娿��
+ 2. JSON 椤跺眰瀛楁鍥哄畾涓�:
+ - success: boolean
+ - businessType: purchase_ledger | payment_registration | purchase_return_order | unknown
+ - action: confirm_required
+ - description: 涓枃璇存槑
+ - confidence: 0鍒�1鐨勫皬鏁�
+ - missingFields: 缂哄け瀛楁涓枃鍚嶇О鏁扮粍锛岄潰鍚戝鎴峰睍绀猴紝涓嶈杈撳嚭鑻辨枃瀛楁鍚�
+ - warnings: 椋庨櫓鎻愮ず鏁扮粍
+ - payload: 寰呭鎴风‘璁ょ殑鏁版嵁锛屽瓧娈靛悕蹇呴』浣跨敤鍚庣 DTO 瀛楁鍚�
+ - preview: 缁欏鎴风‘璁ょ敤鐨勪腑鏂囨憳瑕佹暟缁�
+ 3. 濡傛灉鍙垽鏂负閲囪喘鍙拌处锛宐usinessType 浣跨敤 purchase_ledger锛宲ayload.purchaseLedgers 涓洪噰璐鍗�/閲囪喘鍙拌处鏁扮粍:
+ - purchaseLedgers: 閲囪喘璁㈠崟/閲囪喘鍙拌处鏁扮粍锛屾瘡鏉¤褰曞瓧娈靛悕蹇呴』涓� PurchaseLedgerDto 淇濇寔涓�鑷�
+ - 浜у搧鏄庣粏蹇呴』鏀惧湪姣忔潯閲囪喘鍙拌处璁板綍鐨� productData 瀛楁涓紝productData 绫诲瀷涓� List<SalesLedgerProduct>
+ - 涓嶈浼樺厛浣跨敤 payload 椤跺眰 productData锛涢《灞� productData 浠呬綔涓烘棫鏍煎紡鍏煎
+ - 鏂囦欢閲岀殑鈥滈噰璐崟鍙封�濆氨鏄�滈噰璐悎鍚屽彿鈥濓紝缁熶竴鏄犲皠涓� purchaseContractNumber
+ - 鏂囦欢閲岀殑鈥滈攢鍞崟鍙封�濆氨鏄�滈攢鍞悎鍚屽彿鈥濓紝缁熶竴鏄犲皠涓� salesContractNo
+ - 鎵�鏈夋棩鏈熷瓧娈靛繀椤讳娇鐢� yyyy-MM-dd锛屼緥濡� 2026-04-30锛涗笉瑕佽緭鍑� 4/30/26銆�2026/4/30銆�2026骞�4鏈�30鏃� 鎴栧甫鏃跺垎绉掔殑鏍煎紡
+ - 閲囪喘鍙拌处涓嶉渶瑕佸湪 payload 涓紶瀹℃壒浜猴紝涓嶈杈撳嚭 approveUserIds銆乤pproverId
+ - missingFields 鍙~鍐欎笟鍔″繀濉絾鏃犳硶璇嗗埆鐨勫瓧娈碉紝涓嶈鎶� PurchaseLedgerDto 鐨勬墍鏈夌┖瀛楁閮藉垪涓虹己澶憋紱缂哄け椤瑰繀椤诲啓涓枃锛屼緥濡傗�滀緵搴斿晢鍚嶇О鈥濃�滃惈绋庡崟浠封�濓紝涓嶈鍐� supplierId銆乼axInclusiveUnitPrice
+ - 閲囪喘鍙拌处涓昏〃蹇呭~瀛楁浠呮寜杩欎簺鍒ゆ柇: purchaseContractNumber銆乻upplierName 鎴� supplierId
+ - productData 姣忔潯浜у搧蹇呭~瀛楁: productCategory銆乻pecificationModel銆乽nit銆乹uantity銆乼axInclusiveUnitPrice 鎴� taxInclusiveTotalPrice锛涘鏋滃彧鏈夊惈绋庢�讳环鍜屾暟閲忥紝蹇呴』璁$畻 taxInclusiveUnitPrice锛涘鏋滃彧鏈夊惈绋庡崟浠峰拰鏁伴噺锛屽繀椤昏绠� taxInclusiveTotalPrice
+ - 浜у搧瀛楁鎸夐噰璐鍏ユ帴鍙� PurchaseLedgerProductImportDto 瀵归綈: 閲囪喘鍗曞彿銆佷骇鍝佸ぇ绫汇�佽鏍煎瀷鍙枫�佸崟浣嶃�佹暟閲忋�佺◣鐜囥�佸惈绋庡崟浠枫�佸惈绋庢�讳环銆佸彂绁ㄧ被鍨嬨�佹槸鍚﹁川妫�
+ - 閲囪喘浜у搧 type 鍥哄畾涓� 2
+ - purchaseLedgers 姣忔潯璁板綍鍙娇鐢ㄨ繖浜� PurchaseLedgerDto 瀛楁鍚�:
+ entryDateStart, entryDateEnd, id, purchaseContractNumber, supplierId, supplierName, isWhite, recorderId, recorderName, salesContractNo, salesContractNoId, projectName, entryDate, executionDate, remarks, attachmentMaterials, createdAt, updatedAt, salesLedgerId, hasChildren, Type, productData, tempFileIds, SalesLedgerFiles, phoneNumber, businessPersonId, productId, productModelId, invoiceNumber, invoiceAmount, ticketRegistrationId, contractAmount, receiptPaymentAmount, unReceiptPaymentAmount, type, paymentMethod, approvalStatus, templateName
+ - productData 姣忔潯浜у搧鍙娇鐢ㄨ繖浜� SalesLedgerProduct 瀛楁鍚�:
+ productCategory, specificationModel, unit, quantity, taxRate, taxInclusiveUnitPrice, taxInclusiveTotalPrice, taxExclusiveTotalPrice, invoiceType, productId, productModelId, isChecked, type
+ 4. 濡傛灉鍙垽鏂负浠樻鐧昏锛宐usinessType 浣跨敤 payment_registration锛宲ayload.records 涓轰粯娆剧櫥璁版暟缁勶紝瀛楁灏介噺鍖呭惈 purchaseLedgerId銆乻alesLedgerProductId銆乧urrentPaymentAmount銆乸aymentMethod銆乸aymentDate銆�
+ 5. 濡傛灉鍙垽鏂负閲囪喘閫�璐э紝businessType 浣跨敤 purchase_return_order锛宲ayload 鎸� PurchaseReturnOrderDto 缁勭粐锛屾槑缁嗘斁 purchaseReturnOrderProductsDtos銆�
+ 6. 缂哄皯涓氬姟澶勭悊蹇呴』瀛楁鏃讹紝涓嶈缂栭�� ID锛屾妸瀛楁鏀惧叆 missingFields锛屽苟浠嶈繑鍥炲彲纭鐨勮崏绋挎暟鎹��
+ 7. 鎵�鏈変腑鏂囧唴瀹圭洿鎺ヤ繚鐣欙紝涓嶈杞箟鎴� Unicode銆�
+
+ 鏂囦欢鍐呭:
+ %s
+ """.formatted(message, fileContent);
+ }
+
+ private AjaxResult processPurchaseLedger(Map<String, Object> payload) throws Exception {
+ if (payload.containsKey("purchaseLedgers")) {
+ return processPurchaseLedgerBatch(payload);
+ }
+
+ Map<String, Object> normalizedPayload = normalizePurchaseLedgerMap(payload);
+ PurchaseLedgerDto dto = objectMapper.convertValue(normalizedPayload, PurchaseLedgerDto.class);
+ AjaxResult ledgerResult = validatePurchaseLedger(dto, 0);
+ if (ledgerResult != null) {
+ return ledgerResult;
+ }
+ AjaxResult supplierResult = fillSupplierIdByName(dto);
+ if (supplierResult != null) {
+ return supplierResult;
+ }
+ AjaxResult productResult = validatePurchaseProducts(dto.getProductData(), 0);
+ if (productResult != null) {
+ return productResult;
+ }
+ int result = purchaseLedgerService.addOrEditPurchase(dto);
+ return AjaxResult.success("閲囪喘鍙拌处宸插鐞�", result);
+ }
+
+ private AjaxResult processPurchaseLedgerBatch(Map<String, Object> payload) throws Exception {
+ List<Map<String, Object>> purchaseLedgers = toMapList(payload.get("purchaseLedgers"));
+ if (purchaseLedgers.isEmpty()) {
+ return AjaxResult.error("purchaseLedgers涓嶈兘涓虹┖");
+ }
+
+ List<Map<String, Object>> topLevelProductData = toMapList(payload.get("productData"));
+ List<Map<String, Object>> results = new ArrayList<>();
+ for (int i = 0; i < purchaseLedgers.size(); i++) {
+ Map<String, Object> ledgerMap = normalizePurchaseLedgerMap(purchaseLedgers.get(i));
+ PurchaseLedgerDto dto = objectMapper.convertValue(ledgerMap, PurchaseLedgerDto.class);
+ AjaxResult ledgerResult = validatePurchaseLedger(dto, i);
+ if (ledgerResult != null) {
+ return ledgerResult;
+ }
+ AjaxResult supplierResult = fillSupplierIdByName(dto);
+ if (supplierResult != null) {
+ return supplierResult;
+ }
+
+ List<SalesLedgerProduct> products = dto.getProductData();
+ if (products == null || products.isEmpty()) {
+ products = matchProductsForLedger(ledgerMap, dto, topLevelProductData, purchaseLedgers.size() == 1);
+ dto.setProductData(products);
+ }
+ AjaxResult productResult = validatePurchaseProducts(products, i);
+ if (productResult != null) {
+ return productResult;
+ }
+ int result = purchaseLedgerService.addOrEditPurchase(dto);
+
+ Map<String, Object> item = new LinkedHashMap<>();
+ item.put("index", i);
+ item.put("purchaseContractNumber", dto.getPurchaseContractNumber());
+ item.put("supplierId", dto.getSupplierId());
+ item.put("supplierName", dto.getSupplierName());
+ item.put("productCount", products.size());
+ item.put("result", result);
+ results.add(item);
+ }
+ return AjaxResult.success("閲囪喘鍙拌处宸叉壒閲忓鐞�", results);
+ }
+
+ private List<SalesLedgerProduct> matchProductsForLedger(Map<String, Object> ledgerMap,
+ PurchaseLedgerDto dto,
+ List<Map<String, Object>> productData,
+ boolean onlyOneLedger) {
+ List<SalesLedgerProduct> products = new ArrayList<>();
+ for (Map<String, Object> productMap : productData) {
+ if (onlyOneLedger || productBelongsToLedger(productMap, ledgerMap, dto)) {
+ products.add(objectMapper.convertValue(normalizeSalesLedgerProductMap(productMap), SalesLedgerProduct.class));
+ }
+ }
+ return products;
+ }
+
+ private boolean productBelongsToLedger(Map<String, Object> productMap, Map<String, Object> ledgerMap, PurchaseLedgerDto dto) {
+ Long productPurchaseLedgerId = longValue(productMap, "purchaseLedgerId", "purchaseId", "閲囪喘璁㈠崟id", "閲囪喘鍙拌处id");
+ if (productPurchaseLedgerId != null && dto.getId() != null && productPurchaseLedgerId.equals(dto.getId())) {
+ return true;
+ }
+
+ Long productSalesLedgerId = longValue(productMap, "salesLedgerId");
+ if (productSalesLedgerId != null && dto.getId() != null && productSalesLedgerId.equals(dto.getId())) {
+ return true;
+ }
+
+ String productContractNo = stringValue(productMap, "purchaseContractNumber", "purchaseContractNo", "閲囪喘鍚堝悓鍙�", "閲囪喘鍗曞彿", "閲囪喘璁㈠崟鍙�");
+ if (StringUtils.hasText(productContractNo)
+ && StringUtils.hasText(dto.getPurchaseContractNumber())
+ && productContractNo.trim().equals(dto.getPurchaseContractNumber().trim())) {
+ return true;
+ }
+
+ String ledgerContractNo = stringValue(ledgerMap, "purchaseContractNumber", "purchaseContractNo", "閲囪喘鍚堝悓鍙�", "閲囪喘鍗曞彿", "閲囪喘璁㈠崟鍙�");
+ if (StringUtils.hasText(productContractNo)
+ && StringUtils.hasText(ledgerContractNo)
+ && productContractNo.trim().equals(ledgerContractNo.trim())) {
+ return true;
+ }
+
+ String productSalesContractNo = stringValue(productMap, "salesContractNo", "salesContractNumber", "閿�鍞悎鍚屽彿", "閿�鍞崟鍙�", "閿�鍞鍗曞彿");
+ if (StringUtils.hasText(productSalesContractNo)
+ && StringUtils.hasText(dto.getSalesContractNo())
+ && productSalesContractNo.trim().equals(dto.getSalesContractNo().trim())) {
+ return true;
+ }
+
+ String ledgerSalesContractNo = stringValue(ledgerMap, "salesContractNo", "salesContractNumber", "閿�鍞悎鍚屽彿", "閿�鍞崟鍙�", "閿�鍞鍗曞彿");
+ if (StringUtils.hasText(productSalesContractNo)
+ && StringUtils.hasText(ledgerSalesContractNo)
+ && productSalesContractNo.trim().equals(ledgerSalesContractNo.trim())) {
+ return true;
+ }
+
+ String productSupplierName = stringValue(productMap, "supplierName", "渚涘簲鍟嗗悕绉�");
+ return StringUtils.hasText(productSupplierName)
+ && StringUtils.hasText(dto.getSupplierName())
+ && productSupplierName.trim().equals(dto.getSupplierName().trim());
+ }
+
+ private Map<String, Object> normalizePurchaseLedgerMap(Map<String, Object> source) {
+ Map<String, Object> target = new LinkedHashMap<>();
+ copyPurchaseLedgerDtoFields(source, target);
+ putDtoFieldIfPresent(source, target, "entryDateStart", "褰曞叆寮�濮嬫棩鏈�", "褰曞叆鏃ユ湡寮�濮�");
+ putDtoFieldIfPresent(source, target, "entryDateEnd", "褰曞叆缁撴潫鏃ユ湡", "褰曞叆鏃ユ湡缁撴潫");
+ putDtoFieldIfPresent(source, target, "id", "閲囪喘鍙拌处id", "閲囪喘璁㈠崟id", "涓婚敭");
+ putDtoFieldIfPresent(source, target, "purchaseContractNumber", "purchaseContractNo", "閲囪喘鍚堝悓鍙�", "閲囪喘鍗曞彿", "閲囪喘璁㈠崟鍙�");
+ putDtoFieldIfPresent(source, target, "supplierId", "渚涘簲鍟唅d", "渚涘簲鍟咺D", "渚涘簲鍟嗗悕绉癷d", "渚涘簲鍟嗗悕绉癐D");
+ putDtoFieldIfPresent(source, target, "supplierName", "渚涘簲鍟�", "渚涘簲鍟嗗悕绉�");
+ putDtoFieldIfPresent(source, target, "isWhite", "鏄惁鐧藉悕鍗�");
+ putDtoFieldIfPresent(source, target, "recorderId", "褰曞叆浜篿d", "褰曞叆浜篒D", "褰曞叆浜哄鍚峣d", "褰曞叆浜哄鍚岻D");
+ putDtoFieldIfPresent(source, target, "recorderName", "褰曞叆浜�", "褰曞叆浜哄鍚�");
+ putDtoFieldIfPresent(source, target, "salesContractNo", "salesContractNumber", "閿�鍞悎鍚屽彿", "閿�鍞崟鍙�", "閿�鍞鍗曞彿");
+ putDtoFieldIfPresent(source, target, "salesContractNoId", "閿�鍞悎鍚屽彿id", "閿�鍞悎鍚屽彿ID", "閿�鍞崟鍙穒d", "閿�鍞崟鍙稩D");
+ putDtoFieldIfPresent(source, target, "projectName", "椤圭洰", "椤圭洰鍚嶇О");
+ putDtoFieldIfPresent(source, target, "entryDate", "褰曞叆鏃ユ湡");
+ putDtoFieldIfPresent(source, target, "executionDate", "绛捐鏃ユ湡", "鍚堝悓绛捐鏃ユ湡");
+ putDtoFieldIfPresent(source, target, "remarks", "澶囨敞", "璇存槑");
+ putDtoFieldIfPresent(source, target, "attachmentMaterials", "闄勪欢鏉愭枡", "闄勪欢鏉愭枡璺緞鎴栧悕绉�");
+ putDtoFieldIfPresent(source, target, "createdAt", "鍒涘缓鏃堕棿", "璁板綍鍒涘缓鏃堕棿");
+ putDtoFieldIfPresent(source, target, "updatedAt", "鏇存柊鏃堕棿", "璁板綍鏈�鍚庢洿鏂版椂闂�");
+ putDtoFieldIfPresent(source, target, "salesLedgerId", "閿�鍞彴璐d", "閿�鍞彴璐D", "鍏宠仈閿�鍞彴璐︿富琛ㄤ富閿�");
+ putDtoFieldIfPresent(source, target, "hasChildren", "鏄惁鏈夊瓙绾�", "鏄惁鏈夋槑缁�");
+ putDtoFieldIfPresent(source, target, "Type", "鍙拌处绫诲瀷", "涓氬姟绫诲瀷");
+ putDtoFieldIfPresent(source, target, "productData", "products", "浜у搧鏄庣粏", "閲囪喘浜у搧鏄庣粏");
+ putDtoFieldIfPresent(source, target, "tempFileIds", "涓存椂鏂囦欢id", "涓存椂鏂囦欢ID", "涓存椂鏂囦欢ids");
+ putDtoFieldIfPresent(source, target, "SalesLedgerFiles", "闄勪欢鍒楄〃", "閿�鍞彴璐﹂檮浠�");
+ putDtoFieldIfPresent(source, target, "phoneNumber", "涓氬姟鍛樻墜鏈哄彿", "鎵嬫満鍙�");
+ putDtoFieldIfPresent(source, target, "businessPersonId", "涓氬姟鍛榠d", "涓氬姟鍛業D");
+ putDtoFieldIfPresent(source, target, "productId", "浜у搧id", "浜у搧ID");
+ putDtoFieldIfPresent(source, target, "productModelId", "浜у搧瑙勬牸id", "浜у搧瑙勬牸ID");
+ putDtoFieldIfPresent(source, target, "invoiceNumber", "鍙戠エ鍙�", "鍙戠エ鍙风爜");
+ putDtoFieldIfPresent(source, target, "invoiceAmount", "鍙戠エ閲戦", "鍙戠エ閲戦锛堝厓锛�");
+ putDtoFieldIfPresent(source, target, "ticketRegistrationId", "鏉ョエ鐧昏id", "鏉ョエ鐧昏ID");
+ putDtoFieldIfPresent(source, target, "contractAmount", "鍚堝悓閲戦", "鍚堝悓閲戦锛堜骇鍝佸惈绋庢�讳环锛�");
+ putDtoFieldIfPresent(source, target, "receiptPaymentAmount", "鏉ョエ閲戦", "宸叉潵绁ㄩ噾棰�", "宸叉潵绁ㄩ噾棰�(鍏�)");
+ putDtoFieldIfPresent(source, target, "unReceiptPaymentAmount", "鏈潵绁ㄩ噾棰�", "鏈潵绁ㄩ噾棰�(鍏�)");
+ putDtoFieldIfPresent(source, target, "type", "鏂囦欢绫诲瀷");
+ putDtoFieldIfPresent(source, target, "paymentMethod", "浠樻鏂瑰紡");
+ putDtoFieldIfPresent(source, target, "approvalStatus", "瀹℃壒鐘舵��");
+ putDtoFieldIfPresent(source, target, "templateName", "妯℃澘鍚嶇О");
+ target.remove("approveUserIds");
+ target.remove("approverId");
+ normalizeNestedProductData(target);
+ attachImportStyleProductData(source, target);
+ if (target.get("type") == null) {
+ target.put("type", 2);
+ }
+ target.putIfAbsent("entryDate", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
+ normalizePurchaseLedgerDateFields(target);
+ return target;
+ }
+
+ private void attachImportStyleProductData(Map<String, Object> source, Map<String, Object> target) {
+ if (target.get("productData") != null) {
+ return;
+ }
+ Map<String, Object> productMap = normalizeSalesLedgerProductMap(source);
+ if (hasImportStyleProductData(productMap)) {
+ target.put("productData", List.of(productMap));
+ }
+ }
+
+ private boolean hasImportStyleProductData(Map<String, Object> productMap) {
+ return hasMapText(productMap, "productCategory")
+ || hasMapText(productMap, "specificationModel")
+ || productMap.get("quantity") != null
+ || productMap.get("taxInclusiveUnitPrice") != null
+ || productMap.get("taxInclusiveTotalPrice") != null;
+ }
+
+ private boolean hasMapText(Map<String, Object> map, String key) {
+ Object value = map.get(key);
+ return value != null && StringUtils.hasText(String.valueOf(value));
+ }
+
+ private void normalizeNestedProductData(Map<String, Object> target) {
+ Object productDataValue = target.get("productData");
+ if (productDataValue == null) {
+ return;
+ }
+ List<Map<String, Object>> productMaps = toMapList(productDataValue);
+ List<Map<String, Object>> normalizedProducts = new ArrayList<>();
+ for (Map<String, Object> productMap : productMaps) {
+ normalizedProducts.add(normalizeSalesLedgerProductMap(productMap));
+ }
+ target.put("productData", normalizedProducts);
+ }
+
+ private Map<String, Object> normalizeSalesLedgerProductMap(Map<String, Object> source) {
+ Map<String, Object> target = new LinkedHashMap<>();
+ copySalesLedgerProductFields(source, target);
+ putDtoFieldIfPresent(source, target, "productCategory", "浜у搧澶х被", "浜у搧鍚嶇О", "浜у搧", "鍝佸悕", "鐗╂枡鍚嶇О");
+ putDtoFieldIfPresent(source, target, "specificationModel", "瑙勬牸鍨嬪彿", "鍨嬪彿", "瑙勬牸", "浜у搧瑙勬牸");
+ putDtoFieldIfPresent(source, target, "unit", "鍗曚綅");
+ putDtoFieldIfPresent(source, target, "quantity", "鏁伴噺", "閲囪喘鏁伴噺");
+ putDtoFieldIfPresent(source, target, "taxRate", "绋庣巼");
+ putDtoFieldIfPresent(source, target, "taxInclusiveUnitPrice", "鍚◣鍗曚环", "鍗曚环", "閲囪喘鍗曚环", "鍚◣浠锋牸");
+ putDtoFieldIfPresent(source, target, "taxInclusiveTotalPrice", "鍚◣鎬讳环", "鎬讳环", "閲囪喘閲戦", "閲戦", "鍚堝悓閲戦");
+ putDtoFieldIfPresent(source, target, "taxExclusiveTotalPrice", "涓嶅惈绋庢�讳环");
+ putDtoFieldIfPresent(source, target, "invoiceType", "鍙戠エ绫诲瀷", "鍙戠エ绫诲埆");
+ putDtoFieldIfPresent(source, target, "productId", "浜у搧id", "浜у搧ID");
+ putDtoFieldIfPresent(source, target, "productModelId", "浜у搧瑙勬牸id", "浜у搧瑙勬牸ID", "鍨嬪彿id", "鍨嬪彿ID");
+ putDtoFieldIfPresent(source, target, "isChecked", "鏄惁璐ㄦ", "鏄惁璐ㄦ楠�", "璐ㄦ");
+ putDtoFieldIfPresent(source, target, "type", "鍙拌处绫诲瀷");
+ normalizeProductAmounts(target);
+ target.putIfAbsent("type", 2);
+ return target;
+ }
+
+ private void copySalesLedgerProductFields(Map<String, Object> source, Map<String, Object> target) {
+ String[] productFields = {
+ "id", "salesLedgerId", "warnNum", "productCategory", "specificationModel", "unit",
+ "speculativeTradingName", "quantity", "minStock", "taxRate", "taxInclusiveUnitPrice",
+ "taxInclusiveTotalPrice", "taxExclusiveTotalPrice", "invoiceType", "type", "ticketsNum",
+ "ticketsAmount", "futureTickets", "futureTicketsAmount", "invoiceNum", "noInvoiceNum",
+ "invoiceAmount", "noInvoiceAmount", "productId", "productModelId", "register", "registerDate",
+ "approveStatus", "pendingInvoiceTotal", "invoiceTotal", "pendingTicketsTotal", "ticketsTotal",
+ "isChecked", "isProduction"
+ };
+ for (String field : productFields) {
+ if (source.containsKey(field)) {
+ target.put(field, source.get(field));
+ }
+ }
+ }
+
+ private void normalizeProductAmounts(Map<String, Object> target) {
+ BigDecimal quantity = decimalValue(target.get("quantity"));
+ BigDecimal unitPrice = decimalValue(target.get("taxInclusiveUnitPrice"));
+ BigDecimal totalPrice = decimalValue(target.get("taxInclusiveTotalPrice"));
+ if (unitPrice == null && totalPrice != null && quantity != null && quantity.compareTo(BigDecimal.ZERO) != 0) {
+ target.put("taxInclusiveUnitPrice", totalPrice.divide(quantity, 6, RoundingMode.HALF_UP));
+ }
+ if (totalPrice == null && unitPrice != null && quantity != null) {
+ target.put("taxInclusiveTotalPrice", unitPrice.multiply(quantity));
+ }
+ BigDecimal taxRate = decimalValue(target.get("taxRate"));
+ totalPrice = decimalValue(target.get("taxInclusiveTotalPrice"));
+ if (target.get("taxExclusiveTotalPrice") == null && totalPrice != null && taxRate != null) {
+ BigDecimal divisor = BigDecimal.ONE.add(taxRate.divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP));
+ target.put("taxExclusiveTotalPrice", totalPrice.divide(divisor, 2, RoundingMode.HALF_UP));
+ }
+ }
+
+ private AjaxResult validatePurchaseProducts(List<SalesLedgerProduct> products, int ledgerIndex) {
+ if (products == null || products.isEmpty()) {
+ return null;
+ }
+ for (int i = 0; i < products.size(); i++) {
+ SalesLedgerProduct product = products.get(i);
+ String prefix = "绗�" + (ledgerIndex + 1) + "涓噰璐彴璐︾殑绗�" + (i + 1) + "鏉′骇鍝�";
+ if (!StringUtils.hasText(product.getProductCategory())) {
+ return AjaxResult.error(prefix + "缂哄皯浜у搧鍚嶇О锛岃琛ュ厖鍚庡啀纭");
+ }
+ if (!StringUtils.hasText(product.getSpecificationModel())) {
+ return AjaxResult.error(prefix + "缂哄皯瑙勬牸鍨嬪彿锛岃琛ュ厖鍚庡啀纭");
+ }
+ if (!StringUtils.hasText(product.getUnit())) {
+ return AjaxResult.error(prefix + "缂哄皯鍗曚綅锛岃琛ュ厖鍚庡啀纭");
+ }
+ if (product.getQuantity() == null) {
+ return AjaxResult.error(prefix + "缂哄皯鏁伴噺");
+ }
+ if (product.getTaxInclusiveUnitPrice() == null) {
+ return AjaxResult.error(prefix + "缂哄皯鍚◣鍗曚环锛岃琛ュ厖鍚庡啀纭");
+ }
+ if (product.getTaxInclusiveTotalPrice() == null) {
+ return AjaxResult.error(prefix + "缂哄皯鍚◣鎬讳环锛岃琛ュ厖鍚庡啀纭");
+ }
+ }
+ return null;
+ }
+
+ private AjaxResult validatePurchaseLedger(PurchaseLedgerDto dto, int ledgerIndex) {
+ String prefix = "绗�" + (ledgerIndex + 1) + "涓噰璐彴璐�";
+ if (!StringUtils.hasText(dto.getPurchaseContractNumber())) {
+ return AjaxResult.error(prefix + "缂哄皯閲囪喘鍚堝悓鍙凤紝璇疯ˉ鍏呭悗鍐嶇‘璁�");
+ }
+ if (dto.getSupplierId() == null && !StringUtils.hasText(dto.getSupplierName())) {
+ return AjaxResult.error(prefix + "缂哄皯渚涘簲鍟嗗悕绉帮紝璇疯ˉ鍏呭悗鍐嶇‘璁�");
+ }
+ return null;
+ }
+
+ private void normalizePurchaseLedgerDateFields(Map<String, Object> target) {
+ normalizeDateField(target, "entryDate");
+ normalizeDateField(target, "executionDate");
+ normalizeDateField(target, "createdAt");
+ normalizeDateField(target, "updatedAt");
+ }
+
+ private void normalizeDateField(Map<String, Object> target, String fieldName) {
+ Object value = target.get(fieldName);
+ if (value == null) {
+ return;
+ }
+ String normalizedDate = normalizeDateValue(value);
+ if (StringUtils.hasText(normalizedDate)) {
+ target.put(fieldName, normalizedDate);
+ }
+ }
+
+ private String normalizeDateValue(Object value) {
+ if (value instanceof Date date) {
+ return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
+ }
+ if (value instanceof Number number) {
+ return LocalDate.of(1899, 12, 30)
+ .plusDays(number.longValue())
+ .format(DateTimeFormatter.ISO_LOCAL_DATE);
+ }
+
+ String text = String.valueOf(value).trim();
+ if (!StringUtils.hasText(text)) {
+ return null;
+ }
+ if (text.length() >= 10 && text.charAt(4) == '-' && text.charAt(7) == '-') {
+ return text.substring(0, 10);
+ }
+
+ String normalizedText = text.replace("骞�", "-")
+ .replace("鏈�", "-")
+ .replace("鏃�", "")
+ .replace(".", "-")
+ .replace("/", "-")
+ .trim();
+ DateTimeFormatter[] formatters = {
+ DateTimeFormatter.ofPattern("yyyy-M-d"),
+ DateTimeFormatter.ofPattern("M-d-yyyy"),
+ DateTimeFormatter.ofPattern("M-d-yy")
+ };
+ for (DateTimeFormatter formatter : formatters) {
+ try {
+ return LocalDate.parse(normalizedText, formatter).format(DateTimeFormatter.ISO_LOCAL_DATE);
+ } catch (DateTimeParseException ignored) {
+ // Try the next supported input pattern.
+ }
+ }
+ return text;
+ }
+
+ private void copyPurchaseLedgerDtoFields(Map<String, Object> source, Map<String, Object> target) {
+ String[] dtoFields = {
+ "entryDateStart", "entryDateEnd", "id", "purchaseContractNumber",
+ "supplierId", "supplierName", "isWhite", "recorderId", "recorderName", "salesContractNo",
+ "salesContractNoId", "projectName", "entryDate", "executionDate", "remarks", "attachmentMaterials",
+ "createdAt", "updatedAt", "salesLedgerId", "hasChildren", "Type", "productData", "tempFileIds",
+ "SalesLedgerFiles", "phoneNumber", "businessPersonId", "productId", "productModelId", "invoiceNumber",
+ "invoiceAmount", "ticketRegistrationId", "contractAmount", "receiptPaymentAmount",
+ "unReceiptPaymentAmount", "type", "paymentMethod", "approvalStatus", "templateName"
+ };
+ for (String field : dtoFields) {
+ if (source.containsKey(field)) {
+ target.put(field, source.get(field));
+ }
+ }
+ }
+
+ private void putDtoFieldIfPresent(Map<String, Object> source, Map<String, Object> target, String dtoField, String... aliases) {
+ if (target.containsKey(dtoField) && target.get(dtoField) != null) {
+ return;
+ }
+ for (String alias : aliases) {
+ Object value = source.get(alias);
+ if (value != null && StringUtils.hasText(String.valueOf(value))) {
+ target.put(dtoField, value);
+ return;
+ }
+ }
+ }
+
+ private List<Map<String, Object>> toMapList(Object value) {
+ if (value == null) {
+ return List.of();
+ }
+ return objectMapper.convertValue(value, new TypeReference<List<Map<String, Object>>>() {
+ });
+ }
+
+ private String stringValue(Map<String, Object> map, String... keys) {
+ for (String key : keys) {
+ Object value = map.get(key);
+ if (value != null && StringUtils.hasText(String.valueOf(value))) {
+ return String.valueOf(value);
+ }
+ }
+ return null;
+ }
+
+ private Long longValue(Map<String, Object> map, String... keys) {
+ String value = stringValue(map, keys);
+ if (!StringUtils.hasText(value)) {
+ return null;
+ }
+ try {
+ return Long.parseLong(value.trim());
+ } catch (NumberFormatException ignored) {
+ return null;
+ }
+ }
+
+ private BigDecimal decimalValue(Object value) {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof BigDecimal decimal) {
+ return decimal;
+ }
+ if (value instanceof Number number) {
+ return new BigDecimal(String.valueOf(number));
+ }
+ String text = String.valueOf(value)
+ .replace(",", "")
+ .replace("锛�", "")
+ .replace("鍏�", "")
+ .replace("锟�", "")
+ .trim();
+ if (!StringUtils.hasText(text)) {
+ return null;
+ }
+ try {
+ return new BigDecimal(text);
+ } catch (NumberFormatException ignored) {
+ return null;
+ }
+ }
+
+ private String toCustomerMessage(Exception ex) {
+ String message = ex.getMessage();
+ if (!StringUtils.hasText(message)) {
+ return "澶勭悊澶辫触锛岃妫�鏌ョ‘璁ゆ暟鎹悗閲嶈瘯";
+ }
+ if (message.contains("tax_inclusive_unit_price")) {
+ return "澶勭悊澶辫触锛氫骇鍝佹槑缁嗙己灏戝惈绋庡崟浠凤紝璇疯ˉ鍏呭悗鍐嶇‘璁�";
+ }
+ if (message.contains("tax_inclusive_total_price")) {
+ return "澶勭悊澶辫触锛氫骇鍝佹槑缁嗙己灏戝惈绋庢�讳环锛岃琛ュ厖鍚庡啀纭";
+ }
+ if (message.contains("entryDate")) {
+ return "澶勭悊澶辫触锛氬綍鍏ユ棩鏈熸牸寮忎笉姝g‘锛岃浣跨敤 yyyy-MM-dd锛屼緥濡� 2026-04-30";
+ }
+ if (message.contains("supplier")) {
+ return "澶勭悊澶辫触锛氫緵搴斿晢淇℃伅涓嶅畬鏁达紝璇风‘璁や緵搴斿晢鍚嶇О鎴栦緵搴斿晢ID";
+ }
+ if (message.contains("SQL") || message.contains("java.") || message.contains("Exception")) {
+ return "澶勭悊澶辫触锛氱‘璁ゆ暟鎹笉瀹屾暣鎴栨牸寮忎笉姝g‘锛岃妫�鏌ュ繀濉瓧娈靛悗閲嶈瘯";
+ }
+ return "澶勭悊澶辫触锛�" + message;
+ }
+
+ private AjaxResult fillSupplierIdByName(PurchaseLedgerDto dto) {
+ if (dto.getSupplierId() != null) {
+ return null;
+ }
+ if (!StringUtils.hasText(dto.getSupplierName())) {
+ return AjaxResult.error("渚涘簲鍟咺D涓嶈兘涓虹┖锛涙湭璇嗗埆鍒颁緵搴斿晢鍚嶇О锛屾棤娉曡嚜鍔ㄥ尮閰嶄緵搴斿晢ID");
+ }
+
+ SupplierManage supplier = supplierManageMapper.selectOne(new LambdaQueryWrapper<SupplierManage>()
+ .eq(SupplierManage::getSupplierName, dto.getSupplierName().trim())
+ .last("limit 1"));
+ if (supplier == null) {
+ return AjaxResult.error("鏈壘鍒颁緵搴斿晢锛�" + dto.getSupplierName() + "锛岃鍏堢淮鎶や緵搴斿晢鎴栨墜鍔ㄩ�夋嫨渚涘簲鍟咺D");
+ }
+ dto.setSupplierId(supplier.getId());
+ return null;
+ }
+
+ private AjaxResult processPaymentRegistration(Map<String, Object> payload) {
+ Object recordsValue = payload.get("records");
+ List<PaymentRegistration> records;
+ if (recordsValue == null) {
+ records = Collections.singletonList(objectMapper.convertValue(payload, PaymentRegistration.class));
+ } else {
+ records = objectMapper.convertValue(recordsValue, new TypeReference<List<PaymentRegistration>>() {
+ });
+ }
+ int result = paymentRegistrationService.insertPaymentRegistration(records);
+ return AjaxResult.success("浠樻鐧昏宸插鐞�", result);
+ }
+
+ private AjaxResult processPurchaseReturnOrder(Map<String, Object> payload) {
+ PurchaseReturnOrderDto dto = objectMapper.convertValue(payload, PurchaseReturnOrderDto.class);
+ Boolean result = purchaseReturnOrdersService.add(dto);
+ return AjaxResult.success("閲囪喘閫�璐у崟宸插鐞�", result);
+ }
+}
--
Gitblit v1.9.3