From 9d42f647f5589e4a560d745d6b359ae6c273bd8d Mon Sep 17 00:00:00 2001
From: zss <zss@example.com>
Date: 星期一, 11 五月 2026 13:08:52 +0800
Subject: [PATCH] Merge branch 'dev_New_pro' into dev_宁夏_英泽防锈

---
 src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java |  855 --------------------------------------------------------
 1 files changed, 14 insertions(+), 841 deletions(-)

diff --git a/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java b/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
index 2bb0625..158ea61 100644
--- a/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
+++ b/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
@@ -1,151 +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.mapper.SupplierManageMapper;
-import com.ruoyi.basic.pojo.SupplierManage;
+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.IOException;
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.Base64;
-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.Map;
-import java.util.NoSuchElementException;
-import java.util.UUID;
 
 @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 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,
-                                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.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 = "閲囪喘澶氭枃浠跺垎鏋�")
@@ -153,751 +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
-                : "璇峰垎鏋愯繖浜涢噰璐枃浠讹紝鎻愬彇鍙敤浜庝笟鍔″鐞嗙殑鏁版嵁锛屽苟鏁寸悊鎴愬緟瀹㈡埛纭鐨勬牸寮�";
-
-        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, 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 Flux<String> chatWithPurchaseVisionModel(String memoryId, String userPrompt, MultipartFile[] files) {
-        return Flux.create(sink -> {
-            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)
-                );
-                purchaseVisionStreamingChatModel.chat(messages, new StreamingChatResponseHandler() {
-                    @Override
-                    public void onPartialResponse(String partialResponse) {
-                        sink.next(partialResponse);
-                    }
-
-                    @Override
-                    public void onCompleteResponse(ChatResponse completeResponse) {
-                        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 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));
     }
 }

--
Gitblit v1.9.3