| | |
| | | import com.ruoyi.basic.service.StorageBlobService; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.framework.security.LoginUser; |
| | | import com.ruoyi.framework.web.domain.R; |
| | | import com.ruoyi.framework.web.domain.AjaxResult; |
| | | import com.ruoyi.purchase.dto.PurchaseLedgerDto; |
| | | import com.ruoyi.purchase.dto.PurchaseReturnOrderDto; |
| | | import com.ruoyi.purchase.pojo.PaymentRegistration; |
| | | import com.ruoyi.purchase.service.IPaymentRegistrationService; |
| | | import com.ruoyi.purchase.service.IPurchaseLedgerService; |
| | | import com.ruoyi.purchase.service.PurchaseReturnOrdersService; |
| | | import com.ruoyi.sales.pojo.SalesLedgerProduct; |
| | | import dev.langchain4j.data.image.Image; |
| | | import dev.langchain4j.data.message.*; |
| | | 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.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.nio.file.Files; |
| | | import java.util.Base64; |
| | | import java.time.LocalDate; |
| | | import java.time.ZoneId; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.time.format.DateTimeParseException; |
| | | import java.util.*; |
| | | 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; |
| | | |
| | | @Service |
| | | public class PurchaseAiService { |
| | |
| | | private final AiFileTextExtractor aiFileTextExtractor; |
| | | private final ObjectMapper objectMapper; |
| | | private final IPurchaseLedgerService purchaseLedgerService; |
| | | private final IPaymentRegistrationService paymentRegistrationService; |
| | | private final PurchaseReturnOrdersService purchaseReturnOrdersService; |
| | | private final StorageBlobService storageBlobService; |
| | | private final SupplierManageMapper supplierManageMapper; |
| | |
| | | AiFileTextExtractor aiFileTextExtractor, |
| | | ObjectMapper objectMapper, |
| | | IPurchaseLedgerService purchaseLedgerService, |
| | | IPaymentRegistrationService paymentRegistrationService, |
| | | PurchaseReturnOrdersService purchaseReturnOrdersService, |
| | | StorageBlobService storageBlobService, |
| | | SupplierManageMapper supplierManageMapper, |
| | |
| | | this.aiFileTextExtractor = aiFileTextExtractor; |
| | | this.objectMapper = objectMapper; |
| | | this.purchaseLedgerService = purchaseLedgerService; |
| | | this.paymentRegistrationService = paymentRegistrationService; |
| | | this.purchaseReturnOrdersService = purchaseReturnOrdersService; |
| | | this.storageBlobService = storageBlobService; |
| | | this.supplierManageMapper = supplierManageMapper; |
| | |
| | | .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser)); |
| | | } |
| | | |
| | | public R confirmAnalyzeResult(PurchaseAiConfirmRequest request) { |
| | | public AjaxResult confirmAnalyzeResult(PurchaseAiConfirmRequest request) { |
| | | if (request == null || !StringUtils.hasText(request.getBusinessType())) { |
| | | return R.fail("businessType不能为空"); |
| | | return AjaxResult.error("businessType不能为空"); |
| | | } |
| | | if (request.getPayload() == null || request.getPayload().isEmpty()) { |
| | | return R.fail("payload不能为空"); |
| | | return AjaxResult.error("payload不能为空"); |
| | | } |
| | | |
| | | try { |
| | | String businessType = request.getBusinessType().trim(); |
| | | return switch (businessType) { |
| | | case "purchase_ledger" -> processPurchaseLedger(request.getPayload()); |
| | | case "payment_registration" -> processPaymentRegistration(request.getPayload()); |
| | | case "purchase_return_order" -> processPurchaseReturnOrder(request.getPayload()); |
| | | default -> R.fail("暂不支持该业务类型: " + businessType); |
| | | default -> AjaxResult.error("暂不支持该业务类型: " + businessType); |
| | | }; |
| | | } catch (Exception ex) { |
| | | return R.fail(toCustomerMessage(ex)); |
| | | return AjaxResult.error(toCustomerMessage(ex)); |
| | | } |
| | | } |
| | | |
| | |
| | | 输出要求: |
| | | 1. 只输出合法 JSON,不要 Markdown,不要额外解释。 |
| | | 2. JSON 顶层字段固定为: |
| | | - ok: boolean |
| | | - businessType: purchase_ledger | payment_registration | purchase_return_order | unknown |
| | | - success: boolean |
| | | - businessType: purchase_ledger | purchase_return_order | unknown |
| | | - action: confirm_required |
| | | - description: 中文说明 |
| | | - confidence: 0到1的小数 |
| | |
| | | entryDateStart, entryDateEnd, id, purchaseContractNumber, supplierId, supplierName, isWhite, recorderId, recorderName, salesContractNo, salesContractNoId, projectName, entryDate, executionDate, remarks, attachmentMaterials, createdAt, updatedAt, salesLedgerId, hasChildren, Type, productData, tempFileIds, SalesLedgerFiles, phoneNumber, businessPersonId, productId, productModelId, invoiceNumber, invoiceAmount, ticketRegistrationId, contractAmount, receiptPaymentAmount, unReceiptPaymentAmount, type, paymentMethod, approvalStatus, templateName |
| | | - productData 每条产品只使用这些 SalesLedgerProduct 字段名: |
| | | productCategory, specificationModel, unit, quantity, taxRate, taxInclusiveUnitPrice, taxInclusiveTotalPrice, taxExclusiveTotalPrice, invoiceType, productId, productModelId, isChecked, type |
| | | 4. 如果可判断为付款登记,businessType 使用 payment_registration,payload.records 为付款登记数组,字段尽量包含 purchaseLedgerId、salesLedgerProductId、currentPaymentAmount、paymentMethod、paymentDate。 |
| | | 4. 如果可判断为付款登记,businessType 使用 payload.records 为付款登记数组,字段尽量包含 purchaseLedgerId、salesLedgerProductId、currentPaymentAmount、paymentMethod、paymentDate。 |
| | | 5. 如果可判断为采购退货,businessType 使用 purchase_return_order,payload 按 PurchaseReturnOrderDto 组织,明细放 purchaseReturnOrderProductsDtos。 |
| | | 6. 缺少业务处理必须字段时,不要编造 ID,把字段放入 missingFields,并仍返回可确认的草稿数据。 |
| | | 7. 所有中文内容直接保留,不要转义成 Unicode。 |
| | |
| | | """.formatted(message, fileContent); |
| | | } |
| | | |
| | | private R processPurchaseLedger(Map<String, Object> payload) throws Exception { |
| | | private AjaxResult 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); |
| | | R ledgerResult = validatePurchaseLedger(dto, 0); |
| | | AjaxResult ledgerResult = validatePurchaseLedger(dto, 0); |
| | | if (ledgerResult != null) { |
| | | return ledgerResult; |
| | | } |
| | | R supplierResult = fillSupplierIdByName(dto); |
| | | AjaxResult supplierResult = fillSupplierIdByName(dto); |
| | | if (supplierResult != null) { |
| | | return supplierResult; |
| | | } |
| | | R productResult = validatePurchaseProducts(dto.getProductData(), 0); |
| | | AjaxResult productResult = validatePurchaseProducts(dto.getProductData(), 0); |
| | | if (productResult != null) { |
| | | return productResult; |
| | | } |
| | | int result = purchaseLedgerService.addOrEditPurchase(dto); |
| | | return R.ok( result,"采购台账已处理"); |
| | | return AjaxResult.success("采购台账已处理", result); |
| | | } |
| | | |
| | | private R processPurchaseLedgerBatch(Map<String, Object> payload) throws Exception { |
| | | private AjaxResult processPurchaseLedgerBatch(Map<String, Object> payload) throws Exception { |
| | | List<Map<String, Object>> purchaseLedgers = toMapList(payload.get("purchaseLedgers")); |
| | | if (purchaseLedgers.isEmpty()) { |
| | | return R.fail("purchaseLedgers不能为空"); |
| | | return AjaxResult.error("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); |
| | | R ledgerResult = validatePurchaseLedger(dto, i); |
| | | AjaxResult ledgerResult = validatePurchaseLedger(dto, i); |
| | | if (ledgerResult != null) { |
| | | return ledgerResult; |
| | | } |
| | | R supplierResult = fillSupplierIdByName(dto); |
| | | AjaxResult supplierResult = fillSupplierIdByName(dto); |
| | | if (supplierResult != null) { |
| | | return supplierResult; |
| | | } |
| | |
| | | products = matchProductsForLedger(ledgerMap, dto, topLevelProductData, purchaseLedgers.size() == 1); |
| | | dto.setProductData(products); |
| | | } |
| | | R productResult = validatePurchaseProducts(products, i); |
| | | AjaxResult productResult = validatePurchaseProducts(products, i); |
| | | if (productResult != null) { |
| | | return productResult; |
| | | } |
| | |
| | | item.put("result", result); |
| | | results.add(item); |
| | | } |
| | | return R.ok( results,"采购台账已批量处理"); |
| | | return AjaxResult.success("采购台账已批量处理", results); |
| | | } |
| | | |
| | | private List<SalesLedgerProduct> matchProductsForLedger(Map<String, Object> ledgerMap, |
| | |
| | | } |
| | | } |
| | | |
| | | private R validatePurchaseProducts(List<SalesLedgerProduct> products, int ledgerIndex) { |
| | | private AjaxResult 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 R.fail(prefix + "缺少产品名称,请补充后再确认"); |
| | | return AjaxResult.error(prefix + "缺少产品名称,请补充后再确认"); |
| | | } |
| | | if (!StringUtils.hasText(product.getSpecificationModel())) { |
| | | return R.fail(prefix + "缺少规格型号,请补充后再确认"); |
| | | return AjaxResult.error(prefix + "缺少规格型号,请补充后再确认"); |
| | | } |
| | | if (!StringUtils.hasText(product.getUnit())) { |
| | | return R.fail(prefix + "缺少单位,请补充后再确认"); |
| | | return AjaxResult.error(prefix + "缺少单位,请补充后再确认"); |
| | | } |
| | | if (product.getQuantity() == null) { |
| | | return R.fail(prefix + "缺少数量"); |
| | | return AjaxResult.error(prefix + "缺少数量"); |
| | | } |
| | | if (product.getTaxInclusiveUnitPrice() == null) { |
| | | return R.fail(prefix + "缺少含税单价,请补充后再确认"); |
| | | return AjaxResult.error(prefix + "缺少含税单价,请补充后再确认"); |
| | | } |
| | | if (product.getTaxInclusiveTotalPrice() == null) { |
| | | return R.fail(prefix + "缺少含税总价,请补充后再确认"); |
| | | return AjaxResult.error(prefix + "缺少含税总价,请补充后再确认"); |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private R validatePurchaseLedger(PurchaseLedgerDto dto, int ledgerIndex) { |
| | | private AjaxResult validatePurchaseLedger(PurchaseLedgerDto dto, int ledgerIndex) { |
| | | String prefix = "第" + (ledgerIndex + 1) + "个采购台账"; |
| | | if (!StringUtils.hasText(dto.getPurchaseContractNumber())) { |
| | | return R.fail(prefix + "缺少采购合同号,请补充后再确认"); |
| | | return AjaxResult.error(prefix + "缺少采购合同号,请补充后再确认"); |
| | | } |
| | | if (dto.getSupplierId() == null && !StringUtils.hasText(dto.getSupplierName())) { |
| | | return R.fail(prefix + "缺少供应商名称,请补充后再确认"); |
| | | return AjaxResult.error(prefix + "缺少供应商名称,请补充后再确认"); |
| | | } |
| | | return null; |
| | | } |
| | |
| | | return "处理失败:" + message; |
| | | } |
| | | |
| | | private R fillSupplierIdByName(PurchaseLedgerDto dto) { |
| | | private AjaxResult fillSupplierIdByName(PurchaseLedgerDto dto) { |
| | | if (dto.getSupplierId() != null) { |
| | | return null; |
| | | } |
| | | if (!StringUtils.hasText(dto.getSupplierName())) { |
| | | return R.fail("供应商ID不能为空;未识别到供应商名称,无法自动匹配供应商ID"); |
| | | return AjaxResult.error("供应商ID不能为空;未识别到供应商名称,无法自动匹配供应商ID"); |
| | | } |
| | | |
| | | SupplierManage supplier = supplierManageMapper.selectOne(new LambdaQueryWrapper<SupplierManage>() |
| | | .eq(SupplierManage::getSupplierName, dto.getSupplierName().trim()) |
| | | .last("limit 1")); |
| | | if (supplier == null) { |
| | | return R.fail("未找到供应商:" + dto.getSupplierName() + ",请先维护供应商或手动选择供应商ID"); |
| | | return AjaxResult.error("未找到供应商:" + dto.getSupplierName() + ",请先维护供应商或手动选择供应商ID"); |
| | | } |
| | | dto.setSupplierId(supplier.getId()); |
| | | return null; |
| | | } |
| | | |
| | | private R processPaymentRegistration(Map<String, Object> payload) { |
| | | Object recordsValue = payload.get("records"); |
| | | List<PaymentRegistration> records; |
| | | if (recordsValue == null) { |
| | | records = Collections.singletonList(objectMapper.convertValue(payload, PaymentRegistration.class)); |
| | | } else { |
| | | records = objectMapper.convertValue(recordsValue, new TypeReference<List<PaymentRegistration>>() { |
| | | }); |
| | | } |
| | | int result = paymentRegistrationService.insertPaymentRegistration(records); |
| | | return R.ok( result,"付款登记已处理"); |
| | | } |
| | | |
| | | private R processPurchaseReturnOrder(Map<String, Object> payload) { |
| | | private AjaxResult processPurchaseReturnOrder(Map<String, Object> payload) { |
| | | PurchaseReturnOrderDto dto = objectMapper.convertValue(payload, PurchaseReturnOrderDto.class); |
| | | Boolean result = purchaseReturnOrdersService.add(dto); |
| | | return R.ok( result,"采购退货单已处理"); |
| | | return AjaxResult.success("采购退货单已处理", result); |
| | | } |
| | | } |