6 天以前 e93af9a26dfdedfa47c982151e8f8f0b3d65645b
fix(ai): 解决记录类型字符串前后空格导致的数据映射问题

- 在PurchaseAgentTools中对recordType进行trim操作以避免空格影响类型判断
- 在SalesAgentTools中同样对recordType进行trim处理确保类型匹配准确性
- 移除SalesAgentTools中废弃的客户往来查询逻辑代码
- 更新销售AI模块返回格式适配新的财务数据模型
- 添加财务升级AI模块前端变更联调文档说明新旧数据源切换细节
已添加1个文件
已修改2个文件
266 ■■■■■ 文件已修改
doc/20260522_财务升级AI模块前端变更联调文档.md 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260522_²ÆÎñÉý¼¶AIÄ£¿éǰ¶Ë±ä¸üÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,150 @@
# è´¢åŠ¡æ¨¡å—å‡çº§åŽ AI æ¨¡å—前端变更联调文档(采购/销售/生产/待办)
更新日期:2026-05-22
适用范围:`/sales-ai`、`/purchase-ai`、`/manufacturing-ai`、`/xiaozhi`(审批待办)
## 1. å˜æ›´æ€»è§ˆ
| æ¨¡å— | å¯¹å¤–接口 | æ˜¯å¦éœ€è¦å‰ç«¯æ”¹é€  | ç»“论 |
| --- | --- | --- | --- |
| é”€å”® AI | `POST /sales-ai/chat` | æ˜¯ | è´¢åŠ¡å£å¾„åˆ‡æ¢åˆ°æ–°æ”¶æ¬¾æ¨¡åž‹ï¼Œéƒ¨åˆ† `type` çš„字段语义变化 |
| é‡‡è´­ AI | `POST /purchase-ai/chat` | æ˜¯ | ä»˜æ¬¾/发票/待付款计算切换到新财务链路,统计值从占位改为真实值 |
| ç”Ÿäº§ AI | `POST /manufacturing-ai/chat` | å¦ | å·²æ ¸æŸ¥ï¼Œæ— æ—§è´¢åŠ¡é€»è¾‘ä¾èµ–ï¼Œæ— å­—æ®µå˜æ›´ |
| å¾…办 AI | `POST /xiaozhi/chat` | å¦ | å·²æ ¸æŸ¥ï¼Œæ— æ—§è´¢åŠ¡é€»è¾‘ä¾èµ–ï¼Œæ— å­—æ®µå˜æ›´ |
## 2. é”€å”® AI å˜æ›´ï¼ˆ`/sales-ai/chat`)
### 2.1 `type = sales_return_list`(销售退款/回款记录)
当前返回数据来源统一为新财务表 `account_sales_collection`,不再走旧收款退货逻辑。
`data.items[]` å…³é”®å­—段:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
| --- | --- | --- |
| id | number | æ”¶æ¬¾è®°å½•ID |
| refundId | string | æ˜ å°„ `collectionNumber`,前端可继续作为“退款/回款单号”展示 |
| collectionNumber | string | æ”¶æ¬¾å•号 |
| paymentMethod | string | æ”¶æ¬¾æ–¹å¼ |
| actualAmount | number | æ”¶æ¬¾é‡‘额(与 `collectionAmount` åŒå€¼ï¼‰ |
| collectionAmount | number | æ”¶æ¬¾é‡‘额(推荐主展示字段) |
| customerId | number | å®¢æˆ·ID |
| remark | string | å¤‡æ³¨ |
| createTime | string | æ”¶æ¬¾æ—¥æœŸï¼ˆyyyy-MM-dd) |
`summary` å¢žé‡å…³æ³¨ï¼š
- `returnAmount`:时间范围内金额汇总(按 `collectionAmount` ç»Ÿè®¡ï¼‰
### 2.2 `type = sales_customer_interaction_list`(客户往来)
当前返回基于新链路:
`account_sales_collection.stock_out_record_ids -> stock_out_record(record_type=13) -> shipping_info -> sales_ledger`
返回约定:
- æ— æ•°æ®æ—¶ï¼š`description = "no_customer_interactions"`
- æœ‰æ•°æ®æ—¶ï¼š`description = "ok"`
`summary` å…³é”®å­—段:
- `totalReceiptAmount`
- `customerCount`
`data.items[]` å…³é”®å­—段:
- `salesLedgerId`
- `salesContractNo`
- `customerName`
- `projectName`
- `receiptPaymentDate`
- `receiptPaymentAmount`
- `receiptPaymentType`
- `collectionNumber`
- `registrant`
- `remark`
### 2.3 `type = sales_ledger_list`(销售台账)
字段结构不变,但金额口径已切换:
- `receivedAmount` ç”±æ–°æ”¶æ¬¾æ¨¡åž‹æ±‡æ€»å¾—到;
- `pendingAmount = max(0, invoicedAmount - receivedAmount)`;
- è‹¥æ”¶æ¬¾è®°å½•未显式关联台账,则按客户维度兜底归集。
前端改造建议:
- ä¸æ”¹å­—段名;
- é‡ç‚¹å›žå½’“已收金额/待回款金额”是否与财务台账一致。
## 3. é‡‡è´­ AI å˜æ›´ï¼ˆ`/purchase-ai/chat`)
### 3.1 `type = purchase_stats`(采购统计)
以下字段已从占位值改为真实统计值:
- `summary.paymentCount`
- `summary.invoiceCount`
- `summary.paymentAmount`
- `summary.invoiceAmount`
发票金额口径:
- ä¼˜å…ˆ `taxInclusivePrice`
- è‹¥ä¸ºç©º/0,则使用 `taxExclusivelPrice + taxPrice`
### 3.2 `type = purchase_pending_payment_list`(待付款采购单)
核心计算已切换到新财务链路:
`account_purchase_payment -> account_payment_application -> stock_in_record -> (purchase_ledger / quality_inspect) -> purchase_ledger_id`
映射规则:
1. `stock_in_record.record_type = 7`:`record_id` ç›´æŽ¥è§†ä¸º `purchase_ledger_id`
2. `stock_in_record.record_type = 10`:通过 `quality_inspect.id = record_id` å– `quality_inspect.purchase_ledger_id`
金额字段口径:
- `paidAmount`:新链路累计已付款金额
- `pendingAmount = contractAmount - paidAmount`(<=0 çš„记录不返回)
`summary` å…³é”®å­—段(均为真实值):
- `pendingOrderCount`
- `totalContractAmount`
- `totalPaidAmount`
- `totalPendingAmount`
### 3.3 æ•°æ®æ¸…洗修复
已修复 `record_type` å¸¦ç©ºæ ¼å¯¼è‡´çš„æ˜ å°„丢失问题(后端统一 `trim()` åŽå†åˆ¤æ–­ `7/10`)。
## 4. ç”Ÿäº§ AI / å¾…办 AI æ ¸æŸ¥ç»“论
已核查以下模块代码,未发现旧财务逻辑耦合点:
- `ManufacturingAgentTools`(生产)
- `ApproveTodoTools`(待办审批)
结论:
- å¯¹å¤– `type` ä¸Žå­—段结构无变更;
- å‰ç«¯æ— éœ€åšå…¼å®¹æ”¹é€ ï¼Œä»…需做一次回归验证。
## 5. å‰ç«¯è”调要点
1. `/sales-ai/chat`、`/purchase-ai/chat` ç»§ç»­æŒ‰ SSE æ–‡æœ¬æµæ‹¼æŽ¥åŽåš JSON è§£æžã€‚
2. æŒ‰ `type` è·¯ç”±æ¸²æŸ“,不要仅依赖 `description` æ–‡æ¡ˆã€‚
3. `sales_customer_interaction_list` éœ€å…¼å®¹ `description` æžšä¸¾ï¼š`ok` / `no_customer_interactions`。
4. `sales_return_list` é‡‘额展示统一用 `collectionAmount`(`actualAmount` ä¿ç•™å…¼å®¹ï¼‰ã€‚
5. `purchase_pending_payment_list` çš„æ±‡æ€»å¡ç‰‡è¯·ç›´æŽ¥è¯»å– `summary.totalPendingAmount` ç­‰å­—段,不再前端二次估算。
## 6. å›žå½’清单(建议)
### é”€å”®
1. æé—®ï¼šâ€œè¿‘30天哪个订单回款最少”
   - æ ¡éªŒ `sales_ledger_list` çš„ `receivedAmount/pendingAmount`。
2. æé—®ï¼šâ€œæŸ¥è¯¢æœ¬æœˆé”€å”®é€€æ¬¾â€
   - æ ¡éªŒ `sales_return_list` çš„ `collectionNumber/collectionAmount/returnAmount`。
3. æé—®ï¼šâ€œæŸ¥è¯¢æœ¬æœˆå®¢æˆ·å¾€æ¥â€
   - æ ¡éªŒ `sales_customer_interaction_list` çš„ `totalReceiptAmount/customerCount`。
### é‡‡è´­
1. æé—®ï¼šâ€œç»Ÿè®¡æœ¬æœˆé‡‡è´­æ•°æ®â€
   - æ ¡éªŒ `purchase_stats` çš„ `paymentCount/invoiceCount/paymentAmount/invoiceAmount` éžå›ºå®š0。
2. æé—®ï¼šâ€œåˆ—出待付款采购单”
   - æ ¡éªŒ `purchase_pending_payment_list` çš„ `paidAmount/pendingAmount` ä¸Žè´¢åŠ¡å®žé™…ä¸€è‡´ã€‚
### ç”Ÿäº§/待办
1. ç”Ÿäº§æé—®ï¼šâ€œæŸ¥è¯¢æœ¬å‘¨è®¾å¤‡ç»´ä¿®è®°å½•”
2. å¾…办提问:“查询我的待审批列表”
   - æ ¡éªŒè¿”回结构与升级前一致(无字段破坏)。
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
@@ -591,7 +591,7 @@
    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())))
                .filter(item -> item.getRecordId() != null && "10".equals(safe(item.getRecordType()).trim()))
                .map(StockInRecord::getRecordId)
                .collect(Collectors.toSet());
        if (qualityInspectIds.isEmpty()) {
@@ -614,7 +614,7 @@
            if (stockInRecord.getApprovalStatus() != null && stockInRecord.getApprovalStatus() != 1) {
                continue;
            }
            String recordType = safe(stockInRecord.getRecordType());
            String recordType = safe(stockInRecord.getRecordType()).trim();
            if ("7".equals(recordType)) {
                result.add(stockInRecord.getRecordId());
            } else if ("10".equals(recordType)) {
src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java
@@ -121,60 +121,6 @@
                                      @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);
@@ -398,59 +344,7 @@
                .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);
