6 天以前 200997dac09a7e4d373a6f7709733d57567b38f8
feat(ai): 更新采购和销售AI工具的数据查询功能

- 在PurchaseAgentTools中添加账户支付、付款申请、采购发票相关依赖注入
- 在PurchaseAgentTools中集成质检和入库记录查询功能
- 重构采购待付款项计算逻辑,支持按采购台账ID汇总支付金额
- 在SalesAgentTools中替换收款退货为销售收款相关数据查询
- 添加销售出库记录查询和客户交互数据统计功能
- 实现销售收款与销售台账关联映射逻辑
- 优化数据查询接口,统一使用新的账户模块实体类
- 添加安全的数字转换方法防止数据解析异常
已修改2个文件
623 ■■■■■ 文件已修改
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java 243 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java 380 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
@@ -2,6 +2,12 @@
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;
@@ -13,8 +19,12 @@
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
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;
@@ -41,6 +51,11 @@
    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,
@@ -48,12 +63,22 @@
                              SalesLedgerProductMapper salesLedgerProductMapper,
                              ProcurementRecordMapper procurementRecordMapper,
                              InboundManagementMapper inboundManagementMapper,
                              AccountPurchasePaymentMapper accountPurchasePaymentMapper,
                              AccountPaymentApplicationMapper accountPaymentApplicationMapper,
                              AccountPurchaseInvoiceMapper accountPurchaseInvoiceMapper,
                              StockInRecordMapper stockInRecordMapper,
                              QualityInspectMapper qualityInspectMapper,
                              AiSessionUserContext aiSessionUserContext) {
        this.purchaseLedgerMapper = purchaseLedgerMapper;
        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;
    }
@@ -115,24 +140,22 @@
        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()
                .map(PurchaseLedger::getContractAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal paymentAmount = BigDecimal.ZERO;
//        BigDecimal paymentAmount = payments.stream()
//                .map(PaymentRegistration::getCurrentPaymentAmount)
//                .filter(Objects::nonNull)
//                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal invoiceAmount = BigDecimal.ZERO;
//        BigDecimal invoiceAmount = invoices.stream()
//                .map(InvoicePurchase::getInvoiceAmount)
//                .filter(Objects::nonNull)
//                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal paymentAmount = payments.stream()
                .map(AccountPurchasePayment::getPaymentAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal invoiceAmount = invoices.stream()
                .map(this::invoiceAmountOf)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal returnAmount = returns.stream()
                .map(PurchaseReturnOrders::getTotalAmount)
                .filter(Objects::nonNull)
@@ -143,10 +166,8 @@
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("ledgerCount", ledgers.size());
        summary.put("paymentCount", 0);
//        summary.put("paymentCount", payments.size());
//        summary.put("invoiceCount", invoices.size());
        summary.put("invoiceCount", 0);
        summary.put("paymentCount", payments.size());
        summary.put("invoiceCount", invoices.size());
        summary.put("returnCount", returns.size());
        summary.put("contractAmount", contractAmount);
        summary.put("paymentAmount", paymentAmount);
@@ -268,9 +289,15 @@
                                           @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()
        List<PurchaseLedger> matchedLedgers = queryLedgers(loginUser, range).stream()
                .filter(ledger -> matchLedgerKeyword(ledger, keyword))
                .map(ledger -> toPendingPaymentItem(loginUser, ledger))
                .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))
@@ -411,28 +438,58 @@
        return map;
    }
    private Map<String, Object> toPendingPaymentItem(LoginUser loginUser, PurchaseLedger ledger) {
    private Map<String, Object> toPendingPaymentItem(PurchaseLedger ledger, BigDecimal paidAmount) {
        BigDecimal contractAmount = defaultDecimal(ledger.getContractAmount());
        BigDecimal paidAmount = sumPaymentAmount(loginUser, ledger.getId());
        BigDecimal pendingAmount = contractAmount.subtract(paidAmount);
        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", paidAmount);
        item.put("paidAmount", safePaidAmount);
        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);
        return BigDecimal.ZERO;
    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) {
@@ -462,7 +519,129 @@
        if (value instanceof Number number) {
            return new BigDecimal(String.valueOf(number));
        }
        return BigDecimal.ZERO;
        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())))
                .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());
            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;
    }
