package com.ruoyi.ai.tools;
|
|
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.ai.context.AiSessionUserContext;
|
import com.ruoyi.basic.dto.CustomerDto;
|
import com.ruoyi.basic.mapper.CustomerMapper;
|
import com.ruoyi.basic.vo.CustomerVo;
|
import com.ruoyi.common.utils.SecurityUtils;
|
import com.ruoyi.framework.security.LoginUser;
|
import com.ruoyi.sales.dto.InvoiceLedgerDto;
|
import com.ruoyi.sales.mapper.InvoiceLedgerMapper;
|
import com.ruoyi.sales.mapper.ReceiptPaymentMapper;
|
import com.ruoyi.sales.mapper.SalesLedgerMapper;
|
import com.ruoyi.sales.mapper.SalesQuotationMapper;
|
import com.ruoyi.sales.mapper.ShippingInfoMapper;
|
import com.ruoyi.sales.pojo.ReceiptPayment;
|
import com.ruoyi.sales.pojo.SalesLedger;
|
import com.ruoyi.sales.pojo.SalesQuotation;
|
import com.ruoyi.sales.pojo.ShippingInfo;
|
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.math.RoundingMode;
|
import java.time.LocalDate;
|
import java.time.LocalDateTime;
|
import java.time.YearMonth;
|
import java.time.ZoneId;
|
import java.time.format.DateTimeFormatter;
|
import java.util.ArrayList;
|
import java.util.Comparator;
|
import java.util.Date;
|
import java.util.HashMap;
|
import java.util.LinkedHashMap;
|
import java.util.LinkedList;
|
import java.util.List;
|
import java.util.Locale;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.regex.Matcher;
|
import java.util.regex.Pattern;
|
import java.util.stream.Collectors;
|
|
@Component
|
public class SalesAgentTools {
|
|
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 static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
|
private static final Pattern RELATIVE_PATTERN = Pattern.compile("(近|最近)?\\s*(\\d+)\\s*(天|周|个月|月|年)");
|
private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
|
|
private final CustomerMapper customerMapper;
|
private final SalesLedgerMapper salesLedgerMapper;
|
private final SalesQuotationMapper salesQuotationMapper;
|
private final ShippingInfoMapper shippingInfoMapper;
|
private final ReceiptPaymentMapper receiptPaymentMapper;
|
private final InvoiceLedgerMapper invoiceLedgerMapper;
|
private final SalesReceiptReturnMapper salesReceiptReturnMapper;
|
private final AiSessionUserContext aiSessionUserContext;
|
|
public SalesAgentTools(CustomerMapper customerMapper,
|
SalesLedgerMapper salesLedgerMapper,
|
SalesQuotationMapper salesQuotationMapper,
|
ShippingInfoMapper shippingInfoMapper,
|
ReceiptPaymentMapper receiptPaymentMapper,
|
InvoiceLedgerMapper invoiceLedgerMapper,
|
SalesReceiptReturnMapper salesReceiptReturnMapper,
|
AiSessionUserContext aiSessionUserContext) {
|
this.customerMapper = customerMapper;
|
this.salesLedgerMapper = salesLedgerMapper;
|
this.salesQuotationMapper = salesQuotationMapper;
|
this.shippingInfoMapper = shippingInfoMapper;
|
this.receiptPaymentMapper = receiptPaymentMapper;
|
this.invoiceLedgerMapper = invoiceLedgerMapper;
|
this.salesReceiptReturnMapper = salesReceiptReturnMapper;
|
this.aiSessionUserContext = aiSessionUserContext;
|
}
|
|
@Tool(name = "查询客户档案", value = "按私海/公海类型和关键词查询客户档案列表")
|
public String listCustomerProfiles(@ToolMemoryId String memoryId,
|
@P(value = "客户池类型,可选 private/public", required = false) String seaType,
|
@P(value = "关键词,可匹配客户名称/联系人/电话", required = false) String keyword,
|
@P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
|
LoginUser loginUser = currentLoginUser(memoryId);
|
CustomerDto customerDto = new CustomerDto();
|
customerDto.setType(normalizeSeaType(seaType));
|
customerDto.setUsageStatus(1L);
|
|
List<CustomerVo> rows = defaultList(customerMapper.list(customerDto, loginUser.getUserId()));
|
List<CustomerVo> filtered = rows.stream()
|
.filter(item -> matchCustomerKeyword(item, keyword))
|
.sorted(Comparator.comparing(CustomerVo::getId, Comparator.nullsLast(Comparator.reverseOrder())))
|
.limit(normalizeLimit(limit))
|
.collect(Collectors.toList());
|
|
List<Map<String, Object>> items = filtered.stream().map(item -> {
|
Map<String, Object> map = new LinkedHashMap<>();
|
map.put("id", item.getId());
|
map.put("customerName", safe(item.getCustomerName()));
|
map.put("customerType", safe(item.getCustomerType()));
|
map.put("contactPerson", safe(item.getContactPerson()));
|
map.put("contactPhone", safe(item.getContactPhone()));
|
map.put("companyPhone", safe(item.getCompanyPhone()));
|
map.put("maintainer", safe(item.getMaintainer()));
|
map.put("maintenanceTime", formatDate(item.getMaintenanceTime()));
|
map.put("usageUserName", safe(item.getUsageUserName()));
|
map.put("seaType", customerSeaTypeName(item.getType()));
|
map.put("isAssigned", item.getIsAssigned());
|
return map;
|
}).collect(Collectors.toList());
|
|
Map<String, Object> summary = new LinkedHashMap<>();
|
summary.put("count", items.size());
|
summary.put("seaType", seaType == null ? "all" : seaType);
|
summary.put("keyword", safe(keyword));
|
summary.put("userId", loginUser.getUserId());
|
|
return jsonResponse(true, "sales_customer_profile_list", "已返回客户档案列表", summary, Map.of("items", items), Map.of());
|
}
|
|
@Tool(name = "查询销售报价", value = "按关键词和时间范围查询销售报价单")
|
public String listSalesQuotations(@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);
|
DateRange range = resolveDateRange(startDate, endDate, null);
|
LambdaQueryWrapper<SalesQuotation> wrapper = new LambdaQueryWrapper<>();
|
applyTenantFilter(wrapper, loginUser.getTenantId(), SalesQuotation::getTenantId);
|
applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesQuotation::getDeptId);
|
if (StringUtils.hasText(keyword)) {
|
wrapper.and(w -> w.like(SalesQuotation::getQuotationNo, keyword)
|
.or().like(SalesQuotation::getCustomer, keyword)
|
.or().like(SalesQuotation::getSalesperson, keyword)
|
.or().like(SalesQuotation::getStatus, keyword));
|
}
|
wrapper.ge(SalesQuotation::getQuotationDate, range.start())
|
.le(SalesQuotation::getQuotationDate, range.end())
|
.orderByDesc(SalesQuotation::getQuotationDate, SalesQuotation::getId)
|
.last("limit " + normalizeLimit(limit));
|
|
List<SalesQuotation> rows = defaultList(salesQuotationMapper.selectList(wrapper));
|
BigDecimal quotationAmountTotal = rows.stream()
|
.map(SalesQuotation::getTotalAmount)
|
.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("quotationNo", safe(item.getQuotationNo()));
|
map.put("customer", safe(item.getCustomer()));
|
map.put("salesperson", safe(item.getSalesperson()));
|
map.put("quotationDate", formatDate(item.getQuotationDate()));
|
map.put("validDate", formatDate(item.getValidDate()));
|
map.put("status", safe(item.getStatus()));
|
map.put("paymentMethod", safe(item.getPaymentMethod()));
|
map.put("deliveryPeriod", safe(item.getDeliveryPeriod()));
|
map.put("totalAmount", item.getTotalAmount());
|
return map;
|
}).collect(Collectors.toList());
|
|
Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
|
summary.put("quotationAmountTotal", quotationAmountTotal);
|
return jsonResponse(true, "sales_quotation_list", "已返回销售报价列表", summary, Map.of("items", items), Map.of());
|
}
|
|
@Tool(name = "查询销售台账", value = "按关键词和时间范围查询销售台账,并返回开票回款与发货状态")
|
public String listSalesLedgers(@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);
|
DateRange range = resolveDateRange(startDate, endDate, null);
|
LambdaQueryWrapper<SalesLedger> wrapper = new LambdaQueryWrapper<>();
|
applyTenantFilter(wrapper, loginUser.getTenantId(), SalesLedger::getTenantId);
|
applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesLedger::getDeptId);
|
if (StringUtils.hasText(keyword)) {
|
wrapper.and(w -> w.like(SalesLedger::getSalesContractNo, keyword)
|
.or().like(SalesLedger::getCustomerContractNo, keyword)
|
.or().like(SalesLedger::getCustomerName, keyword)
|
.or().like(SalesLedger::getProjectName, keyword)
|
.or().like(SalesLedger::getSalesman, keyword));
|
}
|
wrapper.ge(SalesLedger::getEntryDate, toDate(range.start()))
|
.lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end()))
|
.orderByDesc(SalesLedger::getEntryDate, SalesLedger::getId)
|
.last("limit " + normalizeLimit(limit));
|
List<SalesLedger> rows = defaultList(salesLedgerMapper.selectList(wrapper));
|
if (rows.isEmpty()) {
|
return jsonResponse(true, "sales_ledger_list", "未查询到符合条件的销售台账", rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
|
}
|
|
List<Long> ledgerIds = rows.stream().map(SalesLedger::getId).filter(Objects::nonNull).collect(Collectors.toList());
|
Map<Long, BigDecimal> invoiceAmountByLedgerId = sumInvoiceAmounts(ledgerIds);
|
Map<Long, BigDecimal> receiptAmountByLedgerId = sumReceiptAmounts(loginUser, ledgerIds);
|
Map<Long, List<ShippingInfo>> shippingByLedgerId = queryShippingsByLedgerIds(loginUser, ledgerIds).stream()
|
.collect(Collectors.groupingBy(ShippingInfo::getSalesLedgerId));
|
|
BigDecimal contractAmountTotal = BigDecimal.ZERO;
|
BigDecimal invoicedAmountTotal = BigDecimal.ZERO;
|
BigDecimal receivedAmountTotal = BigDecimal.ZERO;
|
BigDecimal pendingAmountTotal = BigDecimal.ZERO;
|
|
List<Map<String, Object>> items = new ArrayList<>();
|
for (SalesLedger ledger : rows) {
|
BigDecimal contractAmount = defaultDecimal(ledger.getContractAmount());
|
BigDecimal invoicedAmount = invoiceAmountByLedgerId.getOrDefault(ledger.getId(), BigDecimal.ZERO);
|
BigDecimal receivedAmount = receiptAmountByLedgerId.getOrDefault(ledger.getId(), BigDecimal.ZERO);
|
BigDecimal unbilledAmount = maxZero(contractAmount.subtract(invoicedAmount));
|
BigDecimal pendingAmount = maxZero(invoicedAmount.subtract(receivedAmount));
|
|
contractAmountTotal = contractAmountTotal.add(contractAmount);
|
invoicedAmountTotal = invoicedAmountTotal.add(invoicedAmount);
|
receivedAmountTotal = receivedAmountTotal.add(receivedAmount);
|
pendingAmountTotal = pendingAmountTotal.add(pendingAmount);
|
|
Map<String, Object> item = new LinkedHashMap<>();
|
item.put("id", ledger.getId());
|
item.put("salesContractNo", safe(ledger.getSalesContractNo()));
|
item.put("customerContractNo", safe(ledger.getCustomerContractNo()));
|
item.put("customerName", safe(ledger.getCustomerName()));
|
item.put("projectName", safe(ledger.getProjectName()));
|
item.put("salesman", safe(ledger.getSalesman()));
|
item.put("entryDate", formatDate(ledger.getEntryDate()));
|
item.put("executionDate", formatDate(ledger.getExecutionDate()));
|
item.put("deliveryDate", formatDate(ledger.getDeliveryDate()));
|
item.put("contractAmount", contractAmount);
|
item.put("invoicedAmount", invoicedAmount);
|
item.put("receivedAmount", receivedAmount);
|
item.put("unbilledAmount", unbilledAmount);
|
item.put("pendingAmount", pendingAmount);
|
item.put("shippingStatus", calcLedgerShippingStatus(shippingByLedgerId.get(ledger.getId())));
|
items.add(item);
|
}
|
|
Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
|
summary.put("contractAmountTotal", contractAmountTotal);
|
summary.put("invoicedAmountTotal", invoicedAmountTotal);
|
summary.put("receivedAmountTotal", receivedAmountTotal);
|
summary.put("pendingAmountTotal", pendingAmountTotal);
|
return jsonResponse(true, "sales_ledger_list", "已返回销售台账列表", summary, Map.of("items", items), Map.of());
|
}
|
|
@Tool(name = "查询销售退货", value = "按时间范围和关键词查询销售退货记录")
|
public String listSalesReturns(@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<SalesReceiptReturn> wrapper = new LambdaQueryWrapper<>();
|
applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesReceiptReturn::getDeptId);
|
if (StringUtils.hasText(keyword)) {
|
wrapper.and(w -> w.like(SalesReceiptReturn::getRefundId, keyword)
|
.or().like(SalesReceiptReturn::getTransactionNo, keyword)
|
.or().like(SalesReceiptReturn::getPaymentAccountName, keyword));
|
}
|
wrapper.ge(SalesReceiptReturn::getCreateTime, range.start().atStartOfDay())
|
.le(SalesReceiptReturn::getCreateTime, range.end().atTime(23, 59, 59))
|
.orderByDesc(SalesReceiptReturn::getCreateTime, SalesReceiptReturn::getId)
|
.last("limit " + normalizeLimit(limit));
|
List<SalesReceiptReturn> rows = defaultList(salesReceiptReturnMapper.selectList(wrapper));
|
|
BigDecimal returnAmount = rows.stream()
|
.map(SalesReceiptReturn::getActualAmount)
|
.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()));
|
return map;
|
}).collect(Collectors.toList());
|
|
Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
|
summary.put("returnAmount", returnAmount);
|
return jsonResponse(true, "sales_return_list", "已返回销售退货记录", summary, Map.of("items", items), Map.of());
|
}
|
|
@Tool(name = "查询客户往来", value = "按时间范围和关键词查询客户回款往来明细")
|
public String listCustomerInteractions(@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);
|
DateRange range = resolveDateRange(startDate, endDate, null);
|
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());
|
}
|
|
@Tool(name = "查询发货台账", value = "按关键词和时间范围查询发货台账")
|
public String listShippingLedgers(@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);
|
DateRange range = resolveDateRange(startDate, endDate, null);
|
LambdaQueryWrapper<ShippingInfo> wrapper = new LambdaQueryWrapper<>();
|
applyTenantFilter(wrapper, loginUser.getTenantId(), ShippingInfo::getTenantId);
|
applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ShippingInfo::getDeptId);
|
if (StringUtils.hasText(keyword)) {
|
wrapper.and(w -> w.like(ShippingInfo::getShippingNo, keyword)
|
.or().like(ShippingInfo::getExpressNumber, keyword)
|
.or().like(ShippingInfo::getExpressCompany, keyword)
|
.or().like(ShippingInfo::getShippingCarNumber, keyword)
|
.or().like(ShippingInfo::getStatus, keyword));
|
}
|
wrapper.ge(ShippingInfo::getShippingDate, toDate(range.start()))
|
.lt(ShippingInfo::getShippingDate, toExclusiveEndDate(range.end()))
|
.orderByDesc(ShippingInfo::getShippingDate, ShippingInfo::getId)
|
.last("limit " + normalizeLimit(limit));
|
List<ShippingInfo> rows = defaultList(shippingInfoMapper.selectList(wrapper));
|
if (rows.isEmpty()) {
|
return jsonResponse(true, "sales_shipping_list", "未查询到发货台账记录", rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
|
}
|
|
List<Long> ledgerIds = rows.stream().map(ShippingInfo::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));
|
|
long shippedCount = rows.stream().filter(item -> isShippedStatus(item.getStatus())).count();
|
List<Map<String, Object>> items = rows.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("shippingNo", safe(item.getShippingNo()));
|
map.put("status", safe(item.getStatus()));
|
map.put("shippingDate", formatDate(item.getShippingDate()));
|
map.put("type", safe(item.getType()));
|
map.put("shippingCarNumber", safe(item.getShippingCarNumber()));
|
map.put("expressCompany", safe(item.getExpressCompany()));
|
map.put("expressNumber", safe(item.getExpressNumber()));
|
return map;
|
}).collect(Collectors.toList());
|
|
Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
|
summary.put("shippingCount", rows.size());
|
summary.put("shippedCount", shippedCount);
|
summary.put("pendingCount", Math.max(rows.size() - shippedCount, 0));
|
return jsonResponse(true, "sales_shipping_list", "已返回发货台账记录", summary, Map.of("items", items), Map.of());
|
}
|
|
@Tool(name = "查询销售指标统计", value = "按时间范围统计销售合同、报价、发货、回款等关键指标")
|
public String getSalesDashboard(@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<SalesLedger> ledgers = querySalesLedgers(loginUser, range);
|
List<SalesQuotation> quotations = querySalesQuotations(loginUser, range);
|
List<ShippingInfo> shippings = queryShippings(loginUser, range);
|
List<ReceiptPayment> receipts = queryReceipts(loginUser, range);
|
|
BigDecimal contractAmountTotal = ledgers.stream()
|
.map(SalesLedger::getContractAmount)
|
.filter(Objects::nonNull)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
BigDecimal quotationAmountTotal = quotations.stream()
|
.map(SalesQuotation::getTotalAmount)
|
.filter(Objects::nonNull)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
BigDecimal receivedAmountTotal = receipts.stream()
|
.map(ReceiptPayment::getReceiptPaymentAmount)
|
.filter(Objects::nonNull)
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
BigDecimal pendingAmountTotal = maxZero(contractAmountTotal.subtract(receivedAmountTotal));
|
|
long shippingCount = shippings.size();
|
long shippedCount = shippings.stream().filter(item -> isShippedStatus(item.getStatus())).count();
|
String shipRate = toRate(shippedCount, shippingCount);
|
|
List<Map<String, Object>> topCustomers = buildTopCustomers(ledgers);
|
TrendData trendData = buildContractTrendData(ledgers, range);
|
|
Map<String, Object> summary = new LinkedHashMap<>();
|
summary.put("timeRange", range.label());
|
summary.put("startDate", range.start().toString());
|
summary.put("endDate", range.end().toString());
|
summary.put("orderCount", ledgers.size());
|
summary.put("quotationCount", quotations.size());
|
summary.put("shippingCount", shippingCount);
|
summary.put("shippedCount", shippedCount);
|
summary.put("shipRate", shipRate);
|
summary.put("contractAmountTotal", contractAmountTotal);
|
summary.put("quotationAmountTotal", quotationAmountTotal);
|
summary.put("receivedAmountTotal", receivedAmountTotal);
|
summary.put("pendingAmountTotal", pendingAmountTotal);
|
|
Map<String, Object> charts = new LinkedHashMap<>();
|
charts.put("amountBarOption", buildAmountBarOption(contractAmountTotal, quotationAmountTotal, receivedAmountTotal, pendingAmountTotal));
|
charts.put("shippingPieOption", buildShippingPieOption(shippedCount, Math.max(shippingCount - shippedCount, 0)));
|
charts.put("customerTopBarOption", buildCustomerTopBarOption(topCustomers));
|
charts.put("contractTrendLineOption", buildContractTrendLineOption(trendData.labels(), trendData.values()));
|
|
Map<String, Object> data = new LinkedHashMap<>();
|
data.put("topCustomers", topCustomers);
|
data.put("contractTrend", trendData.toItemList());
|
|
return jsonResponse(true, "sales_dashboard", "已返回销售指标统计", summary, data, charts);
|
}
|
|
@Tool(name = "客户流失风险分析", value = "按客户维度评估流失风险,输出风险分级、原因和建议优先级")
|
public String analyzeCustomerChurnRisk(@ToolMemoryId String memoryId,
|
@P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
|
@P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
|
@P(value = "时间范围描述,如近90天、本年", required = false) String timeRange,
|
@P(value = "关键词,可匹配客户名称", required = false) String keyword,
|
@P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
|
LoginUser loginUser = currentLoginUser(memoryId);
|
DateRange range = resolveDateRange(startDate, endDate, StringUtils.hasText(timeRange) ? timeRange : "近180天");
|
List<CustomerRiskMetric> metrics = buildCustomerRiskMetrics(loginUser, range, keyword);
|
if (metrics.isEmpty()) {
|
return jsonResponse(true, "sales_customer_churn_risk", "当前范围内未查询到可分析的客户数据",
|
rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
|
}
|
|
List<CustomerRiskMetric> sorted = metrics.stream()
|
.sorted(Comparator.comparing(CustomerRiskMetric::getRiskScore).reversed()
|
.thenComparing(CustomerRiskMetric::getPendingAmount, Comparator.reverseOrder()))
|
.limit(normalizeLimit(limit))
|
.collect(Collectors.toList());
|
|
long highCount = sorted.stream().filter(item -> "high".equals(item.getRiskLevel())).count();
|
long mediumCount = sorted.stream().filter(item -> "medium".equals(item.getRiskLevel())).count();
|
long lowCount = sorted.stream().filter(item -> "low".equals(item.getRiskLevel())).count();
|
|
List<Map<String, Object>> items = sorted.stream().map(this::toRiskItem).collect(Collectors.toList());
|
Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
|
summary.put("highRiskCount", highCount);
|
summary.put("mediumRiskCount", mediumCount);
|
summary.put("lowRiskCount", lowCount);
|
|
Map<String, Object> charts = new LinkedHashMap<>();
|
charts.put("riskLevelPieOption", buildRiskLevelPieOption(highCount, mediumCount, lowCount));
|
charts.put("riskScoreBarOption", buildRiskScoreBarOption(sorted));
|
|
return jsonResponse(true, "sales_customer_churn_risk", "已完成客户流失风险分析", summary, Map.of("items", items), charts);
|
}
|
|
@Tool(name = "回款与报价策略建议", value = "基于客户风险、回款和报价情况生成可执行的跟进策略")
|
public String suggestCollectionAndQuotationStrategy(@ToolMemoryId String memoryId,
|
@P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
|
@P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
|
@P(value = "时间范围描述,如近90天、本月", required = false) String timeRange,
|
@P(value = "关键词,可匹配客户名称", required = false) String keyword,
|
@P(value = "返回条数,默认10,最大30", required = false) Integer limit,
|
@P(value = "是否优先高风险客户,true 表示高风险优先", required = false) Boolean prioritizeHighRisk) {
|
LoginUser loginUser = currentLoginUser(memoryId);
|
DateRange range = resolveDateRange(startDate, endDate, StringUtils.hasText(timeRange) ? timeRange : "近90天");
|
List<CustomerRiskMetric> metrics = buildCustomerRiskMetrics(loginUser, range, keyword);
|
if (metrics.isEmpty()) {
|
return jsonResponse(true, "sales_collection_quote_strategy", "当前范围内未查询到可生成策略的客户数据",
|
rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of());
|
}
|
|
boolean highRiskFirst = Boolean.TRUE.equals(prioritizeHighRisk);
|
Comparator<CustomerRiskMetric> sortComparator;
|
if (highRiskFirst) {
|
sortComparator = Comparator
|
.comparingInt((CustomerRiskMetric metric) -> riskLevelRank(metric.getRiskLevel())).reversed()
|
.thenComparing(CustomerRiskMetric::getRiskScore, Comparator.reverseOrder())
|
.thenComparing(CustomerRiskMetric::getPendingAmount, Comparator.reverseOrder());
|
} else {
|
sortComparator = Comparator
|
.comparing(CustomerRiskMetric::getPendingAmount, Comparator.reverseOrder())
|
.thenComparing(CustomerRiskMetric::getRiskScore, Comparator.reverseOrder());
|
}
|
|
List<CustomerRiskMetric> sorted = metrics.stream()
|
.sorted(sortComparator)
|
.limit(normalizeLimit(limit))
|
.collect(Collectors.toList());
|
|
List<Map<String, Object>> items = sorted.stream().map(this::toStrategyItem).collect(Collectors.toList());
|
long highPriorityCount = items.stream().filter(item -> "high".equals(item.get("priority"))).count();
|
long mediumPriorityCount = items.stream().filter(item -> "medium".equals(item.get("priority"))).count();
|
long lowPriorityCount = items.stream().filter(item -> "low".equals(item.get("priority"))).count();
|
|
Map<String, Object> summary = rangeSummary(range, items.size(), keyword);
|
summary.put("highPriorityCount", highPriorityCount);
|
summary.put("mediumPriorityCount", mediumPriorityCount);
|
summary.put("lowPriorityCount", lowPriorityCount);
|
summary.put("prioritizeHighRisk", highRiskFirst);
|
summary.put("priorityMode", highRiskFirst ? "high_risk_first" : "pending_amount_first");
|
|
Map<String, Object> charts = new LinkedHashMap<>();
|
charts.put("pendingAmountBarOption", buildPendingAmountBarOption(sorted));
|
charts.put("priorityPieOption", buildPriorityPieOption(highPriorityCount, mediumPriorityCount, lowPriorityCount));
|
|
return jsonResponse(true, "sales_collection_quote_strategy", "已生成回款与报价策略建议", summary, Map.of("items", items), charts);
|
}
|
|
private List<CustomerRiskMetric> buildCustomerRiskMetrics(LoginUser loginUser, DateRange range, String keyword) {
|
List<SalesLedger> ledgers = querySalesLedgers(loginUser, range).stream()
|
.filter(item -> matchLedgerCustomerKeyword(item, keyword))
|
.collect(Collectors.toList());
|
if (ledgers.isEmpty()) {
|
return List.of();
|
}
|
|
Map<String, CustomerRiskMetric> metricMap = new LinkedHashMap<>();
|
for (SalesLedger ledger : ledgers) {
|
String customerName = StringUtils.hasText(ledger.getCustomerName()) ? ledger.getCustomerName().trim() : "未知客户";
|
CustomerRiskMetric metric = metricMap.computeIfAbsent(customerName, CustomerRiskMetric::new);
|
metric.setOrderCount(metric.getOrderCount() + 1);
|
metric.setContractAmount(metric.getContractAmount().add(defaultDecimal(ledger.getContractAmount())));
|
metric.setTopSingleOrderAmount(metric.getTopSingleOrderAmount().max(defaultDecimal(ledger.getContractAmount())));
|
LocalDate entryDate = toLocalDate(ledger.getEntryDate());
|
if (entryDate != null && (metric.getLastOrderDate() == null || entryDate.isAfter(metric.getLastOrderDate()))) {
|
metric.setLastOrderDate(entryDate);
|
}
|
if (ledger.getId() != null) {
|
metric.getLedgerIds().add(ledger.getId());
|
if (ledger.getDeliveryDate() != null) {
|
metric.getDeliveryDateByLedgerId().put(ledger.getId(), ledger.getDeliveryDate());
|
}
|
}
|
}
|
|
List<Long> allLedgerIds = metricMap.values().stream()
|
.flatMap(metric -> metric.getLedgerIds().stream())
|
.distinct()
|
.collect(Collectors.toList());
|
Map<Long, BigDecimal> receiptAmountByLedgerId = sumReceiptAmounts(loginUser, allLedgerIds);
|
Map<Long, List<ShippingInfo>> shippingByLedgerId = queryShippingsByLedgerIds(loginUser, allLedgerIds).stream()
|
.collect(Collectors.groupingBy(ShippingInfo::getSalesLedgerId));
|
|
List<SalesQuotation> quotations = querySalesQuotations(loginUser, range);
|
for (SalesQuotation quotation : quotations) {
|
String customerName = safe(quotation.getCustomer());
|
CustomerRiskMetric metric = metricMap.get(customerName);
|
if (metric == null) {
|
continue;
|
}
|
metric.setQuoteCount(metric.getQuoteCount() + 1);
|
metric.setQuoteAmount(metric.getQuoteAmount().add(defaultDecimal(quotation.getTotalAmount())));
|
}
|
|
LocalDate today = LocalDate.now();
|
for (CustomerRiskMetric metric : metricMap.values()) {
|
BigDecimal receivedAmount = BigDecimal.ZERO;
|
long overdueDeliveryCount = 0;
|
for (Long ledgerId : metric.getLedgerIds()) {
|
receivedAmount = receivedAmount.add(receiptAmountByLedgerId.getOrDefault(ledgerId, BigDecimal.ZERO));
|
LocalDate deliveryDate = metric.getDeliveryDateByLedgerId().get(ledgerId);
|
if (deliveryDate != null && deliveryDate.isBefore(today) && !isLedgerFullyShipped(ledgerId, shippingByLedgerId)) {
|
overdueDeliveryCount++;
|
}
|
}
|
metric.setReceivedAmount(receivedAmount);
|
metric.setPendingAmount(maxZero(metric.getContractAmount().subtract(receivedAmount)));
|
if (metric.getContractAmount().compareTo(BigDecimal.ZERO) > 0) {
|
metric.setPendingRate(metric.getPendingAmount()
|
.divide(metric.getContractAmount(), 4, RoundingMode.HALF_UP));
|
} else {
|
metric.setPendingRate(BigDecimal.ZERO);
|
}
|
metric.setOverdueDeliveryCount(overdueDeliveryCount);
|
if (metric.getLastOrderDate() == null) {
|
metric.setDaysSinceLastOrder(999);
|
} else {
|
metric.setDaysSinceLastOrder(Math.max(today.toEpochDay() - metric.getLastOrderDate().toEpochDay(), 0));
|
}
|
evaluateRiskMetric(metric);
|
}
|
return new ArrayList<>(metricMap.values());
|
}
|
|
private void evaluateRiskMetric(CustomerRiskMetric metric) {
|
int score = 0;
|
List<String> reasons = new ArrayList<>();
|
if (metric.getDaysSinceLastOrder() >= 90) {
|
score += 35;
|
reasons.add("近90天无新增订单");
|
} else if (metric.getDaysSinceLastOrder() >= 60) {
|
score += 25;
|
reasons.add("近60天订单活跃度下降");
|
} else if (metric.getDaysSinceLastOrder() >= 30) {
|
score += 12;
|
reasons.add("近30天订单波动偏弱");
|
}
|
|
if (metric.getPendingRate().compareTo(new BigDecimal("0.60")) >= 0) {
|
score += 30;
|
reasons.add("待回款占比高于60%");
|
} else if (metric.getPendingRate().compareTo(new BigDecimal("0.30")) >= 0) {
|
score += 20;
|
reasons.add("待回款占比高于30%");
|
} else if (metric.getPendingRate().compareTo(new BigDecimal("0.10")) >= 0) {
|
score += 10;
|
reasons.add("存在待回款风险");
|
}
|
|
if (metric.getOverdueDeliveryCount() > 0) {
|
score += Math.min((int) metric.getOverdueDeliveryCount() * 6, 20);
|
reasons.add("存在交期逾期订单");
|
}
|
|
if (metric.getOrderCount() <= 1) {
|
score += 8;
|
reasons.add("订单基数偏低");
|
}
|
|
if (metric.getQuoteCount() > 0 && metric.getOrderCount() == 0) {
|
score += 10;
|
reasons.add("报价未形成订单转化");
|
}
|
|
score = Math.min(score, 100);
|
metric.setRiskScore(score);
|
if (score >= 70) {
|
metric.setRiskLevel("high");
|
} else if (score >= 40) {
|
metric.setRiskLevel("medium");
|
} else {
|
metric.setRiskLevel("low");
|
}
|
metric.setRiskReasons(reasons);
|
}
|
|
private Map<String, Object> toRiskItem(CustomerRiskMetric metric) {
|
Map<String, Object> map = new LinkedHashMap<>();
|
map.put("customerName", metric.getCustomerName());
|
map.put("riskLevel", metric.getRiskLevel());
|
map.put("riskScore", metric.getRiskScore());
|
map.put("contractAmount", metric.getContractAmount());
|
map.put("receivedAmount", metric.getReceivedAmount());
|
map.put("pendingAmount", metric.getPendingAmount());
|
map.put("pendingRate", toPercent(metric.getPendingRate()));
|
map.put("orderCount", metric.getOrderCount());
|
map.put("quoteCount", metric.getQuoteCount());
|
map.put("overdueDeliveryCount", metric.getOverdueDeliveryCount());
|
map.put("daysSinceLastOrder", metric.getDaysSinceLastOrder());
|
map.put("lastOrderDate", formatDate(metric.getLastOrderDate()));
|
map.put("riskReasons", metric.getRiskReasons());
|
return map;
|
}
|
|
private Map<String, Object> toStrategyItem(CustomerRiskMetric metric) {
|
String priority = strategyPriority(metric);
|
Map<String, Object> map = new LinkedHashMap<>();
|
map.put("customerName", metric.getCustomerName());
|
map.put("riskLevel", metric.getRiskLevel());
|
map.put("riskScore", metric.getRiskScore());
|
map.put("priority", priority);
|
map.put("pendingAmount", metric.getPendingAmount());
|
map.put("pendingRate", toPercent(metric.getPendingRate()));
|
map.put("quoteCount", metric.getQuoteCount());
|
map.put("orderCount", metric.getOrderCount());
|
map.put("quoteConversionRate", toRate(metric.getOrderCount(), Math.max(metric.getQuoteCount(), 1)));
|
map.put("collectionStrategy", buildCollectionStrategy(metric));
|
map.put("quotationStrategy", buildQuotationStrategy(metric));
|
map.put("nextAction", buildNextAction(priority));
|
map.put("topSingleOrderAmount", metric.getTopSingleOrderAmount());
|
return map;
|
}
|
|
private String buildCollectionStrategy(CustomerRiskMetric metric) {
|
if (metric.getPendingAmount().compareTo(BigDecimal.ZERO) <= 0) {
|
return "保持正常月度对账与回款确认,维持客户回款节奏。";
|
}
|
if (metric.getPendingRate().compareTo(new BigDecimal("0.60")) >= 0) {
|
return "优先锁定回款计划,按周拆分回款节点并绑定发货条件,避免新增信用敞口。";
|
}
|
if (metric.getPendingRate().compareTo(new BigDecimal("0.30")) >= 0) {
|
return "建议执行双周催收机制,同步财务与业务联合跟进重点合同。";
|
}
|
return "保持正常催收节奏,按合同节点提前3天提醒客户付款。";
|
}
|
|
private String buildQuotationStrategy(CustomerRiskMetric metric) {
|
if ("high".equals(metric.getRiskLevel())) {
|
return "报价优先保毛利与回款条款,减少超长账期,必要时采用分阶段报价。";
|
}
|
if (metric.getQuoteCount() > 0 && metric.getOrderCount() < metric.getQuoteCount()) {
|
return "优化报价结构,建议提供基础版+升级版组合报价,提高转化率。";
|
}
|
if (metric.getOrderCount() <= 1) {
|
return "加强需求挖掘,围绕客户场景补充增值项与交付保障条款。";
|
}
|
return "保持当前报价策略,重点围绕交期和服务能力做差异化呈现。";
|
}
|
|
private String buildNextAction(String priority) {
|
return switch (priority) {
|
case "high" -> "48小时内完成客户回访,确认回款计划并复核报价有效期。";
|
case "medium" -> "本周内完成客户需求复盘,更新报价版本并同步回款节点。";
|
default -> "保持月度例行跟进,持续追踪客户采购计划变化。";
|
};
|
}
|
|
private String strategyPriority(CustomerRiskMetric metric) {
|
if ("high".equals(metric.getRiskLevel()) || metric.getPendingRate().compareTo(new BigDecimal("0.50")) >= 0) {
|
return "high";
|
}
|
if ("medium".equals(metric.getRiskLevel()) || metric.getPendingRate().compareTo(new BigDecimal("0.30")) >= 0) {
|
return "medium";
|
}
|
return "low";
|
}
|
|
private int riskLevelRank(String riskLevel) {
|
if ("high".equals(riskLevel)) {
|
return 3;
|
}
|
if ("medium".equals(riskLevel)) {
|
return 2;
|
}
|
return 1;
|
}
|
|
private List<Map<String, Object>> buildTopCustomers(List<SalesLedger> ledgers) {
|
Map<String, BigDecimal> grouped = new LinkedHashMap<>();
|
for (SalesLedger ledger : ledgers) {
|
String customerName = StringUtils.hasText(ledger.getCustomerName()) ? ledger.getCustomerName().trim() : "未知客户";
|
grouped.merge(customerName, defaultDecimal(ledger.getContractAmount()), BigDecimal::add);
|
}
|
return grouped.entrySet().stream()
|
.sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed())
|
.limit(5)
|
.map(entry -> {
|
Map<String, Object> map = new LinkedHashMap<>();
|
map.put("customerName", entry.getKey());
|
map.put("contractAmount", entry.getValue());
|
return map;
|
})
|
.collect(Collectors.toList());
|
}
|
|
private TrendData buildContractTrendData(List<SalesLedger> ledgers, DateRange range) {
|
Map<String, BigDecimal> amountByMonth = new LinkedHashMap<>();
|
YearMonth startMonth = YearMonth.from(range.start());
|
YearMonth endMonth = YearMonth.from(range.end());
|
for (YearMonth month = startMonth; !month.isAfter(endMonth); month = month.plusMonths(1)) {
|
amountByMonth.put(month.toString(), BigDecimal.ZERO);
|
}
|
for (SalesLedger ledger : ledgers) {
|
LocalDate entryDate = toLocalDate(ledger.getEntryDate());
|
if (entryDate == null) {
|
continue;
|
}
|
String monthKey = YearMonth.from(entryDate).toString();
|
if (!amountByMonth.containsKey(monthKey)) {
|
continue;
|
}
|
amountByMonth.put(monthKey, amountByMonth.get(monthKey).add(defaultDecimal(ledger.getContractAmount())));
|
}
|
return new TrendData(new ArrayList<>(amountByMonth.keySet()), new ArrayList<>(amountByMonth.values()));
|
}
|
|
private List<SalesLedger> querySalesLedgers(LoginUser loginUser, DateRange range) {
|
LambdaQueryWrapper<SalesLedger> wrapper = new LambdaQueryWrapper<>();
|
applyTenantFilter(wrapper, loginUser.getTenantId(), SalesLedger::getTenantId);
|
applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesLedger::getDeptId);
|
if (range != null) {
|
wrapper.ge(SalesLedger::getEntryDate, toDate(range.start()))
|
.lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end()));
|
}
|
return defaultList(salesLedgerMapper.selectList(wrapper));
|
}
|
|
private List<SalesQuotation> querySalesQuotations(LoginUser loginUser, DateRange range) {
|
LambdaQueryWrapper<SalesQuotation> wrapper = new LambdaQueryWrapper<>();
|
applyTenantFilter(wrapper, loginUser.getTenantId(), SalesQuotation::getTenantId);
|
applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesQuotation::getDeptId);
|
if (range != null) {
|
wrapper.ge(SalesQuotation::getQuotationDate, range.start())
|
.le(SalesQuotation::getQuotationDate, range.end());
|
}
|
return defaultList(salesQuotationMapper.selectList(wrapper));
|
}
|
|
private List<ShippingInfo> queryShippings(LoginUser loginUser, DateRange range) {
|
LambdaQueryWrapper<ShippingInfo> wrapper = new LambdaQueryWrapper<>();
|
applyTenantFilter(wrapper, loginUser.getTenantId(), ShippingInfo::getTenantId);
|
applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ShippingInfo::getDeptId);
|
if (range != null) {
|
wrapper.ge(ShippingInfo::getShippingDate, toDate(range.start()))
|
.lt(ShippingInfo::getShippingDate, toExclusiveEndDate(range.end()));
|
}
|
return defaultList(shippingInfoMapper.selectList(wrapper));
|
}
|
|
private List<ReceiptPayment> queryReceipts(LoginUser loginUser, DateRange range) {
|
LambdaQueryWrapper<ReceiptPayment> wrapper = new LambdaQueryWrapper<>();
|
applyTenantFilter(wrapper, loginUser.getTenantId(), ReceiptPayment::getTenantId);
|
applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ReceiptPayment::getDeptId);
|
if (range != null) {
|
wrapper.ge(ReceiptPayment::getReceiptPaymentDate, range.start())
|
.le(ReceiptPayment::getReceiptPaymentDate, range.end());
|
}
|
return defaultList(receiptPaymentMapper.selectList(wrapper));
|
}
|
|
private List<ReceiptPayment> queryReceiptsByLedgerIds(LoginUser loginUser, List<Long> ledgerIds) {
|
if (ledgerIds == null || ledgerIds.isEmpty()) {
|
return List.of();
|
}
|
LambdaQueryWrapper<ReceiptPayment> wrapper = new LambdaQueryWrapper<>();
|
applyTenantFilter(wrapper, loginUser.getTenantId(), ReceiptPayment::getTenantId);
|
applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ReceiptPayment::getDeptId);
|
wrapper.in(ReceiptPayment::getSalesLedgerId, ledgerIds);
|
return defaultList(receiptPaymentMapper.selectList(wrapper));
|
}
|
|
private List<ShippingInfo> queryShippingsByLedgerIds(LoginUser loginUser, List<Long> ledgerIds) {
|
if (ledgerIds == null || ledgerIds.isEmpty()) {
|
return List.of();
|
}
|
LambdaQueryWrapper<ShippingInfo> wrapper = new LambdaQueryWrapper<>();
|
applyTenantFilter(wrapper, loginUser.getTenantId(), ShippingInfo::getTenantId);
|
applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ShippingInfo::getDeptId);
|
wrapper.in(ShippingInfo::getSalesLedgerId, ledgerIds);
|
return defaultList(shippingInfoMapper.selectList(wrapper));
|
}
|
|
private Map<Long, BigDecimal> sumInvoiceAmounts(List<Long> ledgerIds) {
|
if (ledgerIds == null || ledgerIds.isEmpty()) {
|
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;
|
}
|
|
private Map<Long, BigDecimal> sumReceiptAmounts(LoginUser loginUser, List<Long> ledgerIds) {
|
Map<Long, BigDecimal> result = new HashMap<>();
|
for (ReceiptPayment item : queryReceiptsByLedgerIds(loginUser, ledgerIds)) {
|
if (item.getSalesLedgerId() == null) {
|
continue;
|
}
|
result.merge(item.getSalesLedgerId(), defaultDecimal(item.getReceiptPaymentAmount()), BigDecimal::add);
|
}
|
return result;
|
}
|
|
private boolean isLedgerFullyShipped(Long ledgerId, Map<Long, List<ShippingInfo>> shippingByLedgerId) {
|
List<ShippingInfo> shippingInfos = shippingByLedgerId.get(ledgerId);
|
if (shippingInfos == null || shippingInfos.isEmpty()) {
|
return false;
|
}
|
return shippingInfos.stream().allMatch(item -> isShippedStatus(item.getStatus()));
|
}
|
|
private String calcLedgerShippingStatus(List<ShippingInfo> shippingInfos) {
|
if (shippingInfos == null || shippingInfos.isEmpty()) {
|
return "未发货";
|
}
|
long shippedCount = shippingInfos.stream().filter(item -> isShippedStatus(item.getStatus())).count();
|
if (shippedCount == 0) {
|
return "待发货";
|
}
|
if (shippedCount == shippingInfos.size()) {
|
return "已发货";
|
}
|
return "部分发货";
|
}
|
|
private boolean isShippedStatus(String status) {
|
return StringUtils.hasText(status) && status.contains("已发货");
|
}
|
|
private boolean matchCustomerKeyword(CustomerVo customer, String keyword) {
|
if (!StringUtils.hasText(keyword)) {
|
return true;
|
}
|
String text = keyword.trim();
|
return safe(customer.getCustomerName()).contains(text)
|
|| safe(customer.getContactPerson()).contains(text)
|
|| safe(customer.getContactPhone()).contains(text)
|
|| safe(customer.getCompanyPhone()).contains(text)
|
|| safe(customer.getUsageUserName()).contains(text);
|
}
|
|
private boolean matchInteractionKeyword(ReceiptPayment payment, SalesLedger ledger, String keyword) {
|
if (!StringUtils.hasText(keyword)) {
|
return true;
|
}
|
String text = keyword.trim();
|
return safe(payment.getRegistrant()).contains(text)
|
|| (ledger != null && (safe(ledger.getCustomerName()).contains(text)
|
|| safe(ledger.getSalesContractNo()).contains(text)
|
|| safe(ledger.getProjectName()).contains(text)));
|
}
|
|
private boolean matchLedgerCustomerKeyword(SalesLedger ledger, String keyword) {
|
if (!StringUtils.hasText(keyword)) {
|
return true;
|
}
|
String text = keyword.trim();
|
return safe(ledger.getCustomerName()).contains(text)
|
|| safe(ledger.getSalesContractNo()).contains(text)
|
|| safe(ledger.getProjectName()).contains(text);
|
}
|
|
private Integer normalizeSeaType(String seaType) {
|
if (!StringUtils.hasText(seaType)) {
|
return null;
|
}
|
String value = seaType.trim().toLowerCase(Locale.ROOT);
|
return switch (value) {
|
case "private", "私海", "0" -> 0;
|
case "public", "公海", "1" -> 1;
|
default -> null;
|
};
|
}
|
|
private String customerSeaTypeName(Integer type) {
|
if (type == null) {
|
return "未知";
|
}
|
return type == 1 ? "公海" : "私海";
|
}
|
|
private int normalizeLimit(Integer limit) {
|
if (limit == null || limit <= 0) {
|
return DEFAULT_LIMIT;
|
}
|
return Math.min(limit, MAX_LIMIT);
|
}
|
|
private boolean tenantMatched(Long dataTenantId, Long userTenantId) {
|
if (userTenantId == null) {
|
return true;
|
}
|
return Objects.equals(dataTenantId, userTenantId);
|
}
|
|
private <T> void applyTenantFilter(LambdaQueryWrapper<T> wrapper, Long tenantId, SFunction<T, Long> field) {
|
if (tenantId != null) {
|
wrapper.eq(field, tenantId);
|
}
|
}
|
|
private <T> void applyDeptFilter(LambdaQueryWrapper<T> wrapper, Long deptId, SFunction<T, Long> 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 DateRange resolveDateRange(String startDate, String endDate, String timeRange) {
|
LocalDate today = LocalDate.now();
|
LocalDate explicitStart = parseLocalDate(startDate);
|
LocalDate explicitEnd = parseLocalDate(endDate);
|
if (explicitStart != null || explicitEnd != null) {
|
LocalDate start = explicitStart != null ? explicitStart : explicitEnd;
|
LocalDate end = explicitEnd != null ? explicitEnd : explicitStart;
|
if (start.isAfter(end)) {
|
LocalDate temp = start;
|
start = end;
|
end = temp;
|
}
|
return new DateRange(start, end, start + "至" + end);
|
}
|
if (!StringUtils.hasText(timeRange)) {
|
return new DateRange(today.minusDays(29), today, "近30天");
|
}
|
String text = timeRange.trim();
|
if (text.contains("今天")) {
|
return new DateRange(today, today, "今天");
|
}
|
if (text.contains("昨天") || text.contains("昨日")) {
|
LocalDate day = today.minusDays(1);
|
return new DateRange(day, day, "昨天");
|
}
|
if (text.contains("本周")) {
|
LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
|
return new DateRange(start, today, "本周");
|
}
|
if (text.contains("上周")) {
|
LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L);
|
LocalDate start = thisWeekStart.minusWeeks(1);
|
LocalDate end = thisWeekStart.minusDays(1);
|
return new DateRange(start, end, "上周");
|
}
|
if (text.contains("本月")) {
|
return new DateRange(today.withDayOfMonth(1), today, "本月");
|
}
|
if (text.contains("上月")) {
|
YearMonth lastMonth = YearMonth.from(today).minusMonths(1);
|
return new DateRange(lastMonth.atDay(1), lastMonth.atEndOfMonth(), "上月");
|
}
|
if (text.contains("今年") || text.contains("本年")) {
|
return new DateRange(today.withDayOfYear(1), today, "今年");
|
}
|
if (text.contains("去年")) {
|
LocalDate start = today.minusYears(1).withDayOfYear(1);
|
LocalDate end = today.minusYears(1).withMonth(12).withDayOfMonth(31);
|
return new DateRange(start, end, "去年");
|
}
|
Matcher relativeMatcher = RELATIVE_PATTERN.matcher(text);
|
if (relativeMatcher.find()) {
|
int amount = Integer.parseInt(relativeMatcher.group(2));
|
String unit = relativeMatcher.group(3);
|
LocalDate start = 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(start, today, "近" + amount + unit);
|
}
|
Matcher dateMatcher = DATE_PATTERN.matcher(text);
|
if (dateMatcher.find()) {
|
LocalDate start = parseLocalDate(dateMatcher.group(1));
|
LocalDate end = dateMatcher.find() ? parseLocalDate(dateMatcher.group(1)) : start;
|
if (start != null && end != null) {
|
if (start.isAfter(end)) {
|
LocalDate temp = start;
|
start = end;
|
end = temp;
|
}
|
return new DateRange(start, end, start + "至" + end);
|
}
|
}
|
return new DateRange(today.minusDays(29), today, "近30天");
|
}
|
|
private LocalDate parseLocalDate(String text) {
|
if (!StringUtils.hasText(text)) {
|
return null;
|
}
|
try {
|
return LocalDate.parse(text.trim(), DATE_FMT);
|
} catch (Exception ignored) {
|
return null;
|
}
|
}
|
|
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 LocalDate toLocalDate(Date date) {
|
if (date == null) {
|
return null;
|
}
|
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
}
|
|
private String formatDate(Date date) {
|
LocalDate localDate = toLocalDate(date);
|
return formatDate(localDate);
|
}
|
|
private String formatDate(LocalDate date) {
|
return date == null ? "" : date.format(DATE_FMT);
|
}
|
|
private String formatDateTime(LocalDateTime time) {
|
return time == null ? "" : time.toString().replace('T', ' ');
|
}
|
|
private BigDecimal defaultDecimal(BigDecimal value) {
|
return value == null ? BigDecimal.ZERO : value;
|
}
|
|
private BigDecimal maxZero(BigDecimal value) {
|
return value == null || value.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : value;
|
}
|
|
private String toRate(long numerator, long denominator) {
|
if (denominator <= 0) {
|
return "0.00%";
|
}
|
BigDecimal rate = new BigDecimal(numerator)
|
.multiply(ONE_HUNDRED)
|
.divide(new BigDecimal(denominator), 2, RoundingMode.HALF_UP);
|
return rate.toPlainString() + "%";
|
}
|
|
private String toPercent(BigDecimal decimal) {
|
if (decimal == null) {
|
return "0.00%";
|
}
|
BigDecimal rate = decimal.multiply(ONE_HUNDRED).setScale(2, RoundingMode.HALF_UP);
|
return rate.toPlainString() + "%";
|
}
|
|
private String safe(Object value) {
|
return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ').trim();
|
}
|
|
private <T> List<T> defaultList(List<T> list) {
|
return list == null ? List.of() : list;
|
}
|
|
private Map<String, Object> rangeSummary(DateRange range, int count, String keyword) {
|
Map<String, Object> summary = new LinkedHashMap<>();
|
summary.put("timeRange", range.label());
|
summary.put("startDate", range.start().toString());
|
summary.put("endDate", range.end().toString());
|
summary.put("count", count);
|
summary.put("keyword", safe(keyword));
|
return summary;
|
}
|
|
private Map<String, Object> buildAmountBarOption(BigDecimal contractAmount,
|
BigDecimal quotationAmount,
|
BigDecimal receivedAmount,
|
BigDecimal pendingAmount) {
|
List<String> xData = List.of("合同额", "报价额", "回款额", "待回款");
|
List<BigDecimal> yData = List.of(contractAmount, quotationAmount, receivedAmount, pendingAmount);
|
Map<String, Object> option = new LinkedHashMap<>();
|
option.put("title", Map.of("text", "销售经营金额概览", "left", "center"));
|
option.put("tooltip", Map.of("trigger", "axis"));
|
option.put("xAxis", Map.of("type", "category", "data", xData));
|
option.put("yAxis", Map.of("type", "value"));
|
option.put("series", List.of(Map.of("name", "金额", "type", "bar", "data", yData)));
|
return option;
|
}
|
|
private Map<String, Object> buildShippingPieOption(long shippedCount, long pendingCount) {
|
List<Map<String, Object>> data = List.of(
|
Map.of("name", "已发货", "value", shippedCount),
|
Map.of("name", "未发货", "value", pendingCount)
|
);
|
Map<String, Object> option = new LinkedHashMap<>();
|
option.put("title", Map.of("text", "发货状态分布", "left", "center"));
|
option.put("tooltip", Map.of("trigger", "item"));
|
option.put("series", List.of(Map.of("type", "pie", "radius", "60%", "data", data)));
|
return option;
|
}
|
|
private Map<String, Object> buildCustomerTopBarOption(List<Map<String, Object>> topCustomers) {
|
List<String> xData = new ArrayList<>();
|
List<BigDecimal> yData = new ArrayList<>();
|
for (Map<String, Object> item : topCustomers) {
|
xData.add(String.valueOf(item.get("customerName")));
|
yData.add((BigDecimal) item.get("contractAmount"));
|
}
|
Map<String, Object> option = new LinkedHashMap<>();
|
option.put("title", Map.of("text", "客户合同额TOP5", "left", "center"));
|
option.put("tooltip", Map.of("trigger", "axis"));
|
option.put("xAxis", Map.of("type", "category", "data", xData));
|
option.put("yAxis", Map.of("type", "value"));
|
option.put("series", List.of(Map.of("name", "合同额", "type", "bar", "data", yData)));
|
return option;
|
}
|
|
private Map<String, Object> buildContractTrendLineOption(List<String> labels, List<BigDecimal> values) {
|
Map<String, Object> option = new LinkedHashMap<>();
|
option.put("title", Map.of("text", "合同额月度趋势", "left", "center"));
|
option.put("tooltip", Map.of("trigger", "axis"));
|
option.put("xAxis", Map.of("type", "category", "data", labels));
|
option.put("yAxis", Map.of("type", "value"));
|
option.put("series", List.of(Map.of("name", "合同额", "type", "line", "smooth", true, "data", values)));
|
return option;
|
}
|
|
private Map<String, Object> buildRiskLevelPieOption(long highCount, long mediumCount, long lowCount) {
|
List<Map<String, Object>> data = List.of(
|
Map.of("name", "高风险", "value", highCount),
|
Map.of("name", "中风险", "value", mediumCount),
|
Map.of("name", "低风险", "value", lowCount)
|
);
|
Map<String, Object> option = new LinkedHashMap<>();
|
option.put("title", Map.of("text", "客户风险等级分布", "left", "center"));
|
option.put("tooltip", Map.of("trigger", "item"));
|
option.put("series", List.of(Map.of("name", "风险等级", "type", "pie", "radius", "60%", "data", data)));
|
return option;
|
}
|
|
private Map<String, Object> buildRiskScoreBarOption(List<CustomerRiskMetric> metrics) {
|
List<String> xData = metrics.stream().map(CustomerRiskMetric::getCustomerName).collect(Collectors.toList());
|
List<Integer> yData = metrics.stream().map(CustomerRiskMetric::getRiskScore).collect(Collectors.toList());
|
Map<String, Object> option = new LinkedHashMap<>();
|
option.put("title", Map.of("text", "客户风险分值", "left", "center"));
|
option.put("tooltip", Map.of("trigger", "axis"));
|
option.put("xAxis", Map.of("type", "category", "data", xData));
|
option.put("yAxis", Map.of("type", "value", "max", 100));
|
option.put("series", List.of(Map.of("name", "风险分值", "type", "bar", "data", yData)));
|
return option;
|
}
|
|
private Map<String, Object> buildPendingAmountBarOption(List<CustomerRiskMetric> metrics) {
|
List<String> xData = metrics.stream().map(CustomerRiskMetric::getCustomerName).collect(Collectors.toList());
|
List<BigDecimal> yData = metrics.stream().map(CustomerRiskMetric::getPendingAmount).collect(Collectors.toList());
|
Map<String, Object> option = new LinkedHashMap<>();
|
option.put("title", Map.of("text", "客户待回款排名", "left", "center"));
|
option.put("tooltip", Map.of("trigger", "axis"));
|
option.put("xAxis", Map.of("type", "category", "data", xData));
|
option.put("yAxis", Map.of("type", "value"));
|
option.put("series", List.of(Map.of("name", "待回款", "type", "bar", "data", yData)));
|
return option;
|
}
|
|
private Map<String, Object> buildPriorityPieOption(long high, long medium, long low) {
|
List<Map<String, Object>> data = List.of(
|
Map.of("name", "高优先级", "value", high),
|
Map.of("name", "中优先级", "value", medium),
|
Map.of("name", "低优先级", "value", low)
|
);
|
Map<String, Object> option = new LinkedHashMap<>();
|
option.put("title", Map.of("text", "策略优先级分布", "left", "center"));
|
option.put("tooltip", Map.of("trigger", "item"));
|
option.put("series", List.of(Map.of("name", "优先级", "type", "pie", "radius", "60%", "data", data)));
|
return option;
|
}
|
|
private String jsonResponse(boolean success,
|
String type,
|
String description,
|
Map<String, Object> summary,
|
Map<String, Object> data,
|
Map<String, Object> charts) {
|
Map<String, Object> 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 record TrendData(List<String> labels, List<BigDecimal> values) {
|
private List<Map<String, Object>> toItemList() {
|
List<Map<String, Object>> items = new LinkedList<>();
|
for (int i = 0; i < labels.size(); i++) {
|
Map<String, Object> item = new LinkedHashMap<>();
|
item.put("month", labels.get(i));
|
item.put("amount", values.get(i));
|
items.add(item);
|
}
|
return items;
|
}
|
}
|
|
private static class CustomerRiskMetric {
|
private final String customerName;
|
private final List<Long> ledgerIds = new ArrayList<>();
|
private final Map<Long, LocalDate> deliveryDateByLedgerId = new HashMap<>();
|
private BigDecimal contractAmount = BigDecimal.ZERO;
|
private BigDecimal receivedAmount = BigDecimal.ZERO;
|
private BigDecimal pendingAmount = BigDecimal.ZERO;
|
private BigDecimal pendingRate = BigDecimal.ZERO;
|
private BigDecimal quoteAmount = BigDecimal.ZERO;
|
private BigDecimal topSingleOrderAmount = BigDecimal.ZERO;
|
private int orderCount;
|
private int quoteCount;
|
private LocalDate lastOrderDate;
|
private long daysSinceLastOrder;
|
private long overdueDeliveryCount;
|
private int riskScore;
|
private String riskLevel = "low";
|
private List<String> riskReasons = new ArrayList<>();
|
|
private CustomerRiskMetric(String customerName) {
|
this.customerName = customerName;
|
}
|
|
private String getCustomerName() {
|
return customerName;
|
}
|
|
private List<Long> getLedgerIds() {
|
return ledgerIds;
|
}
|
|
private Map<Long, LocalDate> getDeliveryDateByLedgerId() {
|
return deliveryDateByLedgerId;
|
}
|
|
private BigDecimal getContractAmount() {
|
return contractAmount;
|
}
|
|
private void setContractAmount(BigDecimal contractAmount) {
|
this.contractAmount = contractAmount;
|
}
|
|
private BigDecimal getReceivedAmount() {
|
return receivedAmount;
|
}
|
|
private void setReceivedAmount(BigDecimal receivedAmount) {
|
this.receivedAmount = receivedAmount;
|
}
|
|
private BigDecimal getPendingAmount() {
|
return pendingAmount;
|
}
|
|
private void setPendingAmount(BigDecimal pendingAmount) {
|
this.pendingAmount = pendingAmount;
|
}
|
|
private BigDecimal getPendingRate() {
|
return pendingRate;
|
}
|
|
private void setPendingRate(BigDecimal pendingRate) {
|
this.pendingRate = pendingRate;
|
}
|
|
private BigDecimal getQuoteAmount() {
|
return quoteAmount;
|
}
|
|
private void setQuoteAmount(BigDecimal quoteAmount) {
|
this.quoteAmount = quoteAmount;
|
}
|
|
private BigDecimal getTopSingleOrderAmount() {
|
return topSingleOrderAmount;
|
}
|
|
private void setTopSingleOrderAmount(BigDecimal topSingleOrderAmount) {
|
this.topSingleOrderAmount = topSingleOrderAmount;
|
}
|
|
private int getOrderCount() {
|
return orderCount;
|
}
|
|
private void setOrderCount(int orderCount) {
|
this.orderCount = orderCount;
|
}
|
|
private int getQuoteCount() {
|
return quoteCount;
|
}
|
|
private void setQuoteCount(int quoteCount) {
|
this.quoteCount = quoteCount;
|
}
|
|
private LocalDate getLastOrderDate() {
|
return lastOrderDate;
|
}
|
|
private void setLastOrderDate(LocalDate lastOrderDate) {
|
this.lastOrderDate = lastOrderDate;
|
}
|
|
private long getDaysSinceLastOrder() {
|
return daysSinceLastOrder;
|
}
|
|
private void setDaysSinceLastOrder(long daysSinceLastOrder) {
|
this.daysSinceLastOrder = daysSinceLastOrder;
|
}
|
|
private long getOverdueDeliveryCount() {
|
return overdueDeliveryCount;
|
}
|
|
private void setOverdueDeliveryCount(long overdueDeliveryCount) {
|
this.overdueDeliveryCount = overdueDeliveryCount;
|
}
|
|
private int getRiskScore() {
|
return riskScore;
|
}
|
|
private void setRiskScore(int riskScore) {
|
this.riskScore = riskScore;
|
}
|
|
private String getRiskLevel() {
|
return riskLevel;
|
}
|
|
private void setRiskLevel(String riskLevel) {
|
this.riskLevel = riskLevel;
|
}
|
|
private List<String> getRiskReasons() {
|
return riskReasons;
|
}
|
|
private void setRiskReasons(List<String> riskReasons) {
|
this.riskReasons = riskReasons;
|
}
|
}
|
}
|