From 1ca5584d7e3200a9af65a099bd26d3593e2ba702 Mon Sep 17 00:00:00 2001
From: liyong <18434998025@163.com>
Date: 星期四, 07 五月 2026 14:36:08 +0800
Subject: [PATCH] 迁移pro

---
 src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java |  643 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 643 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java b/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
new file mode 100644
index 0000000..17b6868
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
@@ -0,0 +1,643 @@
+package com.ruoyi.ai.tools;
+
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.ruoyi.ai.context.AiSessionUserContext;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.framework.security.LoginUser;
+import com.ruoyi.purchase.mapper.InvoicePurchaseMapper;
+import com.ruoyi.purchase.mapper.PaymentRegistrationMapper;
+import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
+import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
+import com.ruoyi.purchase.pojo.InvoicePurchase;
+import com.ruoyi.purchase.pojo.PaymentRegistration;
+import com.ruoyi.purchase.pojo.PurchaseLedger;
+import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
+import com.ruoyi.procurementrecord.mapper.InboundManagementMapper;
+import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
+import com.ruoyi.procurementrecord.pojo.InboundManagement;
+import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
+import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
+import com.ruoyi.sales.pojo.SalesLedgerProduct;
+import dev.langchain4j.agent.tool.P;
+import dev.langchain4j.agent.tool.Tool;
+import dev.langchain4j.agent.tool.ToolMemoryId;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Comparator;
+import java.util.stream.Collectors;
+
+@Component
+public class PurchaseAgentTools {
+
+    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private static final int DEFAULT_LIMIT = 10;
+    private static final int MAX_LIMIT = 30;
+
+    private final PurchaseLedgerMapper purchaseLedgerMapper;
+    private final PaymentRegistrationMapper paymentRegistrationMapper;
+    private final InvoicePurchaseMapper invoicePurchaseMapper;
+    private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
+    private final SalesLedgerProductMapper salesLedgerProductMapper;
+    private final ProcurementRecordMapper procurementRecordMapper;
+    private final InboundManagementMapper inboundManagementMapper;
+    private final AiSessionUserContext aiSessionUserContext;
+
+    public PurchaseAgentTools(PurchaseLedgerMapper purchaseLedgerMapper,
+                              PaymentRegistrationMapper paymentRegistrationMapper,
+                              InvoicePurchaseMapper invoicePurchaseMapper,
+                              PurchaseReturnOrdersMapper purchaseReturnOrdersMapper,
+                              SalesLedgerProductMapper salesLedgerProductMapper,
+                              ProcurementRecordMapper procurementRecordMapper,
+                              InboundManagementMapper inboundManagementMapper,
+                              AiSessionUserContext aiSessionUserContext) {
+        this.purchaseLedgerMapper = purchaseLedgerMapper;
+        this.paymentRegistrationMapper = paymentRegistrationMapper;
+        this.invoicePurchaseMapper = invoicePurchaseMapper;
+        this.purchaseReturnOrdersMapper = purchaseReturnOrdersMapper;
+        this.salesLedgerProductMapper = salesLedgerProductMapper;
+        this.procurementRecordMapper = procurementRecordMapper;
+        this.inboundManagementMapper = inboundManagementMapper;
+        this.aiSessionUserContext = aiSessionUserContext;
+    }
+
+    @Tool(name = "鏌ヨ閲囪喘鍙拌处鍒楄〃", value = "鎸夊叧閿瓧鍜屾椂闂磋寖鍥存煡璇㈤噰璐彴璐︼紝鏀寔杩斿洖鏈�杩慛鏉�")
+    public String listPurchaseLedgers(@ToolMemoryId String memoryId,
+                                      @P(value = "鍏抽敭瀛楋紝鍙尮閰嶉噰璐悎鍚屽彿/渚涘簲鍟�/椤圭洰鍚�", required = false) String keyword,
+                                      @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+                                      @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+                                      @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit) {
+        LoginUser loginUser = currentLoginUser(memoryId);
+        LocalDate start = parseLocalDate(startDate);
+        LocalDate end = parseLocalDate(endDate);
+        int finalLimit = normalizeLimit(limit);
+
+        LambdaQueryWrapper<PurchaseLedger> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), PurchaseLedger::getTenantId);
+        if (StringUtils.hasText(keyword)) {
+            wrapper.and(w -> w.like(PurchaseLedger::getPurchaseContractNumber, keyword)
+                    .or().like(PurchaseLedger::getSupplierName, keyword)
+                    .or().like(PurchaseLedger::getProjectName, keyword));
+        }
+        if (start != null) {
+            wrapper.ge(PurchaseLedger::getEntryDate, toDate(start));
+        }
+        if (end != null) {
+            wrapper.lt(PurchaseLedger::getEntryDate, toExclusiveEndDate(end));
+        }
+        wrapper.orderByDesc(PurchaseLedger::getEntryDate, PurchaseLedger::getId).last("limit " + finalLimit);
+
+        List<PurchaseLedger> rows = defaultList(purchaseLedgerMapper.selectList(wrapper));
+        List<Map<String, Object>> items = rows.stream().map(this::toLedgerItem).collect(Collectors.toList());
+        return jsonResponse(true, "purchase_ledger_list", "宸茶繑鍥為噰璐彴璐﹀垪琛�",
+                Map.of("count", items.size(), "limit", finalLimit, "keyword", safe(keyword)),
+                Map.of("items", items), Map.of());
+    }
+
+    @Tool(name = "鏌ヨ閲囪喘鍙拌处璇︽儏", value = "鎸夐噰璐彴璐D鏌ヨ璇︽儏")
+    public String getPurchaseLedgerDetail(@ToolMemoryId String memoryId, @P("閲囪喘鍙拌处ID") Long ledgerId) {
+        if (ledgerId == null) {
+            return jsonResponse(false, "purchase_ledger_detail", "閲囪喘鍙拌处ID涓嶈兘涓虹┖", Map.of(), Map.of(), Map.of());
+        }
+        LoginUser loginUser = currentLoginUser(memoryId);
+        PurchaseLedger ledger = purchaseLedgerMapper.selectById(ledgerId);
+        if (ledger == null || !tenantMatched(ledger.getTenantId(), loginUser.getTenantId())) {
+            return jsonResponse(false, "purchase_ledger_detail", "鏈壘鍒拌閲囪喘鍙拌处鎴栨棤鏉冮檺璁块棶", Map.of("ledgerId", ledgerId), Map.of(), Map.of());
+        }
+        return jsonResponse(true, "purchase_ledger_detail", "宸茶繑鍥為噰璐彴璐﹁鎯�",
+                Map.of("ledgerId", ledgerId),
+                Map.of("detail", toLedgerItem(ledger)),
+                Map.of());
+    }
+
+    @Tool(name = "缁熻閲囪喘鏁版嵁", value = "缁熻鏃堕棿鑼冨洿鍐呴噰璐悎鍚屾暟銆佸悎鍚岄噾棰濄�佷粯娆鹃噾棰濄�佸彂绁ㄩ噾棰濄�侀��璐ч噾棰�")
+    public String getPurchaseStats(@ToolMemoryId String memoryId,
+                                   @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+                                   @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+                                   @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屼緥濡備粖骞淬�佹湰鏈堛�佽繎30澶�", required = false) String timeRange) {
+        LoginUser loginUser = currentLoginUser(memoryId);
+        DateRange range = resolveDateRange(startDate, endDate, timeRange);
+
+        List<PurchaseLedger> ledgers = queryLedgers(loginUser, range);
+        List<PaymentRegistration> payments = queryPayments(loginUser, range);
+        List<InvoicePurchase> invoices = queryInvoices(loginUser, range);
+        List<PurchaseReturnOrders> returns = queryReturns(loginUser, range);
+
+        BigDecimal contractAmount = ledgers.stream()
+                .map(PurchaseLedger::getContractAmount)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal paymentAmount = payments.stream()
+                .map(PaymentRegistration::getCurrentPaymentAmount)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal invoiceAmount = invoices.stream()
+                .map(InvoicePurchase::getInvoiceAmount)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal returnAmount = returns.stream()
+                .map(PurchaseReturnOrders::getTotalAmount)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        Map<String, Object> summary = new LinkedHashMap<>();
+        summary.put("timeRange", range.label());
+        summary.put("startDate", range.start().toString());
+        summary.put("endDate", range.end().toString());
+        summary.put("ledgerCount", ledgers.size());
+        summary.put("paymentCount", payments.size());
+        summary.put("invoiceCount", invoices.size());
+        summary.put("returnCount", returns.size());
+        summary.put("contractAmount", contractAmount);
+        summary.put("paymentAmount", paymentAmount);
+        summary.put("invoiceAmount", invoiceAmount);
+        summary.put("returnAmount", returnAmount);
+
+        return jsonResponse(true, "purchase_stats", "宸茶繑鍥為噰璐粺璁℃暟鎹�", summary, Map.of(), Map.of());
+    }
+
+    @Tool(name = "閲囪喘鐗╂枡閲戦鎺掕", value = "鎸夋椂闂磋寖鍥寸粺璁¢噰璐墿鏂欓噾棰濇帓琛岋紝鍙洖绛旀湰鏈堥噰璐噾棰濇帓鍚嶉潬鍓嶇殑鐗╂枡銆�")
+    public String rankPurchaseMaterials(@ToolMemoryId String memoryId,
+                                        @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+                                        @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+                                        @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屼緥濡傛湰鏈堛�佽繎7澶┿�佽繎30澶�", required = false) String timeRange,
+                                        @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit) {
+        LoginUser loginUser = currentLoginUser(memoryId);
+        DateRange range = resolveDateRange(startDate, endDate, timeRange);
+        List<Long> ledgerIds = queryLedgers(loginUser, range).stream()
+                .map(PurchaseLedger::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        if (ledgerIds.isEmpty()) {
+            return jsonResponse(true, "purchase_material_rank", "褰撳墠鏃堕棿鑼冨洿鍐呮病鏈夐噰璐墿鏂欐暟鎹��",
+                    rangeSummary(range, 0), Map.of("items", List.of()), Map.of());
+        }
+
+        List<SalesLedgerProduct> products = defaultList(salesLedgerProductMapper.selectList(new LambdaQueryWrapper<SalesLedgerProduct>()
+                .eq(SalesLedgerProduct::getType, 2)
+                .in(SalesLedgerProduct::getSalesLedgerId, ledgerIds)));
+
+        Map<String, MaterialRankItem> grouped = new LinkedHashMap<>();
+        for (SalesLedgerProduct product : products) {
+            String name = safe(product.getProductCategory());
+            String model = safe(product.getSpecificationModel());
+            String key = name + "|" + model;
+            MaterialRankItem item = grouped.computeIfAbsent(key, ignored -> new MaterialRankItem(name, model, safe(product.getUnit())));
+            item.quantity = item.quantity.add(defaultDecimal(product.getQuantity()));
+            item.amount = item.amount.add(defaultDecimal(product.getTaxInclusiveTotalPrice()));
+        }
+
+        List<Map<String, Object>> items = grouped.values().stream()
+                .sorted(Comparator.comparing((MaterialRankItem item) -> item.amount).reversed())
+                .limit(normalizeLimit(limit))
+                .map(MaterialRankItem::toMap)
+                .collect(Collectors.toList());
+
+        return jsonResponse(true, "purchase_material_rank", "宸茶繑鍥為噰璐墿鏂欓噾棰濇帓琛屻��",
+                rangeSummary(range, items.size()), Map.of("items", items), Map.of());
+    }
+
+    @Tool(name = "鏌ヨ鏈叆搴撻噰璐鍗�", value = "鏌ヨ閲囪喘璁㈠崟涓嬩粛鏈夊緟鍏ュ簱鏁伴噺鐨勭墿鏂欐槑缁嗐��")
+    public String listUnstockedPurchaseOrders(@ToolMemoryId String memoryId,
+                                              @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+                                              @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+                                              @P(value = "鍏抽敭瀛楋紝鍙尮閰嶉噰璐悎鍚屽彿/渚涘簲鍟�/鐗╂枡", required = false) String keyword,
+                                              @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit) {
+        LoginUser loginUser = currentLoginUser(memoryId);
+        DateRange range = resolveDateRange(startDate, endDate, null);
+        List<PurchaseLedger> ledgers = queryLedgers(loginUser, range).stream()
+                .filter(ledger -> matchLedgerKeyword(ledger, keyword))
+                .collect(Collectors.toList());
+        Map<Long, PurchaseLedger> ledgerMap = ledgers.stream()
+                .filter(ledger -> ledger.getId() != null)
+                .collect(Collectors.toMap(PurchaseLedger::getId, ledger -> ledger, (a, b) -> a, LinkedHashMap::new));
+        if (ledgerMap.isEmpty()) {
+            return jsonResponse(true, "purchase_unstocked_list", "鏈煡璇㈠埌绗﹀悎鏉′欢鐨勯噰璐鍗曘��",
+                    rangeSummary(range, 0), Map.of("items", List.of()), Map.of());
+        }
+
+        List<SalesLedgerProduct> products = defaultList(salesLedgerProductMapper.selectList(new LambdaQueryWrapper<SalesLedgerProduct>()
+                .eq(SalesLedgerProduct::getType, 2)
+                .in(SalesLedgerProduct::getSalesLedgerId, ledgerMap.keySet())));
+
+        List<Map<String, Object>> items = products.stream()
+                .filter(product -> matchProductKeyword(product, keyword))
+                .map(product -> toUnstockedItem(product, ledgerMap.get(product.getSalesLedgerId())))
+                .filter(Objects::nonNull)
+                .limit(normalizeLimit(limit))
+                .collect(Collectors.toList());
+
+        return jsonResponse(true, "purchase_unstocked_list", "宸茶繑鍥炴湭鍏ュ簱閲囪喘璁㈠崟銆�",
+                rangeSummary(range, items.size()), Map.of("items", items), Map.of());
+    }
+
+    @Tool(name = "鏌ヨ閲囪喘鍒拌揣寮傚父", value = "鏌ヨ鍒拌揣鐘舵�佸紓甯告垨澶囨敞鍖呭惈寮傚父淇℃伅鐨勫埌璐ц褰曘��")
+    public String listArrivalExceptions(@ToolMemoryId String memoryId,
+                                        @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+                                        @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+                                        @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屼緥濡傝繎7澶┿�佹湰鏈�", required = false) String timeRange,
+                                        @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit) {
+        LoginUser loginUser = currentLoginUser(memoryId);
+        DateRange range = resolveDateRange(startDate, endDate, timeRange);
+        LambdaQueryWrapper<InboundManagement> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), InboundManagement::getTenantId);
+        wrapper.ge(InboundManagement::getArrivalTime, toDate(range.start()))
+                .lt(InboundManagement::getArrivalTime, toExclusiveEndDate(range.end()))
+                .and(w -> w.notLike(InboundManagement::getStatus, "姝e父")
+                        .notLike(InboundManagement::getStatus, "瀹屾垚")
+                        .notLike(InboundManagement::getStatus, "宸插埌璐�")
+                        .or().like(InboundManagement::getStatus, "寮傚父")
+                        .or().like(InboundManagement::getRemark, "寮傚父")
+                        .or().like(InboundManagement::getRemark, "闂")
+                        .or().like(InboundManagement::getRemark, "寤惰繜")
+                        .or().like(InboundManagement::getRemark, "鐭己"));
+        wrapper.orderByDesc(InboundManagement::getArrivalTime).last("limit " + normalizeLimit(limit));
+
+        List<Map<String, Object>> items = defaultList(inboundManagementMapper.selectList(wrapper)).stream()
+                .map(this::toArrivalItem)
+                .collect(Collectors.toList());
+        return jsonResponse(true, "purchase_arrival_exception_list", "宸茶繑鍥為噰璐埌璐у紓甯歌褰曘��",
+                rangeSummary(range, items.size()), Map.of("items", items), Map.of());
+    }
+
+    @Tool(name = "鏌ヨ寰呬粯娆鹃噰璐崟", value = "鏌ヨ鍚堝悓閲戦澶т簬宸蹭粯娆鹃噾棰濈殑閲囪喘鍗曘��")
+    public String listPendingPaymentOrders(@ToolMemoryId String memoryId,
+                                           @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+                                           @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+                                           @P(value = "鍏抽敭瀛楋紝鍙尮閰嶉噰璐悎鍚屽彿/渚涘簲鍟�/椤圭洰鍚�", required = false) String keyword,
+                                           @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit) {
+        LoginUser loginUser = currentLoginUser(memoryId);
+        DateRange range = resolveDateRange(startDate, endDate, null);
+        List<Map<String, Object>> items = queryLedgers(loginUser, range).stream()
+                .filter(ledger -> matchLedgerKeyword(ledger, keyword))
+                .map(ledger -> toPendingPaymentItem(loginUser, ledger))
+                .filter(Objects::nonNull)
+                .sorted(Comparator.comparing(item -> (BigDecimal) item.get("pendingAmount"), Comparator.reverseOrder()))
+                .limit(normalizeLimit(limit))
+                .collect(Collectors.toList());
+        return jsonResponse(true, "purchase_pending_payment_list", "宸茶繑鍥炲緟浠樻閲囪喘鍗曘��",
+                rangeSummary(range, items.size()), Map.of("items", items), Map.of());
+    }
+
+    @Tool(name = "鏌ヨ閲囪喘閫�璐ф儏鍐�", value = "鎸夋椂闂磋寖鍥存煡璇㈤噰璐��璐у崟鍒楄〃鍜岄��璐ч噾棰濄��")
+    public String listPurchaseReturns(@ToolMemoryId String memoryId,
+                                      @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+                                      @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+                                      @P(value = "鍏抽敭瀛楋紝鍙尮閰嶉��璐у崟鍙�/澶囨敞", required = false) String keyword,
+                                      @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit) {
+        LoginUser loginUser = currentLoginUser(memoryId);
+        DateRange range = resolveDateRange(startDate, endDate, null);
+        LambdaQueryWrapper<PurchaseReturnOrders> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), PurchaseReturnOrders::getDeptId);
+        wrapper.ge(PurchaseReturnOrders::getPreparedAt, range.start())
+                .le(PurchaseReturnOrders::getPreparedAt, range.end());
+        if (StringUtils.hasText(keyword)) {
+            wrapper.and(w -> w.like(PurchaseReturnOrders::getNo, keyword)
+                    .or().like(PurchaseReturnOrders::getRemark, keyword)
+                    .or().like(PurchaseReturnOrders::getReturnUserName, keyword));
+        }
+        wrapper.orderByDesc(PurchaseReturnOrders::getPreparedAt).last("limit " + normalizeLimit(limit));
+
+        List<PurchaseReturnOrders> returns = defaultList(purchaseReturnOrdersMapper.selectList(wrapper));
+        BigDecimal totalAmount = returns.stream()
+                .map(PurchaseReturnOrders::getTotalAmount)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        Map<String, Object> summary = rangeSummary(range, returns.size());
+        summary.put("returnAmount", totalAmount);
+
+        return jsonResponse(true, "purchase_return_list", "宸茶繑鍥為噰璐��璐ф儏鍐点��",
+                summary,
+                Map.of("items", returns.stream().map(this::toReturnItem).collect(Collectors.toList())),
+                Map.of());
+    }
+
+    private List<PurchaseLedger> queryLedgers(LoginUser loginUser, DateRange range) {
+        LambdaQueryWrapper<PurchaseLedger> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), PurchaseLedger::getTenantId);
+        wrapper.ge(PurchaseLedger::getEntryDate, toDate(range.start()))
+                .lt(PurchaseLedger::getEntryDate, toExclusiveEndDate(range.end()));
+        return defaultList(purchaseLedgerMapper.selectList(wrapper));
+    }
+
+    private Map<String, Object> rangeSummary(DateRange range, int count) {
+        Map<String, Object> summary = new LinkedHashMap<>();
+        summary.put("timeRange", range.label());
+        summary.put("startDate", range.start().toString());
+        summary.put("endDate", range.end().toString());
+        summary.put("count", count);
+        return summary;
+    }
+
+    private boolean matchLedgerKeyword(PurchaseLedger ledger, String keyword) {
+        if (!StringUtils.hasText(keyword)) {
+            return true;
+        }
+        String text = keyword.trim();
+        return safe(ledger.getPurchaseContractNumber()).contains(text)
+                || safe(ledger.getSupplierName()).contains(text)
+                || safe(ledger.getProjectName()).contains(text);
+    }
+
+    private boolean matchProductKeyword(SalesLedgerProduct product, String keyword) {
+        if (!StringUtils.hasText(keyword)) {
+            return true;
+        }
+        String text = keyword.trim();
+        return safe(product.getProductCategory()).contains(text)
+                || safe(product.getSpecificationModel()).contains(text);
+    }
+
+    private Map<String, Object> toUnstockedItem(SalesLedgerProduct product, PurchaseLedger ledger) {
+        if (product == null || ledger == null || product.getId() == null) {
+            return null;
+        }
+        BigDecimal orderedQuantity = defaultDecimal(product.getQuantity());
+        BigDecimal inboundQuantity = sumInboundQuantity(product.getId());
+        BigDecimal pendingQuantity = orderedQuantity.subtract(inboundQuantity);
+        if (pendingQuantity.compareTo(BigDecimal.ZERO) <= 0) {
+            return null;
+        }
+        Map<String, Object> item = new LinkedHashMap<>();
+        item.put("purchaseLedgerId", ledger.getId());
+        item.put("purchaseContractNumber", safe(ledger.getPurchaseContractNumber()));
+        item.put("supplierName", safe(ledger.getSupplierName()));
+        item.put("productCategory", safe(product.getProductCategory()));
+        item.put("specificationModel", safe(product.getSpecificationModel()));
+        item.put("unit", safe(product.getUnit()));
+        item.put("orderedQuantity", orderedQuantity);
+        item.put("inboundQuantity", inboundQuantity);
+        item.put("pendingInboundQuantity", pendingQuantity);
+        item.put("entryDate", formatDate(ledger.getEntryDate()));
+        return item;
+    }
+
+    private BigDecimal sumInboundQuantity(Long salesLedgerProductId) {
+        List<ProcurementRecordStorage> records = defaultList(procurementRecordMapper.selectList(new LambdaQueryWrapper<ProcurementRecordStorage>()
+                .eq(ProcurementRecordStorage::getType, 1)
+                .eq(ProcurementRecordStorage::getSalesLedgerProductId, salesLedgerProductId)));
+        return records.stream()
+                .map(ProcurementRecordStorage::getInboundNum)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+    }
+
+    private Map<String, Object> toArrivalItem(InboundManagement item) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("id", item.getId());
+        map.put("orderNo", safe(item.getOrderNo()));
+        map.put("arrivalNo", safe(item.getArrivalNo()));
+        map.put("supplierName", safe(item.getSupplierName()));
+        map.put("status", safe(item.getStatus()));
+        map.put("arrivalTime", formatDate(item.getArrivalTime()));
+        map.put("arrivalQuantity", safe(item.getArrivalQuantity()));
+        map.put("remark", safe(item.getRemark()));
+        return map;
+    }
+
+    private Map<String, Object> toPendingPaymentItem(LoginUser loginUser, PurchaseLedger ledger) {
+        BigDecimal contractAmount = defaultDecimal(ledger.getContractAmount());
+        BigDecimal paidAmount = sumPaymentAmount(loginUser, ledger.getId());
+        BigDecimal pendingAmount = contractAmount.subtract(paidAmount);
+        if (pendingAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            return null;
+        }
+        Map<String, Object> item = toLedgerItem(ledger);
+        item.put("paidAmount", paidAmount);
+        item.put("pendingAmount", pendingAmount);
+        return item;
+    }
+
+    private BigDecimal sumPaymentAmount(LoginUser loginUser, Long purchaseLedgerId) {
+        LambdaQueryWrapper<PaymentRegistration> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), PaymentRegistration::getTenantId);
+        wrapper.eq(PaymentRegistration::getPurchaseLedgerId, purchaseLedgerId);
+        return defaultList(paymentRegistrationMapper.selectList(wrapper)).stream()
+                .map(PaymentRegistration::getCurrentPaymentAmount)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+    }
+
+    private Map<String, Object> toReturnItem(PurchaseReturnOrders item) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("id", item.getId());
+        map.put("no", safe(item.getNo()));
+        map.put("returnType", item.getReturnType());
+        map.put("purchaseLedgerId", item.getPurchaseLedgerId());
+        map.put("preparedAt", item.getPreparedAt() == null ? "" : item.getPreparedAt().toString());
+        map.put("returnUserName", safe(item.getReturnUserName()));
+        map.put("totalAmount", item.getTotalAmount());
+        map.put("remark", safe(item.getRemark()));
+        return map;
+    }
+
+    private BigDecimal defaultDecimal(BigDecimal value) {
+        return value == null ? BigDecimal.ZERO : value;
+    }
+
+    private List<PaymentRegistration> queryPayments(LoginUser loginUser, DateRange range) {
+        LambdaQueryWrapper<PaymentRegistration> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), PaymentRegistration::getTenantId);
+        wrapper.ge(PaymentRegistration::getPaymentDate, toDate(range.start()))
+                .lt(PaymentRegistration::getPaymentDate, toExclusiveEndDate(range.end()));
+        return defaultList(paymentRegistrationMapper.selectList(wrapper));
+    }
+
+    private List<InvoicePurchase> queryInvoices(LoginUser loginUser, DateRange range) {
+        LambdaQueryWrapper<InvoicePurchase> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), InvoicePurchase::getTenantId);
+        wrapper.ge(InvoicePurchase::getIssueDate, range.start())
+                .le(InvoicePurchase::getIssueDate, range.end());
+        return defaultList(invoicePurchaseMapper.selectList(wrapper));
+    }
+
+    private List<PurchaseReturnOrders> queryReturns(LoginUser loginUser, DateRange range) {
+        LambdaQueryWrapper<PurchaseReturnOrders> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), PurchaseReturnOrders::getDeptId);
+        wrapper.ge(PurchaseReturnOrders::getPreparedAt, range.start())
+                .le(PurchaseReturnOrders::getPreparedAt, range.end());
+        return defaultList(purchaseReturnOrdersMapper.selectList(wrapper));
+    }
+
+    private Map<String, Object> toLedgerItem(PurchaseLedger item) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("id", item.getId());
+        map.put("purchaseContractNumber", safe(item.getPurchaseContractNumber()));
+        map.put("supplierName", safe(item.getSupplierName()));
+        map.put("projectName", safe(item.getProjectName()));
+        map.put("entryDate", formatDate(item.getEntryDate()));
+        map.put("contractAmount", item.getContractAmount());
+        map.put("approvalStatus", item.getApprovalStatus());
+        map.put("paymentMethod", safe(item.getPaymentMethod()));
+        return map;
+    }
+
+    private DateRange resolveDateRange(String startDate, String endDate, String timeRange) {
+        LocalDate today = LocalDate.now();
+        LocalDate start = parseLocalDate(startDate);
+        LocalDate end = parseLocalDate(endDate);
+        if (start != null || end != null) {
+            LocalDate s = start != null ? start : end;
+            LocalDate e = end != null ? end : start;
+            if (s.isAfter(e)) {
+                LocalDate temp = s;
+                s = e;
+                e = temp;
+            }
+            return new DateRange(s, e, s + "鑷�" + e);
+        }
+        if (!StringUtils.hasText(timeRange)) {
+            return new DateRange(today.minusDays(29), today, "杩�30澶�");
+        }
+        String text = timeRange.trim();
+        if (text.contains("浠婂勾") || text.contains("鏈勾")) {
+            return new DateRange(today.withDayOfYear(1), today, "浠婂勾");
+        }
+        if (text.contains("鏈湀")) {
+            return new DateRange(today.withDayOfMonth(1), today, "鏈湀");
+        }
+        if (text.contains("涓婃湀")) {
+            LocalDate first = today.minusMonths(1).withDayOfMonth(1);
+            LocalDate last = first.withDayOfMonth(first.lengthOfMonth());
+            return new DateRange(first, last, "涓婃湀");
+        }
+        if (text.contains("杩戝崐骞�") || text.contains("鏈�杩戝崐骞�")) {
+            return new DateRange(today.minusMonths(6).plusDays(1), today, "杩戝崐骞�");
+        }
+        if (text.contains("杩戝崐涓湀") || text.contains("鏈�杩戝崐涓湀") || text.contains("鍗婁釜鏈�")) {
+            return new DateRange(today.minusDays(14), today, "杩戝崐涓湀");
+        }
+        java.util.regex.Matcher relativeMatcher = java.util.regex.Pattern.compile("(杩憒鏈�杩�)(\\d+)(澶﹟鍛▅涓湀|鏈坾骞�)").matcher(text);
+        if (relativeMatcher.find()) {
+            int amount = Integer.parseInt(relativeMatcher.group(2));
+            String unit = relativeMatcher.group(3);
+            LocalDate relativeStart = switch (unit) {
+                case "澶�" -> today.minusDays(Math.max(amount - 1L, 0));
+                case "鍛�" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1);
+                case "涓湀", "鏈�" -> today.minusMonths(Math.max(amount, 1)).plusDays(1);
+                case "骞�" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
+                default -> today.minusDays(29);
+            };
+            return new DateRange(relativeStart, today, "杩�" + amount + unit);
+        }
+        return new DateRange(today.minusDays(29), today, "杩�30澶�");
+    }
+
+    private LocalDate parseLocalDate(String text) {
+        if (!StringUtils.hasText(text)) {
+            return null;
+        }
+        return LocalDate.parse(text.trim(), DATE_FMT);
+    }
+
+    private Date toDate(LocalDate localDate) {
+        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
+    }
+
+    private Date toExclusiveEndDate(LocalDate localDate) {
+        return Date.from(localDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
+    }
+
+    private String formatDate(Date date) {
+        if (date == null) {
+            return "";
+        }
+        return DATE_FMT.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
+    }
+
+    private boolean tenantMatched(Long dataTenantId, Long userTenantId) {
+        if (userTenantId == null) {
+            return true;
+        }
+        return Objects.equals(dataTenantId, userTenantId);
+    }
+
+    private <T> void applyTenantFilter(LambdaQueryWrapper<T> wrapper, Long tenantId, com.baomidou.mybatisplus.core.toolkit.support.SFunction<T, Long> field) {
+        if (tenantId != null) {
+            wrapper.eq(field, tenantId);
+        }
+    }
+
+    private <T> void applyDeptFilter(LambdaQueryWrapper<T> wrapper, Long deptId, com.baomidou.mybatisplus.core.toolkit.support.SFunction<T, Long> field) {
+        if (deptId != null) {
+            wrapper.eq(field, deptId);
+        }
+    }
+
+    private LoginUser currentLoginUser(String memoryId) {
+        LoginUser loginUser = aiSessionUserContext.get(memoryId);
+        if (loginUser != null) {
+            return loginUser;
+        }
+        return SecurityUtils.getLoginUser();
+    }
+
+    private int normalizeLimit(Integer limit) {
+        if (limit == null || limit <= 0) {
+            return DEFAULT_LIMIT;
+        }
+        return Math.min(limit, MAX_LIMIT);
+    }
+
+    private String safe(Object value) {
+        return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ');
+    }
+
+    private <T> List<T> defaultList(List<T> list) {
+        return list == null ? List.of() : list;
+    }
+
+    private String jsonResponse(boolean success,
+                                String type,
+                                String description,
+                                Map<String, Object> summary,
+                                Map<String, Object> data,
+                                Map<String, Object> charts) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("success", success);
+        result.put("type", type);
+        result.put("description", description);
+        result.put("summary", summary == null ? Map.of() : summary);
+        result.put("data", data == null ? Map.of() : data);
+        result.put("charts", charts == null ? Map.of() : charts);
+        return JSON.toJSONString(result);
+    }
+
+    private record DateRange(LocalDate start, LocalDate end, String label) {
+    }
+
+    private static class MaterialRankItem {
+        private final String productCategory;
+        private final String specificationModel;
+        private final String unit;
+        private BigDecimal quantity = BigDecimal.ZERO;
+        private BigDecimal amount = BigDecimal.ZERO;
+
+        private MaterialRankItem(String productCategory, String specificationModel, String unit) {
+            this.productCategory = productCategory;
+            this.specificationModel = specificationModel;
+            this.unit = unit;
+        }
+
+        private Map<String, Object> toMap() {
+            Map<String, Object> map = new LinkedHashMap<>();
+            map.put("productCategory", productCategory);
+            map.put("specificationModel", specificationModel);
+            map.put("unit", unit);
+            map.put("quantity", quantity);
+            map.put("amount", amount);
+            return map;
+        }
+    }
+}

--
Gitblit v1.9.3