src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java
@@ -3,8 +3,8 @@
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.ruoyi.account.mapper.SalesReceiptReturnMapper;
import com.ruoyi.account.pojo.SalesReceiptReturn;
import com.ruoyi.account.mapper.sales.AccountSalesCollectionMapper;
import com.ruoyi.account.pojo.sales.AccountSalesCollection;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.basic.dto.CustomerDto;
import com.ruoyi.basic.mapper.CustomerMapper;
@@ -17,6 +17,8 @@
import com.ruoyi.sales.pojo.SalesLedger;
import com.ruoyi.sales.pojo.SalesQuotation;
import com.ruoyi.sales.pojo.ShippingInfo;
import com.ruoyi.stock.mapper.StockOutRecordMapper;
import com.ruoyi.stock.pojo.StockOutRecord;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolMemoryId;
@@ -49,20 +51,23 @@
    private final SalesLedgerMapper salesLedgerMapper;
    private final SalesQuotationMapper salesQuotationMapper;
    private final ShippingInfoMapper shippingInfoMapper;
    private final SalesReceiptReturnMapper salesReceiptReturnMapper;
    private final AccountSalesCollectionMapper accountSalesCollectionMapper;
    private final StockOutRecordMapper stockOutRecordMapper;
    private final AiSessionUserContext aiSessionUserContext;
    public SalesAgentTools(CustomerMapper customerMapper,
                           SalesLedgerMapper salesLedgerMapper,
                           SalesQuotationMapper salesQuotationMapper,
                           ShippingInfoMapper shippingInfoMapper,
                           SalesReceiptReturnMapper salesReceiptReturnMapper,
                           AccountSalesCollectionMapper accountSalesCollectionMapper,
                           StockOutRecordMapper stockOutRecordMapper,
                           AiSessionUserContext aiSessionUserContext) {
        this.customerMapper = customerMapper;
        this.salesLedgerMapper = salesLedgerMapper;
        this.salesQuotationMapper = salesQuotationMapper;
        this.shippingInfoMapper = shippingInfoMapper;
        this.salesReceiptReturnMapper = salesReceiptReturnMapper;
        this.accountSalesCollectionMapper = accountSalesCollectionMapper;
        this.stockOutRecordMapper = stockOutRecordMapper;
        this.aiSessionUserContext = aiSessionUserContext;
    }
@@ -116,6 +121,60 @@
                                      @P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, null);
        /*
        List<AccountSalesCollection> collections = queryCollections(loginUser, range);
        if (collections.isEmpty()) {
            return jsonResponse(true, "sales_customer_interaction_list", "鏈煡璇㈠埌瀹㈡埛寰€鏉ヨ褰?, rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
        }
        Map<Integer, Set<Long>> ledgerIdsByCollectionId = mapCollectionLedgerIds(loginUser, collections);
        Set<Long> ledgerIds = ledgerIdsByCollectionId.values().stream()
                .flatMap(Collection::stream)
                .collect(Collectors.toSet());
        Map<Long, SalesLedger> ledgerMap = defaultList(salesLedgerMapper.selectBatchIds(ledgerIds)).stream()
                .filter(ledger -> tenantMatched(ledger.getTenantId(), loginUser.getTenantId()))
                .collect(Collectors.toMap(SalesLedger::getId, item -> item, (a, b) -> a, LinkedHashMap::new));
        int finalLimit = normalizeLimit(limit);
        List<Map<String, Object>> items = new ArrayList<>();
        for (AccountSalesCollection collection : collections) {
            Set<Long> relatedLedgerIds = ledgerIdsByCollectionId.get(collection.getId());
            if (relatedLedgerIds == null || relatedLedgerIds.isEmpty()) {
                if (!matchInteractionKeyword(collection, null, keyword)) {
                    continue;
                }
                items.add(toInteractionItem(collection, null));
                if (items.size() >= finalLimit) {
                    break;
                }
                continue;
            }
            for (Long ledgerId : relatedLedgerIds) {
                SalesLedger ledger = ledgerMap.get(ledgerId);
                if (ledger == null || !matchInteractionKeyword(collection, ledger, keyword)) {
                    continue;
                }
                items.add(toInteractionItem(collection, ledger));
                if (items.size() >= finalLimit) {
                    break;
                }
            }
            if (items.size() >= finalLimit) {
                break;
            }
        }
        BigDecimal totalReceiptAmount = items.stream()
                .map(item -> asBigDecimal(item.get("receiptPaymentAmount")))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
        summary.put("totalReceiptAmount", totalReceiptAmount);
        summary.put("customerCount", items.stream()
                .map(item -> String.valueOf(item.get("customerName")))
                .filter(StringUtils::hasText)
                .distinct()
                .count());
        */
        LambdaQueryWrapper<SalesQuotation> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), SalesQuotation::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesQuotation::getDeptId);