//        wrapper.ge(ReceiptPayment::getReceiptPaymentDate, range.start())
//                .le(ReceiptPayment::getReceiptPaymentDate, range.end())
//                .orderByDesc(ReceiptPayment::getReceiptPaymentDate, ReceiptPayment::getId);
//        List<ReceiptPayment> payments = defaultList(receiptPaymentMapper.selectList(wrapper));
//        if (payments.isEmpty()) {
//            return jsonResponse(true, "sales_customer_interaction_list", "未查询到客户往来记录", rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
//        }
//
//        List<Long> ledgerIds = payments.stream()
//                .map(ReceiptPayment::getSalesLedgerId)
//                .filter(Objects::nonNull)
//                .distinct()
//                .collect(Collectors.toList());
//        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));
//
//        List<ReceiptPayment> filtered = payments.stream()
//                .filter(item -> matchInteractionKeyword(item, ledgerMap.get(item.getSalesLedgerId()), keyword))
//                .limit(normalizeLimit(limit))
//                .collect(Collectors.toList());
//
//        BigDecimal totalReceiptAmount = filtered.stream()
//                .map(ReceiptPayment::getReceiptPaymentAmount)
//                .filter(Objects::nonNull)
//                .reduce(BigDecimal.ZERO, BigDecimal::add);
//        List<Map<String, Object>> items = filtered.stream().map(item -> {
//            SalesLedger ledger = ledgerMap.get(item.getSalesLedgerId());
//            Map<String, Object> map = new LinkedHashMap<>();
//            map.put("id", item.getId());
//            map.put("salesLedgerId", item.getSalesLedgerId());
//            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(item.getReceiptPaymentDate()));
//            map.put("receiptPaymentAmount", item.getReceiptPaymentAmount());
//            map.put("receiptPaymentType", safe(item.getReceiptPaymentType()));
//            map.put("registrant", safe(item.getRegistrant()));
//            return map;
//        }).collect(Collectors.toList());
//        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());
//        return jsonResponse(true, "sales_customer_interaction_list", "已返回客户往来明细", summary, Map.of("items", items), Map.of());
        return jsonResponse(true, "sales_customer_interaction_list", "已返回客户往来明细", null, Map.of("items", List.of()), Map.of());
    }
    @Tool(name = "查询发货台账", value = "按关键词和时间范围查询发货台账")
@@ -965,12 +859,6 @@
            return Map.of();
        }
        Map<Long, BigDecimal> result = new HashMap<>();
//        for (InvoiceLedgerDto item : defaultList(invoiceLedgerMapper.invoicedTotal(ledgerIds))) {
//            if (item.getSalesLedgerId() == null) {
//                continue;
//            }
//            result.merge(item.getSalesLedgerId().longValue(), defaultDecimal(item.getInvoiceTotal()), BigDecimal::add);
//        }
        return result;
    }