liyong
6 天以前 1c518e10a50050d383e714b581c94dea58ec4d67
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
@@ -2,17 +2,29 @@
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.account.mapper.purchase.AccountPaymentApplicationMapper;
import com.ruoyi.account.mapper.purchase.AccountPurchaseInvoiceMapper;
import com.ruoyi.account.mapper.purchase.AccountPurchasePaymentMapper;
import com.ruoyi.account.pojo.purchase.AccountPaymentApplication;
import com.ruoyi.account.pojo.purchase.AccountPurchaseInvoice;
import com.ruoyi.account.pojo.purchase.AccountPurchasePayment;
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.procurementrecord.mapper.InboundManagementMapper;
import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
import com.ruoyi.procurementrecord.pojo.InboundManagement;
import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
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.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
import com.ruoyi.sales.pojo.SalesLedgerProduct;
import com.ruoyi.stock.mapper.StockInRecordMapper;
import com.ruoyi.stock.pojo.StockInRecord;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolMemoryId;
@@ -21,38 +33,52 @@
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.*;
import java.util.stream.Collectors;
@Component
public class PurchaseAgentTools {
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
    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 AccountPurchasePaymentMapper accountPurchasePaymentMapper;
    private final AccountPaymentApplicationMapper accountPaymentApplicationMapper;
    private final AccountPurchaseInvoiceMapper accountPurchaseInvoiceMapper;
    private final StockInRecordMapper stockInRecordMapper;
    private final QualityInspectMapper qualityInspectMapper;
    private final AiSessionUserContext aiSessionUserContext;
    public PurchaseAgentTools(PurchaseLedgerMapper purchaseLedgerMapper,
                              PaymentRegistrationMapper paymentRegistrationMapper,
                              InvoicePurchaseMapper invoicePurchaseMapper,
                              PurchaseReturnOrdersMapper purchaseReturnOrdersMapper,
                              SalesLedgerProductMapper salesLedgerProductMapper,
                              ProcurementRecordMapper procurementRecordMapper,
                              InboundManagementMapper inboundManagementMapper,
                              AccountPurchasePaymentMapper accountPurchasePaymentMapper,
                              AccountPaymentApplicationMapper accountPaymentApplicationMapper,
                              AccountPurchaseInvoiceMapper accountPurchaseInvoiceMapper,
                              StockInRecordMapper stockInRecordMapper,
                              QualityInspectMapper qualityInspectMapper,
                              AiSessionUserContext aiSessionUserContext) {
        this.purchaseLedgerMapper = purchaseLedgerMapper;
        this.paymentRegistrationMapper = paymentRegistrationMapper;
        this.invoicePurchaseMapper = invoicePurchaseMapper;
        this.purchaseReturnOrdersMapper = purchaseReturnOrdersMapper;
        this.salesLedgerProductMapper = salesLedgerProductMapper;
        this.procurementRecordMapper = procurementRecordMapper;
        this.inboundManagementMapper = inboundManagementMapper;
        this.accountPurchasePaymentMapper = accountPurchasePaymentMapper;
        this.accountPaymentApplicationMapper = accountPaymentApplicationMapper;
        this.accountPurchaseInvoiceMapper = accountPurchaseInvoiceMapper;
        this.stockInRecordMapper = stockInRecordMapper;
        this.qualityInspectMapper = qualityInspectMapper;
        this.aiSessionUserContext = aiSessionUserContext;
    }
@@ -78,7 +104,7 @@
            wrapper.ge(PurchaseLedger::getEntryDate, toDate(start));
        }
        if (end != null) {
            wrapper.le(PurchaseLedger::getEntryDate, toDate(end));
            wrapper.lt(PurchaseLedger::getEntryDate, toExclusiveEndDate(end));
        }
        wrapper.orderByDesc(PurchaseLedger::getEntryDate, PurchaseLedger::getId).last("limit " + finalLimit);
@@ -114,8 +140,8 @@
        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<AccountPurchasePayment> payments = queryPayments(loginUser, range);
        List<AccountPurchaseInvoice> invoices = queryInvoices(loginUser, range);
        List<PurchaseReturnOrders> returns = queryReturns(loginUser, range);
        BigDecimal contractAmount = ledgers.stream()