@@ -242,36 +301,35 @@
                                   @P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, null);
        LambdaQueryWrapper<SalesReceiptReturn> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesReceiptReturn::getDeptId);
        LambdaQueryWrapper<AccountSalesCollection> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountSalesCollection::getDeptId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(SalesReceiptReturn::getRefundId, keyword)
                    .or().like(SalesReceiptReturn::getTransactionNo, keyword)
                    .or().like(SalesReceiptReturn::getPaymentAccountName, keyword));
            wrapper.and(w -> w.like(AccountSalesCollection::getCollectionNumber, keyword)
                    .or().like(AccountSalesCollection::getCollectionMethod, keyword)
                    .or().like(AccountSalesCollection::getRemark, keyword));
        }
        wrapper.ge(SalesReceiptReturn::getCreateTime, range.start().atStartOfDay())
                .le(SalesReceiptReturn::getCreateTime, range.end().atTime(23, 59, 59))
                .orderByDesc(SalesReceiptReturn::getCreateTime, SalesReceiptReturn::getId)
        wrapper.ge(AccountSalesCollection::getCollectionDate, range.start())
                .le(AccountSalesCollection::getCollectionDate, range.end())
                .orderByDesc(AccountSalesCollection::getCollectionDate, AccountSalesCollection::getId)
                .last("limit " + normalizeLimit(limit));
        List<SalesReceiptReturn> rows = defaultList(salesReceiptReturnMapper.selectList(wrapper));
        List<AccountSalesCollection> rows = defaultList(accountSalesCollectionMapper.selectList(wrapper));
        BigDecimal returnAmount = rows.stream()
                .map(SalesReceiptReturn::getActualAmount)
                .map(AccountSalesCollection::getCollectionAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        List<Map<String, Object>> items = rows.stream().map(item -> {
            Map<String, Object> map = new LinkedHashMap<>();
            map.put("id", item.getId());
            map.put("refundId", safe(item.getRefundId()));
            map.put("paymentAccount", safe(item.getPaymentAccount()));
            map.put("paymentAccountName", safe(item.getPaymentAccountName()));
            map.put("paymentMethod", item.getPaymentMethod());
            map.put("actualAmount", item.getActualAmount());
            map.put("fee", item.getFee());
            map.put("discountAmount", item.getDiscountAmount());
            map.put("transactionNo", safe(item.getTransactionNo()));
            map.put("createTime", formatDateTime(item.getCreateTime()));
            map.put("refundId", safe(item.getCollectionNumber()));
            map.put("collectionNumber", safe(item.getCollectionNumber()));
            map.put("paymentMethod", safe(item.getCollectionMethod()));
            map.put("actualAmount", item.getCollectionAmount());
            map.put("collectionAmount", item.getCollectionAmount());
            map.put("customerId", item.getCustomerId());
            map.put("remark", safe(item.getRemark()));
            map.put("createTime", formatDate(item.getCollectionDate()));
            return map;
        }).collect(Collectors.toList());
@@ -288,6 +346,61 @@
                                           @P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, null);
        List<AccountSalesCollection> collections = queryCollections(loginUser, range);
        if (collections.isEmpty()) {
            return jsonResponse(true, "sales_customer_interaction_list", "no_customer_interactions", rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
        }
        Map<Integer, Set<Long>> ledgerIdsByCollectionId = mapCollectionLedgerIds(loginUser, collections);
        Set<Long> ledgerIds = ledgerIdsByCollectionId.values().stream()
                .flatMap(Collection::stream)
                .collect(Collectors.toSet());
        Map<Long, SalesLedger> ledgerMap = defaultList(salesLedgerMapper.selectBatchIds(ledgerIds)).stream()
                .filter(ledger -> tenantMatched(ledger.getTenantId(), loginUser.getTenantId()))
                .collect(Collectors.toMap(SalesLedger::getId, item -> item, (a, b) -> a, LinkedHashMap::new));
        int finalLimit = normalizeLimit(limit);
        List<Map<String, Object>> items = new ArrayList<>();
        for (AccountSalesCollection collection : collections) {
            Set<Long> relatedLedgerIds = ledgerIdsByCollectionId.get(collection.getId());
            if (relatedLedgerIds == null || relatedLedgerIds.isEmpty()) {
                if (!matchInteractionKeyword(collection, null, keyword)) {
                    continue;
                }
                items.add(toInteractionItem(collection, null));
                if (items.size() >= finalLimit) {
                    break;
                }
                continue;
            }
            for (Long ledgerId : relatedLedgerIds) {
                SalesLedger ledger = ledgerMap.get(ledgerId);
                if (ledger == null || !matchInteractionKeyword(collection, ledger, keyword)) {
                    continue;
                }
                items.add(toInteractionItem(collection, ledger));
                if (items.size() >= finalLimit) {
                    break;
                }
            }
            if (items.size() >= finalLimit) {
                break;
            }
        }
        BigDecimal totalReceiptAmount = items.stream()
                .map(item -> asBigDecimal(item.get("receiptPaymentAmount")))
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
        summary.put("totalReceiptAmount", totalReceiptAmount);
        summary.put("customerCount", items.stream()
                .map(item -> String.valueOf(item.get("customerName")))
                .filter(StringUtils::hasText)
                .distinct()
                .count());
        if (summary.size() >= 0) {
            return jsonResponse(true, "sales_customer_interaction_list", "ok", summary, Map.of("items", items), Map.of());
        }
//        LambdaQueryWrapper<ReceiptPayment> wrapper = new LambdaQueryWrapper<>();
//        applyTenantFilter(wrapper, loginUser.getTenantId(), ReceiptPayment::getTenantId);
//        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ReceiptPayment::getDeptId);
@@ -862,9 +975,209 @@
    }
    private Map<Long, BigDecimal> sumReceiptAmounts(LoginUser loginUser, List<Long> ledgerIds) {
        Map<Long, BigDecimal> result = new HashMap<>();
        if (ledgerIds == null || ledgerIds.isEmpty()) {
            return Map.of();
        }
        List<SalesLedger> ledgers = defaultList(salesLedgerMapper.selectBatchIds(ledgerIds)).stream()
                .filter(ledger -> tenantMatched(ledger.getTenantId(), loginUser.getTenantId()))
                .collect(Collectors.toList());
        if (ledgers.isEmpty()) {
            return Map.of();
        }
        Set<Integer> customerIds = ledgers.stream()
                .map(SalesLedger::getCustomerId)
                .filter(Objects::nonNull)
                .map(Long::intValue)
                .collect(Collectors.toSet());
        if (customerIds.isEmpty()) {
            return Map.of();
        }
        LambdaQueryWrapper<AccountSalesCollection> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountSalesCollection::getDeptId);
        wrapper.in(AccountSalesCollection::getCustomerId, customerIds);
        List<AccountSalesCollection> collections = defaultList(accountSalesCollectionMapper.selectList(wrapper));
        if (collections.isEmpty()) {
            return Map.of();
        }
        Map<Integer, Set<Long>> ledgerIdsByCollectionId = mapCollectionLedgerIds(loginUser, collections);
        Map<Long, List<Long>> ledgerIdsByCustomerId = ledgers.stream()
                .filter(item -> item.getId() != null && item.getCustomerId() != null)
                .collect(Collectors.groupingBy(item -> item.getCustomerId().longValue(),
                        Collectors.mapping(SalesLedger::getId, Collectors.toList())));
        Set<Long> targetLedgerIdSet = new HashSet<>(ledgerIds);
        Map<Long, BigDecimal> result = new HashMap<>();
        for (AccountSalesCollection collection : collections) {
            BigDecimal amount = defaultDecimal(collection.getCollectionAmount());
            if (amount.compareTo(BigDecimal.ZERO) == 0) {
                continue;
            }
            Set<Long> relatedLedgerIds = ledgerIdsByCollectionId.getOrDefault(collection.getId(), Set.of());
            if (!relatedLedgerIds.isEmpty()) {
                for (Long ledgerId : relatedLedgerIds) {
                    if (targetLedgerIdSet.contains(ledgerId)) {
                        result.merge(ledgerId, amount, BigDecimal::add);
                    }
                }
                continue;
            }
            if (collection.getCustomerId() == null) {
                continue;
            }
            List<Long> customerLedgerIds = ledgerIdsByCustomerId.get(collection.getCustomerId().longValue());
            if (customerLedgerIds == null || customerLedgerIds.isEmpty()) {
                continue;
            }
            for (Long ledgerId : customerLedgerIds) {
                if (targetLedgerIdSet.contains(ledgerId)) {
                    result.merge(ledgerId, amount, BigDecimal::add);
                }
            }
        }
        return result;
    }
    private List<AccountSalesCollection> queryCollections(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<AccountSalesCollection> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), AccountSalesCollection::getDeptId);
        if (range != null) {
            wrapper.ge(AccountSalesCollection::getCollectionDate, range.start())
                    .le(AccountSalesCollection::getCollectionDate, range.end());
        }
        wrapper.orderByDesc(AccountSalesCollection::getCollectionDate, AccountSalesCollection::getId);
        return defaultList(accountSalesCollectionMapper.selectList(wrapper));
    }
    private Map<Integer, Set<Long>> mapCollectionLedgerIds(LoginUser loginUser, List<AccountSalesCollection> collections) {
        Map<Integer, Set<Long>> result = new HashMap<>();
        if (collections == null || collections.isEmpty()) {
            return result;
        }
        Map<Integer, List<Long>> stockOutRecordIdsByCollection = new HashMap<>();
        Set<Long> allStockOutRecordIds = new HashSet<>();
        for (AccountSalesCollection collection : collections) {
            if (collection.getId() == null) {
                continue;
            }
            List<Long> stockOutRecordIds = parseLongIds(collection.getStockOutRecordIds());
            if (stockOutRecordIds.isEmpty()) {
                continue;
            }
            stockOutRecordIdsByCollection.put(collection.getId(), stockOutRecordIds);
            allStockOutRecordIds.addAll(stockOutRecordIds);
        }
        if (allStockOutRecordIds.isEmpty()) {
            return result;
        }
        List<StockOutRecord> stockOutRecords = defaultList(stockOutRecordMapper.selectList(new LambdaQueryWrapper<StockOutRecord>()
                .in(StockOutRecord::getId, allStockOutRecordIds)));
        if (stockOutRecords.isEmpty()) {
            return result;
        }
        Map<Long, StockOutRecord> stockOutRecordMap = stockOutRecords.stream()
                .filter(item -> item.getId() != null)
                .collect(Collectors.toMap(StockOutRecord::getId, item -> item, (a, b) -> a));
        Set<Long> shippingIds = stockOutRecords.stream()
                .filter(this::isSalesOutboundRecord)
                .map(StockOutRecord::getRecordId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (shippingIds.isEmpty()) {
            return result;
        }
        LambdaQueryWrapper<ShippingInfo> shippingWrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(shippingWrapper, loginUser.getTenantId(), ShippingInfo::getTenantId);
        applyDeptFilter(shippingWrapper, loginUser.getCurrentDeptId(), ShippingInfo::getDeptId);
        shippingWrapper.in(ShippingInfo::getId, shippingIds);
        Map<Long, Long> ledgerIdByShippingId = defaultList(shippingInfoMapper.selectList(shippingWrapper)).stream()
                .filter(item -> item.getId() != null && item.getSalesLedgerId() != null)
                .collect(Collectors.toMap(ShippingInfo::getId, ShippingInfo::getSalesLedgerId, (a, b) -> a));
        for (Map.Entry<Integer, List<Long>> entry : stockOutRecordIdsByCollection.entrySet()) {
            Set<Long> ledgerIds = new LinkedHashSet<>();
            for (Long stockOutRecordId : entry.getValue()) {
                StockOutRecord stockOutRecord = stockOutRecordMap.get(stockOutRecordId);
                if (!isSalesOutboundRecord(stockOutRecord)) {
                    continue;
                }
                Long ledgerId = ledgerIdByShippingId.get(stockOutRecord.getRecordId());
                if (ledgerId != null) {
                    ledgerIds.add(ledgerId);
                }
            }
            if (!ledgerIds.isEmpty()) {
                result.put(entry.getKey(), ledgerIds);
            }
        }
        return result;
    }
    private boolean isSalesOutboundRecord(StockOutRecord stockOutRecord) {
        if (stockOutRecord == null || !StringUtils.hasText(stockOutRecord.getRecordType())) {
            return false;
        }
        if (stockOutRecord.getApprovalStatus() != null && stockOutRecord.getApprovalStatus() != 1) {
            return false;
        }
        return "13".equals(stockOutRecord.getRecordType().trim());
    }
    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 boolean matchInteractionKeyword(AccountSalesCollection collection, SalesLedger ledger, String keyword) {
        if (!StringUtils.hasText(keyword)) {
            return true;
        }
        String text = keyword.trim();
        if (safe(collection.getCollectionNumber()).contains(text)
                || safe(collection.getCollectionMethod()).contains(text)
                || safe(collection.getRemark()).contains(text)) {
            return true;
        }
        if (ledger == null) {
            return false;
        }
        return safe(ledger.getSalesContractNo()).contains(text)
                || safe(ledger.getCustomerName()).contains(text)
                || safe(ledger.getProjectName()).contains(text);
    }
    private Map<String, Object> toInteractionItem(AccountSalesCollection collection, SalesLedger ledger) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", collection.getId());
        map.put("salesLedgerId", ledger == null ? null : ledger.getId());
        map.put("salesContractNo", ledger == null ? "" : safe(ledger.getSalesContractNo()));
        map.put("customerName", ledger == null ? "" : safe(ledger.getCustomerName()));
        map.put("projectName", ledger == null ? "" : safe(ledger.getProjectName()));
        map.put("receiptPaymentDate", formatDate(collection.getCollectionDate()));
        map.put("receiptPaymentAmount", collection.getCollectionAmount());
        map.put("receiptPaymentType", safe(collection.getCollectionMethod()));
        map.put("collectionNumber", safe(collection.getCollectionNumber()));
        map.put("registrant", collection.getCreateUser());
        map.put("remark", safe(collection.getRemark()));
        return map;
    }
    private boolean isLedgerFullyShipped(Long ledgerId, Map<Long, List<ShippingInfo>> shippingByLedgerId) {
@@ -1090,6 +1403,23 @@
        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 maxZero(BigDecimal value) {
        return value == null || value.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : value;
    }