package com.ruoyi.ai.tools; import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.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.procurementrecord.mapper.InboundManagementMapper; import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper; import com.ruoyi.procurementrecord.pojo.InboundManagement; import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage; import com.ruoyi.sales.mapper.SalesLedgerProductMapper; import com.ruoyi.sales.pojo.SalesLedgerProduct; import dev.langchain4j.agent.tool.P; import dev.langchain4j.agent.tool.Tool; import dev.langchain4j.agent.tool.ToolMemoryId; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; 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.Comparator; import java.util.stream.Collectors; @Component public class PurchaseAgentTools { private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 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 AiSessionUserContext aiSessionUserContext; public PurchaseAgentTools(PurchaseLedgerMapper purchaseLedgerMapper, PaymentRegistrationMapper paymentRegistrationMapper, InvoicePurchaseMapper invoicePurchaseMapper, PurchaseReturnOrdersMapper purchaseReturnOrdersMapper, SalesLedgerProductMapper salesLedgerProductMapper, ProcurementRecordMapper procurementRecordMapper, InboundManagementMapper inboundManagementMapper, AiSessionUserContext aiSessionUserContext) { this.purchaseLedgerMapper = purchaseLedgerMapper; this.paymentRegistrationMapper = paymentRegistrationMapper; this.invoicePurchaseMapper = invoicePurchaseMapper; this.purchaseReturnOrdersMapper = purchaseReturnOrdersMapper; this.salesLedgerProductMapper = salesLedgerProductMapper; this.procurementRecordMapper = procurementRecordMapper; this.inboundManagementMapper = inboundManagementMapper; this.aiSessionUserContext = aiSessionUserContext; } @Tool(name = "查询采购台账列表", value = "按关键字和时间范围查询采购台账,支持返回最近N条") public String listPurchaseLedgers(@ToolMemoryId String memoryId, @P(value = "关键字,可匹配采购合同号/供应商/项目名", required = false) String keyword, @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate, @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate, @P(value = "返回条数,默认10,最大30", required = false) Integer limit) { LoginUser loginUser = currentLoginUser(memoryId); LocalDate start = parseLocalDate(startDate); LocalDate end = parseLocalDate(endDate); int finalLimit = normalizeLimit(limit); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); applyTenantFilter(wrapper, loginUser.getTenantId(), PurchaseLedger::getTenantId); if (StringUtils.hasText(keyword)) { wrapper.and(w -> w.like(PurchaseLedger::getPurchaseContractNumber, keyword) .or().like(PurchaseLedger::getSupplierName, keyword) .or().like(PurchaseLedger::getProjectName, keyword)); } if (start != null) { wrapper.ge(PurchaseLedger::getEntryDate, toDate(start)); } if (end != null) { wrapper.lt(PurchaseLedger::getEntryDate, toExclusiveEndDate(end)); } wrapper.orderByDesc(PurchaseLedger::getEntryDate, PurchaseLedger::getId).last("limit " + finalLimit); List rows = defaultList(purchaseLedgerMapper.selectList(wrapper)); List> items = rows.stream().map(this::toLedgerItem).collect(Collectors.toList()); return jsonResponse(true, "purchase_ledger_list", "已返回采购台账列表", Map.of("count", items.size(), "limit", finalLimit, "keyword", safe(keyword)), Map.of("items", items), Map.of()); } @Tool(name = "查询采购台账详情", value = "按采购台账ID查询详情") public String getPurchaseLedgerDetail(@ToolMemoryId String memoryId, @P("采购台账ID") Long ledgerId) { if (ledgerId == null) { return jsonResponse(false, "purchase_ledger_detail", "采购台账ID不能为空", Map.of(), Map.of(), Map.of()); } LoginUser loginUser = currentLoginUser(memoryId); PurchaseLedger ledger = purchaseLedgerMapper.selectById(ledgerId); if (ledger == null || !tenantMatched(ledger.getTenantId(), loginUser.getTenantId())) { return jsonResponse(false, "purchase_ledger_detail", "未找到该采购台账或无权限访问", Map.of("ledgerId", ledgerId), Map.of(), Map.of()); } return jsonResponse(true, "purchase_ledger_detail", "已返回采购台账详情", Map.of("ledgerId", ledgerId), Map.of("detail", toLedgerItem(ledger)), Map.of()); } @Tool(name = "统计采购数据", value = "统计时间范围内采购合同数、合同金额、付款金额、发票金额、退货金额") public String getPurchaseStats(@ToolMemoryId String memoryId, @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate, @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate, @P(value = "时间范围描述,例如今年、本月、近30天", required = false) String timeRange) { LoginUser loginUser = currentLoginUser(memoryId); DateRange range = resolveDateRange(startDate, endDate, timeRange); List ledgers = queryLedgers(loginUser, range); List payments = queryPayments(loginUser, range); List invoices = queryInvoices(loginUser, range); List returns = queryReturns(loginUser, range); BigDecimal contractAmount = ledgers.stream() .map(PurchaseLedger::getContractAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal paymentAmount = payments.stream() .map(PaymentRegistration::getCurrentPaymentAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal invoiceAmount = invoices.stream() .map(InvoicePurchase::getInvoiceAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal returnAmount = returns.stream() .map(PurchaseReturnOrders::getTotalAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); Map summary = new LinkedHashMap<>(); summary.put("timeRange", range.label()); summary.put("startDate", range.start().toString()); summary.put("endDate", range.end().toString()); summary.put("ledgerCount", ledgers.size()); summary.put("paymentCount", payments.size()); summary.put("invoiceCount", invoices.size()); summary.put("returnCount", returns.size()); summary.put("contractAmount", contractAmount); summary.put("paymentAmount", paymentAmount); summary.put("invoiceAmount", invoiceAmount); summary.put("returnAmount", returnAmount); 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 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 products = defaultList(salesLedgerProductMapper.selectList(new LambdaQueryWrapper() .eq(SalesLedgerProduct::getType, 2) .in(SalesLedgerProduct::getSalesLedgerId, ledgerIds))); Map 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> 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 ledgers = queryLedgers(loginUser, range).stream() .filter(ledger -> matchLedgerKeyword(ledger, keyword)) .collect(Collectors.toList()); Map 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 products = defaultList(salesLedgerProductMapper.selectList(new LambdaQueryWrapper() .eq(SalesLedgerProduct::getType, 2) .in(SalesLedgerProduct::getSalesLedgerId, ledgerMap.keySet()))); List> 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 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> 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> items = queryLedgers(loginUser, range).stream() .filter(ledger -> matchLedgerKeyword(ledger, keyword)) .map(ledger -> toPendingPaymentItem(loginUser, ledger)) .filter(Objects::nonNull) .sorted(Comparator.comparing(item -> (BigDecimal) item.get("pendingAmount"), Comparator.reverseOrder())) .limit(normalizeLimit(limit)) .collect(Collectors.toList()); return jsonResponse(true, "purchase_pending_payment_list", "已返回待付款采购单。", rangeSummary(range, items.size()), 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 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 returns = defaultList(purchaseReturnOrdersMapper.selectList(wrapper)); BigDecimal totalAmount = returns.stream() .map(PurchaseReturnOrders::getTotalAmount) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); Map 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 queryLedgers(LoginUser loginUser, DateRange range) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); applyTenantFilter(wrapper, loginUser.getTenantId(), PurchaseLedger::getTenantId); wrapper.ge(PurchaseLedger::getEntryDate, toDate(range.start())) .lt(PurchaseLedger::getEntryDate, toExclusiveEndDate(range.end())); return defaultList(purchaseLedgerMapper.selectList(wrapper)); } private Map rangeSummary(DateRange range, int count) { Map 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 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 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 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 records = defaultList(procurementRecordMapper.selectList(new LambdaQueryWrapper() .eq(ProcurementRecordStorage::getType, 1) .eq(ProcurementRecordStorage::getSalesLedgerProductId, salesLedgerProductId))); return records.stream() .map(ProcurementRecordStorage::getInboundNum) .filter(Objects::nonNull) .reduce(BigDecimal.ZERO, BigDecimal::add); } private Map toArrivalItem(InboundManagement item) { Map 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 toPendingPaymentItem(LoginUser loginUser, PurchaseLedger ledger) { BigDecimal contractAmount = defaultDecimal(ledger.getContractAmount()); BigDecimal paidAmount = sumPaymentAmount(loginUser, ledger.getId()); BigDecimal pendingAmount = contractAmount.subtract(paidAmount); if (pendingAmount.compareTo(BigDecimal.ZERO) <= 0) { return null; } Map item = toLedgerItem(ledger); item.put("paidAmount", paidAmount); item.put("pendingAmount", pendingAmount); return item; } private BigDecimal sumPaymentAmount(LoginUser loginUser, Long purchaseLedgerId) { LambdaQueryWrapper 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); } private Map toReturnItem(PurchaseReturnOrders item) { Map 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 List queryPayments(LoginUser loginUser, DateRange range) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); applyTenantFilter(wrapper, loginUser.getTenantId(), PaymentRegistration::getTenantId); wrapper.ge(PaymentRegistration::getPaymentDate, toDate(range.start())) .lt(PaymentRegistration::getPaymentDate, toExclusiveEndDate(range.end())); return defaultList(paymentRegistrationMapper.selectList(wrapper)); } private List queryInvoices(LoginUser loginUser, DateRange range) { LambdaQueryWrapper 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 List queryReturns(LoginUser loginUser, DateRange range) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), PurchaseReturnOrders::getDeptId); wrapper.ge(PurchaseReturnOrders::getPreparedAt, range.start()) .le(PurchaseReturnOrders::getPreparedAt, range.end()); return defaultList(purchaseReturnOrdersMapper.selectList(wrapper)); } private Map toLedgerItem(PurchaseLedger item) { Map map = new LinkedHashMap<>(); map.put("id", item.getId()); map.put("purchaseContractNumber", safe(item.getPurchaseContractNumber())); map.put("supplierName", safe(item.getSupplierName())); map.put("projectName", safe(item.getProjectName())); map.put("entryDate", formatDate(item.getEntryDate())); map.put("contractAmount", item.getContractAmount()); map.put("approvalStatus", item.getApprovalStatus()); map.put("paymentMethod", safe(item.getPaymentMethod())); return map; } private DateRange resolveDateRange(String startDate, String endDate, String timeRange) { LocalDate today = LocalDate.now(); LocalDate start = parseLocalDate(startDate); LocalDate end = parseLocalDate(endDate); if (start != null || end != null) { LocalDate s = start != null ? start : end; LocalDate e = end != null ? end : start; if (s.isAfter(e)) { LocalDate temp = s; s = e; e = temp; } return new DateRange(s, e, s + "至" + e); } if (!StringUtils.hasText(timeRange)) { return new DateRange(today.minusDays(29), today, "近30天"); } String text = timeRange.trim(); if (text.contains("今年") || text.contains("本年")) { return new DateRange(today.withDayOfYear(1), today, "今年"); } if (text.contains("本月")) { return new DateRange(today.withDayOfMonth(1), today, "本月"); } if (text.contains("上月")) { LocalDate first = today.minusMonths(1).withDayOfMonth(1); LocalDate last = first.withDayOfMonth(first.lengthOfMonth()); return new DateRange(first, last, "上月"); } if (text.contains("近半年") || text.contains("最近半年")) { return new DateRange(today.minusMonths(6).plusDays(1), today, "近半年"); } 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天"); } private LocalDate parseLocalDate(String text) { if (!StringUtils.hasText(text)) { return null; } return LocalDate.parse(text.trim(), DATE_FMT); } 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) { if (date == null) { return ""; } return DATE_FMT.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()); } private boolean tenantMatched(Long dataTenantId, Long userTenantId) { if (userTenantId == null) { return true; } return Objects.equals(dataTenantId, userTenantId); } private void applyTenantFilter(LambdaQueryWrapper wrapper, Long tenantId, com.baomidou.mybatisplus.core.toolkit.support.SFunction field) { if (tenantId != null) { wrapper.eq(field, tenantId); } } private void applyDeptFilter(LambdaQueryWrapper wrapper, Long deptId, com.baomidou.mybatisplus.core.toolkit.support.SFunction field) { if (deptId != null) { wrapper.eq(field, deptId); } } private LoginUser currentLoginUser(String memoryId) { LoginUser loginUser = aiSessionUserContext.get(memoryId); if (loginUser != null) { return loginUser; } return SecurityUtils.getLoginUser(); } private int normalizeLimit(Integer limit) { if (limit == null || limit <= 0) { return DEFAULT_LIMIT; } return Math.min(limit, MAX_LIMIT); } private String safe(Object value) { return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' '); } private List defaultList(List list) { return list == null ? List.of() : list; } private String jsonResponse(boolean success, String type, String description, Map summary, Map data, Map charts) { Map result = new LinkedHashMap<>(); result.put("success", success); result.put("type", type); result.put("description", description); result.put("summary", summary == null ? Map.of() : summary); result.put("data", data == null ? Map.of() : data); result.put("charts", charts == null ? Map.of() : charts); return JSON.toJSONString(result); } 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 toMap() { Map 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; } } }