| | |
| | | package com.ruoyi.ai.service; |
| | | |
| | | import com.alibaba.fastjson2.JSON; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.fasterxml.jackson.core.type.TypeReference; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | |
| | | import com.ruoyi.basic.service.StorageBlobService; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.framework.security.LoginUser; |
| | | import com.ruoyi.framework.web.domain.AjaxResult; |
| | | import com.ruoyi.framework.web.domain.R; |
| | | import com.ruoyi.purchase.dto.PurchaseLedgerDto; |
| | | import com.ruoyi.purchase.dto.PurchaseReturnOrderDto; |
| | | import com.ruoyi.purchase.pojo.PaymentRegistration; |
| | |
| | | import com.ruoyi.purchase.service.PurchaseReturnOrdersService; |
| | | import com.ruoyi.sales.pojo.SalesLedgerProduct; |
| | | import dev.langchain4j.data.image.Image; |
| | | import dev.langchain4j.data.message.AiMessage; |
| | | import dev.langchain4j.data.message.ChatMessage; |
| | | import dev.langchain4j.data.message.Content; |
| | | import dev.langchain4j.data.message.ImageContent; |
| | | import dev.langchain4j.data.message.SystemMessage; |
| | | import dev.langchain4j.data.message.TextContent; |
| | | import dev.langchain4j.data.message.UserMessage; |
| | | import dev.langchain4j.data.message.*; |
| | | import dev.langchain4j.model.chat.StreamingChatLanguageModel; |
| | | import dev.langchain4j.model.chat.response.ChatResponse; |
| | | import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; |
| | |
| | | import java.io.InputStream; |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.util.Base64; |
| | | import java.util.Arrays; |
| | | import java.nio.file.Files; |
| | | import java.time.LocalDate; |
| | | import java.time.ZoneId; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.time.format.DateTimeParseException; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.Date; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | import java.util.Map; |
| | | import java.util.NoSuchElementException; |
| | | import java.util.UUID; |
| | | import java.nio.file.Files; |
| | | import java.util.*; |
| | | |
| | | @Service |
| | | public class PurchaseAiService { |
| | |
| | | private static final int MAX_FILE_COUNT = 10; |
| | | private static final int MAX_SINGLE_FILE_TEXT_LENGTH = 8000; |
| | | private static final int MAX_TOTAL_FILE_TEXT_LENGTH = 30000; |
| | | private static final DateTimeFormatter CURRENT_DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); |
| | | private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai"); |
| | | |
| | | private final PurchaseAgent purchaseAgent; |
| | | private final PurchaseIntentExecutor purchaseIntentExecutor; |
| | |
| | | return Flux.just(directResponse); |
| | | } |
| | | |
| | | return purchaseAgent.chat(memoryId, userMessage) |
| | | if (isPurchaseBusinessIntent(userMessage)) { |
| | | String noGuessResponse = buildNoGuessResponse(); |
| | | mongoChatMemoryStore.appendMessages( |
| | | memoryId, |
| | | List.of(UserMessage.from(userMessage), AiMessage.from(noGuessResponse)) |
| | | ); |
| | | aiChatSessionService.refreshSessionStats(memoryId, loginUser); |
| | | return Flux.just(noGuessResponse); |
| | | } |
| | | |
| | | return purchaseAgent.chat(memoryId, userMessage, currentDateForPrompt()) |
| | | .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser)) |
| | | .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser)); |
| | | } |
| | |
| | | .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser)); |
| | | } |
| | | |
| | | return Flux.defer(() -> purchaseAgent.chat(finalMemoryId, userPrompt)) |
| | | return Flux.defer(() -> purchaseAgent.chat(finalMemoryId, userPrompt, currentDateForPrompt())) |
| | | .onErrorResume(NoSuchElementException.class, ex -> { |
| | | mongoChatMemoryStore.deleteMessages(finalMemoryId); |
| | | return purchaseAgent.chat(finalMemoryId, userPrompt); |
| | | return purchaseAgent.chat(finalMemoryId, userPrompt, currentDateForPrompt()); |
| | | }) |
| | | .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser)) |
| | | .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser)); |
| | | } |
| | | |
| | | public AjaxResult confirmAnalyzeResult(PurchaseAiConfirmRequest request) { |
| | | public R confirmAnalyzeResult(PurchaseAiConfirmRequest request) { |
| | | if (request == null || !StringUtils.hasText(request.getBusinessType())) { |
| | | return AjaxResult.error("businessType不能为空"); |
| | | return R.fail("businessType不能为空"); |
| | | } |
| | | if (request.getPayload() == null || request.getPayload().isEmpty()) { |
| | | return AjaxResult.error("payload不能为空"); |
| | | return R.fail("payload不能为空"); |
| | | } |
| | | |
| | | try { |
| | |
| | | case "purchase_ledger" -> processPurchaseLedger(request.getPayload()); |
| | | case "payment_registration" -> processPaymentRegistration(request.getPayload()); |
| | | case "purchase_return_order" -> processPurchaseReturnOrder(request.getPayload()); |
| | | default -> AjaxResult.error("暂不支持该业务类型: " + businessType); |
| | | default -> R.fail("暂不支持该业务类型: " + businessType); |
| | | }; |
| | | } catch (Exception ex) { |
| | | return AjaxResult.error(toCustomerMessage(ex)); |
| | | return R.fail(toCustomerMessage(ex)); |
| | | } |
| | | } |
| | | |
| | |
| | | }; |
| | | } |
| | | |
| | | private String currentDateForPrompt() { |
| | | return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT); |
| | | } |
| | | |
| | | private boolean isPurchaseBusinessIntent(String message) { |
| | | if (!StringUtils.hasText(message)) { |
| | | return false; |
| | | } |
| | | String text = message.trim(); |
| | | boolean hasDomainWord = containsAny(text, |
| | | "采购", "采购台账", "采购单", "采购订单", "供应商", "物料", "入库", "到货", "待付款", |
| | | "付款", "退货", "退料", "发票", "合同"); |
| | | boolean hasIntentWord = containsAny(text, |
| | | "查询", "查看", "统计", "分析", "排行", "排名", "列出", "有哪些", "情况", "明细", "详情", "报表"); |
| | | return hasDomainWord && hasIntentWord; |
| | | } |
| | | |
| | | private boolean containsAny(String text, String... keywords) { |
| | | for (String keyword : keywords) { |
| | | if (text.contains(keyword)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private String buildNoGuessResponse() { |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("success", false); |
| | | result.put("type", "purchase_intent_not_recognized"); |
| | | result.put("description", "未识别到可执行的采购查询条件。为保证结果准确,当前不会推测或编造数据,请补充明确时间范围、供应商、采购合同号或物料后再查询。"); |
| | | result.put("summary", Map.of()); |
| | | result.put("data", Map.of( |
| | | "quickPrompts", List.of( |
| | | "本月采购金额排名前十的物料有哪些?", |
| | | "哪些采购订单还未入库?", |
| | | "最近7天供应商到货异常有哪些?", |
| | | "帮我统计待付款采购单!", |
| | | "列出本月采购退货情况" |
| | | ) |
| | | )); |
| | | result.put("charts", Map.of()); |
| | | return JSON.toJSONString(result); |
| | | } |
| | | |
| | | private String buildPurchaseFileAnalyzePrompt(String message, String fileContent) { |
| | | return """ |
| | | 你是采购业务文件分析助手。请严格根据用户上传的多个文件和用户要求提取采购业务数据。 |
| | |
| | | 输出要求: |
| | | 1. 只输出合法 JSON,不要 Markdown,不要额外解释。 |
| | | 2. JSON 顶层字段固定为: |
| | | - success: boolean |
| | | - ok: boolean |
| | | - businessType: purchase_ledger | payment_registration | purchase_return_order | unknown |
| | | - action: confirm_required |
| | | - description: 中文说明 |
| | |
| | | """.formatted(message, fileContent); |
| | | } |
| | | |
| | | private AjaxResult processPurchaseLedger(Map<String, Object> payload) throws Exception { |
| | | private R processPurchaseLedger(Map<String, Object> payload) throws Exception { |
| | | if (payload.containsKey("purchaseLedgers")) { |
| | | return processPurchaseLedgerBatch(payload); |
| | | } |
| | | |
| | | Map<String, Object> normalizedPayload = normalizePurchaseLedgerMap(payload); |
| | | PurchaseLedgerDto dto = objectMapper.convertValue(normalizedPayload, PurchaseLedgerDto.class); |
| | | AjaxResult ledgerResult = validatePurchaseLedger(dto, 0); |
| | | R ledgerResult = validatePurchaseLedger(dto, 0); |
| | | if (ledgerResult != null) { |
| | | return ledgerResult; |
| | | } |
| | | AjaxResult supplierResult = fillSupplierIdByName(dto); |
| | | R supplierResult = fillSupplierIdByName(dto); |
| | | if (supplierResult != null) { |
| | | return supplierResult; |
| | | } |
| | | AjaxResult productResult = validatePurchaseProducts(dto.getProductData(), 0); |
| | | R productResult = validatePurchaseProducts(dto.getProductData(), 0); |
| | | if (productResult != null) { |
| | | return productResult; |
| | | } |
| | | int result = purchaseLedgerService.addOrEditPurchase(dto); |
| | | return AjaxResult.success("采购台账已处理", result); |
| | | return R.ok( result,"采购台账已处理"); |
| | | } |
| | | |
| | | private AjaxResult processPurchaseLedgerBatch(Map<String, Object> payload) throws Exception { |
| | | private R processPurchaseLedgerBatch(Map<String, Object> payload) throws Exception { |
| | | List<Map<String, Object>> purchaseLedgers = toMapList(payload.get("purchaseLedgers")); |
| | | if (purchaseLedgers.isEmpty()) { |
| | | return AjaxResult.error("purchaseLedgers不能为空"); |
| | | return R.fail("purchaseLedgers不能为空"); |
| | | } |
| | | |
| | | List<Map<String, Object>> topLevelProductData = toMapList(payload.get("productData")); |
| | |
| | | for (int i = 0; i < purchaseLedgers.size(); i++) { |
| | | Map<String, Object> ledgerMap = normalizePurchaseLedgerMap(purchaseLedgers.get(i)); |
| | | PurchaseLedgerDto dto = objectMapper.convertValue(ledgerMap, PurchaseLedgerDto.class); |
| | | AjaxResult ledgerResult = validatePurchaseLedger(dto, i); |
| | | R ledgerResult = validatePurchaseLedger(dto, i); |
| | | if (ledgerResult != null) { |
| | | return ledgerResult; |
| | | } |
| | | AjaxResult supplierResult = fillSupplierIdByName(dto); |
| | | R supplierResult = fillSupplierIdByName(dto); |
| | | if (supplierResult != null) { |
| | | return supplierResult; |
| | | } |
| | |
| | | products = matchProductsForLedger(ledgerMap, dto, topLevelProductData, purchaseLedgers.size() == 1); |
| | | dto.setProductData(products); |
| | | } |
| | | AjaxResult productResult = validatePurchaseProducts(products, i); |
| | | R productResult = validatePurchaseProducts(products, i); |
| | | if (productResult != null) { |
| | | return productResult; |
| | | } |
| | |
| | | item.put("result", result); |
| | | results.add(item); |
| | | } |
| | | return AjaxResult.success("采购台账已批量处理", results); |
| | | return R.ok( results,"采购台账已批量处理"); |
| | | } |
| | | |
| | | private List<SalesLedgerProduct> matchProductsForLedger(Map<String, Object> ledgerMap, |
| | |
| | | } |
| | | } |
| | | |
| | | private AjaxResult validatePurchaseProducts(List<SalesLedgerProduct> products, int ledgerIndex) { |
| | | private R validatePurchaseProducts(List<SalesLedgerProduct> products, int ledgerIndex) { |
| | | if (products == null || products.isEmpty()) { |
| | | return null; |
| | | } |
| | |
| | | SalesLedgerProduct product = products.get(i); |
| | | String prefix = "第" + (ledgerIndex + 1) + "个采购台账的第" + (i + 1) + "条产品"; |
| | | if (!StringUtils.hasText(product.getProductCategory())) { |
| | | return AjaxResult.error(prefix + "缺少产品名称,请补充后再确认"); |
| | | return R.fail(prefix + "缺少产品名称,请补充后再确认"); |
| | | } |
| | | if (!StringUtils.hasText(product.getSpecificationModel())) { |
| | | return AjaxResult.error(prefix + "缺少规格型号,请补充后再确认"); |
| | | return R.fail(prefix + "缺少规格型号,请补充后再确认"); |
| | | } |
| | | if (!StringUtils.hasText(product.getUnit())) { |
| | | return AjaxResult.error(prefix + "缺少单位,请补充后再确认"); |
| | | return R.fail(prefix + "缺少单位,请补充后再确认"); |
| | | } |
| | | if (product.getQuantity() == null) { |
| | | return AjaxResult.error(prefix + "缺少数量"); |
| | | return R.fail(prefix + "缺少数量"); |
| | | } |
| | | if (product.getTaxInclusiveUnitPrice() == null) { |
| | | return AjaxResult.error(prefix + "缺少含税单价,请补充后再确认"); |
| | | return R.fail(prefix + "缺少含税单价,请补充后再确认"); |
| | | } |
| | | if (product.getTaxInclusiveTotalPrice() == null) { |
| | | return AjaxResult.error(prefix + "缺少含税总价,请补充后再确认"); |
| | | return R.fail(prefix + "缺少含税总价,请补充后再确认"); |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private AjaxResult validatePurchaseLedger(PurchaseLedgerDto dto, int ledgerIndex) { |
| | | private R validatePurchaseLedger(PurchaseLedgerDto dto, int ledgerIndex) { |
| | | String prefix = "第" + (ledgerIndex + 1) + "个采购台账"; |
| | | if (!StringUtils.hasText(dto.getPurchaseContractNumber())) { |
| | | return AjaxResult.error(prefix + "缺少采购合同号,请补充后再确认"); |
| | | return R.fail(prefix + "缺少采购合同号,请补充后再确认"); |
| | | } |
| | | if (dto.getSupplierId() == null && !StringUtils.hasText(dto.getSupplierName())) { |
| | | return AjaxResult.error(prefix + "缺少供应商名称,请补充后再确认"); |
| | | return R.fail(prefix + "缺少供应商名称,请补充后再确认"); |
| | | } |
| | | return null; |
| | | } |
| | |
| | | return "处理失败:" + message; |
| | | } |
| | | |
| | | private AjaxResult fillSupplierIdByName(PurchaseLedgerDto dto) { |
| | | private R fillSupplierIdByName(PurchaseLedgerDto dto) { |
| | | if (dto.getSupplierId() != null) { |
| | | return null; |
| | | } |
| | | if (!StringUtils.hasText(dto.getSupplierName())) { |
| | | return AjaxResult.error("供应商ID不能为空;未识别到供应商名称,无法自动匹配供应商ID"); |
| | | return R.fail("供应商ID不能为空;未识别到供应商名称,无法自动匹配供应商ID"); |
| | | } |
| | | |
| | | SupplierManage supplier = supplierManageMapper.selectOne(new LambdaQueryWrapper<SupplierManage>() |
| | | .eq(SupplierManage::getSupplierName, dto.getSupplierName().trim()) |
| | | .last("limit 1")); |
| | | if (supplier == null) { |
| | | return AjaxResult.error("未找到供应商:" + dto.getSupplierName() + ",请先维护供应商或手动选择供应商ID"); |
| | | return R.fail("未找到供应商:" + dto.getSupplierName() + ",请先维护供应商或手动选择供应商ID"); |
| | | } |
| | | dto.setSupplierId(supplier.getId()); |
| | | return null; |
| | | } |
| | | |
| | | private AjaxResult processPaymentRegistration(Map<String, Object> payload) { |
| | | private R processPaymentRegistration(Map<String, Object> payload) { |
| | | Object recordsValue = payload.get("records"); |
| | | List<PaymentRegistration> records; |
| | | if (recordsValue == null) { |
| | |
| | | }); |
| | | } |
| | | int result = paymentRegistrationService.insertPaymentRegistration(records); |
| | | return AjaxResult.success("付款登记已处理", result); |
| | | return R.ok( result,"付款登记已处理"); |
| | | } |
| | | |
| | | private AjaxResult processPurchaseReturnOrder(Map<String, Object> payload) { |
| | | private R processPurchaseReturnOrder(Map<String, Object> payload) { |
| | | PurchaseReturnOrderDto dto = objectMapper.convertValue(payload, PurchaseReturnOrderDto.class); |
| | | Boolean result = purchaseReturnOrdersService.add(dto); |
| | | return AjaxResult.success("采购退货单已处理", result); |
| | | return R.ok( result,"采购退货单已处理"); |
| | | } |
| | | } |