@@ -123,11 +149,11 @@
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal paymentAmount = payments.stream()
                .map(PaymentRegistration::getCurrentPaymentAmount)
                .map(AccountPurchasePayment::getPaymentAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal invoiceAmount = invoices.stream()
                .map(InvoicePurchase::getInvoiceAmount)
                .map(this::invoiceAmountOf)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal returnAmount = returns.stream()
@@ -151,29 +177,473 @@
        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, "正常")
                        .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<PurchaseLedger> matchedLedgers = queryLedgers(loginUser, range).stream()
                .filter(ledger -> matchLedgerKeyword(ledger, keyword))
                .collect(Collectors.toList());
        Map<Long, BigDecimal> paidAmountByLedgerId = sumPaymentAmountByLedgerId(loginUser, matchedLedgers.stream()
                .map(PurchaseLedger::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList()));
        List<Map<String, Object>> items = matchedLedgers.stream()
                .map(ledger -> toPendingPaymentItem(ledger, paidAmountByLedgerId.getOrDefault(ledger.getId(), BigDecimal.ZERO)))
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(item -> (BigDecimal) item.get("pendingAmount"), Comparator.reverseOrder()))
                .limit(normalizeLimit(limit))
                .collect(Collectors.toList());
        BigDecimal totalContractAmount = items.stream()
                .map(item -> asBigDecimal(item.get("contractAmount")))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalPaidAmount = items.stream()
                .map(item -> asBigDecimal(item.get("paidAmount")))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal totalPendingAmount = items.stream()
                .map(item -> asBigDecimal(item.get("pendingAmount")))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        Map<String, Object> summary = rangeSummary(range, items.size());
        summary.put("pendingOrderCount", items.size());
        summary.put("totalContractAmount", totalContractAmount);
        summary.put("totalPaidAmount", totalPaidAmount);
        summary.put("totalPendingAmount", totalPendingAmount);
        return jsonResponse(true, "purchase_pending_payment_list", "已返回待付款采购单。",
                summary, 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()))
                .le(PurchaseLedger::getEntryDate, toDate(range.end()));
                .lt(PurchaseLedger::getEntryDate, toExclusiveEndDate(range.end()));
        return defaultList(purchaseLedgerMapper.selectList(wrapper));
    }
    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()))
                .le(PaymentRegistration::getPaymentDate, toDate(range.end()));
        return defaultList(paymentRegistrationMapper.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 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 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(PurchaseLedger ledger, BigDecimal paidAmount) {
        BigDecimal contractAmount = defaultDecimal(ledger.getContractAmount());
        BigDecimal safePaidAmount = defaultDecimal(paidAmount);
        BigDecimal pendingAmount = contractAmount.subtract(safePaidAmount);
        if (pendingAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return null;
        }
        Map<String, Object> item = toLedgerItem(ledger);
        item.put("paidAmount", safePaidAmount);
        item.put("pendingAmount", pendingAmount);
        return item;
    }
    private Map<Long, BigDecimal> sumPaymentAmountByLedgerId(LoginUser loginUser, List<Long> purchaseLedgerIds) {
        if (purchaseLedgerIds == null || purchaseLedgerIds.isEmpty()) {
            return Map.of();
        }
        List<AccountPurchasePayment> payments = queryPayments(loginUser);
        if (payments.isEmpty()) {
            return Map.of();
        }
        Map<Integer, AccountPaymentApplication> applicationById = queryPaymentApplications(payments);
        if (applicationById.isEmpty()) {
            return Map.of();
        }
        Map<Long, StockInRecord> stockInRecordById = queryStockInRecords(applicationById.values());
        Map<Long, Long> purchaseLedgerIdByQualityInspectId = queryPurchaseLedgerIdByQualityInspectId(stockInRecordById.values());
        Set<Long> targetLedgerIdSet = new HashSet<>(purchaseLedgerIds);
        Map<Long, BigDecimal> result = new HashMap<>();
        for (AccountPurchasePayment payment : payments) {
            if (payment.getAccountPaymentApplicationId() == null) {
                continue;
            }
            AccountPaymentApplication application = applicationById.get(payment.getAccountPaymentApplicationId());
            if (application == null) {
                continue;
            }
            Set<Long> ledgerIds = resolvePurchaseLedgerIds(application, stockInRecordById, purchaseLedgerIdByQualityInspectId);
            if (ledgerIds.isEmpty()) {
                continue;
            }
            BigDecimal amount = defaultDecimal(payment.getPaymentAmount());
            for (Long ledgerId : ledgerIds) {
                if (targetLedgerIdSet.contains(ledgerId)) {
                    result.merge(ledgerId, amount, BigDecimal::add);
                }
            }
        }
        return result;
    }
    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 BigDecimal asBigDecimal(Object value) {
        if (value == null) {
            return BigDecimal.ZERO;
        }
        if (value instanceof BigDecimal decimal) {
            return decimal;
        }
        if (value instanceof Number number) {
            return new BigDecimal(String.valueOf(number));
        }
        try {
            return new BigDecimal(String.valueOf(value));
        } catch (Exception ignored) {
            return BigDecimal.ZERO;
        }
    }
    private BigDecimal invoiceAmountOf(AccountPurchaseInvoice invoice) {
        if (invoice == null) {
            return BigDecimal.ZERO;
        }
        BigDecimal amount = defaultDecimal(invoice.getTaxInclusivePrice());
        if (amount.compareTo(BigDecimal.ZERO) > 0) {
            return amount;
        }
        return defaultDecimal(invoice.getTaxExclusivelPrice()).add(defaultDecimal(invoice.getTaxPrice()));
    }
    private List<AccountPurchasePayment> queryPayments(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<AccountPurchasePayment> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountPurchasePayment::getDeptId);
        wrapper.ge(AccountPurchasePayment::getPaymentDate, range.start())
                .le(AccountPurchasePayment::getPaymentDate, range.end())
                .orderByDesc(AccountPurchasePayment::getPaymentDate, AccountPurchasePayment::getId);
        return defaultList(accountPurchasePaymentMapper.selectList(wrapper));
    }
    private List<AccountPurchasePayment> queryPayments(LoginUser loginUser) {
        LambdaQueryWrapper<AccountPurchasePayment> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountPurchasePayment::getDeptId);
        return defaultList(accountPurchasePaymentMapper.selectList(wrapper));
    }
    private List<AccountPurchaseInvoice> queryInvoices(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<AccountPurchaseInvoice> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountPurchaseInvoice::getDeptId);
        wrapper.ge(AccountPurchaseInvoice::getIssueDate, range.start())
                .le(AccountPurchaseInvoice::getIssueDate, range.end())
                .orderByDesc(AccountPurchaseInvoice::getIssueDate, AccountPurchaseInvoice::getId);
        return defaultList(accountPurchaseInvoiceMapper.selectList(wrapper));
    }
    private Map<Integer, AccountPaymentApplication> queryPaymentApplications(List<AccountPurchasePayment> payments) {
        List<Integer> ids = payments.stream()
                .map(AccountPurchasePayment::getAccountPaymentApplicationId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
        if (ids.isEmpty()) {
            return Map.of();
        }
        return defaultList(accountPaymentApplicationMapper.selectBatchIds(ids)).stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(AccountPaymentApplication::getId, item -> item, (a, b) -> a));
    }
    private Map<Long, StockInRecord> queryStockInRecords(Collection<AccountPaymentApplication> applications) {
        Set<Long> stockInRecordIds = new HashSet<>();
        for (AccountPaymentApplication application : applications) {
            stockInRecordIds.addAll(parseLongIds(application.getStockInRecordIds()));
        }
        if (stockInRecordIds.isEmpty()) {
            return Map.of();
        }
        return defaultList(stockInRecordMapper.selectBatchIds(stockInRecordIds)).stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(StockInRecord::getId, item -> item, (a, b) -> a));
    }
    private Map<Long, Long> queryPurchaseLedgerIdByQualityInspectId(Collection<StockInRecord> stockInRecords) {
        Set<Long> qualityInspectIds = stockInRecords.stream()
                .filter(Objects::nonNull)
                .filter(item -> item.getRecordId() != null && "10".equals(safe(item.getRecordType()).trim()))
                .map(StockInRecord::getRecordId)
                .collect(Collectors.toSet());
        if (qualityInspectIds.isEmpty()) {
            return Map.of();
        }
        return defaultList(qualityInspectMapper.selectBatchIds(qualityInspectIds)).stream()
                .filter(item -> item.getId() != null && item.getPurchaseLedgerId() != null)
                .collect(Collectors.toMap(QualityInspect::getId, QualityInspect::getPurchaseLedgerId, (a, b) -> a));
    }
    private Set<Long> resolvePurchaseLedgerIds(AccountPaymentApplication application,
                                               Map<Long, StockInRecord> stockInRecordById,
                                               Map<Long, Long> purchaseLedgerIdByQualityInspectId) {
        Set<Long> result = new LinkedHashSet<>();
        for (Long stockInRecordId : parseLongIds(application.getStockInRecordIds())) {
            StockInRecord stockInRecord = stockInRecordById.get(stockInRecordId);
            if (stockInRecord == null || stockInRecord.getRecordId() == null) {
                continue;
            }
            if (stockInRecord.getApprovalStatus() != null && stockInRecord.getApprovalStatus() != 1) {
                continue;
            }
            String recordType = safe(stockInRecord.getRecordType()).trim();
            if ("7".equals(recordType)) {
                result.add(stockInRecord.getRecordId());
            } else if ("10".equals(recordType)) {
                Long purchaseLedgerId = purchaseLedgerIdByQualityInspectId.get(stockInRecord.getRecordId());
                if (purchaseLedgerId != null) {
                    result.add(purchaseLedgerId);
                }
            }
        }
        return result;
    }
    private List<Long> parseLongIds(String raw) {
        if (!StringUtils.hasText(raw)) {
            return List.of();
        }
        List<Long> result = new ArrayList<>();
        for (String part : raw.split(",")) {
            if (!StringUtils.hasText(part)) {
                continue;
            }
            try {
                result.add(Long.parseLong(part.trim()));
            } catch (Exception ignored) {
            }
        }
        return result;
    }
    private List<PurchaseReturnOrders> queryReturns(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<PurchaseReturnOrders> wrapper = new LambdaQueryWrapper<>();
@@ -197,7 +667,7 @@
    }
    private DateRange resolveDateRange(String startDate, String endDate, String timeRange) {
        LocalDate today = LocalDate.now();
        LocalDate today = LocalDate.now(CHINA_ZONE_ID);
        LocalDate start = parseLocalDate(startDate);
        LocalDate end = parseLocalDate(endDate);
        if (start != null || end != null) {
@@ -214,6 +684,22 @@
            return new DateRange(today.minusDays(29), today, "近30天");
        }
        String text = timeRange.trim();
        if (text.contains("今天")) {
            return new DateRange(today, today, "今天");
        }
        if (text.contains("昨天")) {
            LocalDate yesterday = today.minusDays(1);
            return new DateRange(yesterday, yesterday, "昨天");
        }
        if (text.contains("本周")) {
            LocalDate startOfWeek = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            return new DateRange(startOfWeek, today, "本周");
        }
        if (text.contains("上周")) {
            LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            LocalDate startOfLastWeek = thisWeekStart.minusWeeks(1);
            return new DateRange(startOfLastWeek, startOfLastWeek.plusDays(6), "上周");
        }
        if (text.contains("今年") || text.contains("本年")) {
            return new DateRange(today.withDayOfYear(1), today, "今年");
        }
@@ -231,6 +717,19 @@
        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天");
    }
@@ -238,11 +737,19 @@
        if (!StringUtils.hasText(text)) {
            return null;
        }
        return LocalDate.parse(text.trim(), DATE_FMT);
        try {
            return LocalDate.parse(text.trim(), DATE_FMT);
        } catch (Exception ignored) {
            return null;
        }
    }
    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) {
@@ -312,4 +819,28 @@
    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;
        }
    }
}