引言:为什么采购管理是跨境电商的命脉

在跨境电商的成本结构中,商品采购成本通常占到销售额的40%-60%。采购管理的好坏,直接决定了企业的毛利率和现金流健康度。

然而,很多跨境电商企业的采购管理还停留在"Excel+微信"的原始阶段:

痛点表现后果
信息孤岛采购数据散落在各处无法追溯、难以分析
流程混乱没有标准化审批流程越权采购、价格失控
库存失控凭感觉补货要么断货、要么积压
供应商管理缺失没有评级和淘汰机制质量不稳定、交期延误
成本核算困难多币种、多批次混乱毛利算不清

本文目标:设计一套适合5-7亿规模跨境电商的采购管理系统,从需求计划到入库结算,实现全流程数字化管理。


一、跨境电商采购的特殊性

1.1 与传统采购的核心差异

维度传统企业采购跨境电商采购
供应商分布国内为主国内+海外(1688、阿里国际站、海外工厂)
结算币种单一(CNY)多币种(CNY、USD、EUR等)
付款方式银行转账、承兑T/T、信用证、PayPal、西联
采购周期相对固定差异大(国内7天 vs 海运45天)
质检要求标准化需适应目的国标准(CE、FCC、FDA等)
单据要求国内发票形式发票、装箱单、原产地证等

1.2 多币种采购的挑战

跨境电商经常面临这样的场景:

场景:从美国供应商采购一批电子配件
- 报价币种:USD
- 付款币种:USD(通过香港公司付款)
- 入库成本:需转换为CNY(国内公司记账)
- 销售币种:EUR(欧洲站销售)

汇率波动的影响

// 假设采购时汇率 1 USD = 7.2 CNY
// 采购成本:$100 × 7.2 = ¥720

// 一个月后付款时汇率 1 USD = 7.0 CNY
// 实际付款:$100 × 7.0 = ¥700
// 汇兑收益:¥20

// 但如果汇率变成 1 USD = 7.4 CNY
// 实际付款:$100 × 7.4 = ¥740
// 汇兑损失:¥20

1.3 海外供应商管理难点

时差问题

  • 美国供应商:时差12-15小时,沟通窗口有限
  • 欧洲供应商:时差6-8小时,下午才能联系

语言和文化差异

  • 合同条款理解偏差
  • 质量标准认知不同
  • 交期承诺的"弹性"

付款风险

  • 预付款比例高(30%-50%)
  • 跨境追款困难
  • 汇率锁定需求

1.4 国际物流协调

采购流程中的物流节点:

国内采购:工厂 → 国内仓(3-7天)
海外采购:海外工厂 → 港口 → 海运 → 清关 → 国内仓(30-60天)
         └── 空运可缩短至7-15天,但成本高3-5倍

关键决策点

  • 海运 vs 空运的成本效益分析
  • 整柜 vs 拼柜的选择
  • 清关时效的预估

二、采购全流程设计

2.1 流程总览

┌─────────────────────────────────────────────────────────────────────────┐
│                         采购管理全流程                                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐    ┌─────────┐│
│  │ 需求计划 │───▶│ 询价比价 │───▶│ 采购下单 │───▶│ 到货验收 │───▶│ 入库结算 ││
│  └─────────┘    └─────────┘    └─────────┘    └─────────┘    └─────────┘│
│       │              │              │              │              │     │
│       ▼              ▼              ▼              ▼              ▼     │
│   库存预警        供应商报价      PO审批        WMS收货       财务应付   │
│   销售预测        价格对比        合同签订      质检验收       成本核算   │
│   采购建议        供应商选择      付款申请      差异处理       凭证生成   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

2.2 环节一:需求计划

触发采购需求的三种方式

方式触发条件适用场景
安全库存预警可用库存 < 安全库存常规补货
销售预测驱动预测销量 × 备货周期大促备货
手动创建业务人员判断新品采购、临时需求

采购建议算法

/**
 * 采购建议生成服务
 *
 * 核心公式:
 * 建议采购量 = MAX(0, 目标库存 - 可用库存 - 在途库存 - 已下单未发货)
 *
 * 其中:
 * 目标库存 = 安全库存 + 日均销量 × (采购周期 + 安全天数)
 */
@Service
public class PurchaseSuggestionService {

    @Autowired
    private InventoryRepository inventoryRepository;

    @Autowired
    private SalesStatisticsService salesStatisticsService;

    @Autowired
    private PurchaseOrderRepository purchaseOrderRepository;

    /**
     * 生成单个SKU的采购建议
     */
    public PurchaseSuggestion generateSuggestion(String skuId, String warehouseId) {
        // 1. 获取SKU配置
        SkuPurchaseConfig config = getSkuConfig(skuId);

        // 2. 计算日均销量(取近30天数据)
        BigDecimal avgDailySales = salesStatisticsService
            .getAvgDailySales(skuId, 30);

        // 3. 计算目标库存
        int safetyStock = config.getSafetyStock();
        int leadTimeDays = config.getLeadTimeDays();  // 采购周期
        int safetyDays = config.getSafetyDays();      // 安全天数

        int targetStock = safetyStock +
            avgDailySales.multiply(BigDecimal.valueOf(leadTimeDays + safetyDays))
                .intValue();

        // 4. 获取当前库存状况
        int availableStock = inventoryRepository
            .getAvailableStock(skuId, warehouseId);
        int inTransitStock = purchaseOrderRepository
            .getInTransitQty(skuId, warehouseId);
        int pendingStock = purchaseOrderRepository
            .getPendingShipmentQty(skuId, warehouseId);

        // 5. 计算建议采购量
        int suggestQty = targetStock - availableStock - inTransitStock - pendingStock;
        suggestQty = Math.max(0, suggestQty);

        // 6. 考虑最小起订量(MOQ)
        int moq = config.getMinOrderQty();
        if (suggestQty > 0 && suggestQty < moq) {
            suggestQty = moq;
        }

        // 7. 考虑采购倍数
        int orderMultiple = config.getOrderMultiple();
        if (orderMultiple > 1) {
            suggestQty = (int) Math.ceil((double) suggestQty / orderMultiple)
                * orderMultiple;
        }

        return PurchaseSuggestion.builder()
            .skuId(skuId)
            .warehouseId(warehouseId)
            .currentStock(availableStock)
            .inTransitStock(inTransitStock)
            .targetStock(targetStock)
            .avgDailySales(avgDailySales)
            .suggestQty(suggestQty)
            .preferredSupplierId(config.getPreferredSupplierId())
            .urgencyLevel(calculateUrgency(availableStock, safetyStock, avgDailySales))
            .build();
    }

    /**
     * 计算紧急程度
     * - URGENT: 库存 < 安全库存的50%
     * - HIGH: 库存 < 安全库存
     * - NORMAL: 库存 < 目标库存
     */
    private String calculateUrgency(int available, int safety, BigDecimal avgSales) {
        if (available < safety * 0.5) {
            return "URGENT";
        } else if (available < safety) {
            return "HIGH";
        } else {
            return "NORMAL";
        }
    }
}

2.3 环节二:询价比价

询价流程

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  创建询价单  │────▶│  发送供应商  │────▶│  收集报价   │────▶│  比价分析   │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
       │                   │                   │                   │
       ▼                   ▼                   ▼                   ▼
   选择SKU列表         邮件/系统通知       供应商回复报价      生成比价报告
   指定目标供应商       设置截止时间       记录报价明细        推荐最优方案

比价维度

维度权重建议说明
单价40%含税价、到岸价
交期25%能否满足需求时间
质量20%历史质量表现
付款条件10%账期、预付比例
服务5%响应速度、售后支持

比价算法实现

@Service
public class QuotationCompareService {

    /**
     * 综合评分比价
     *
     * 综合得分 = 价格得分×40% + 交期得分×25% + 质量得分×20%
     *          + 付款得分×10% + 服务得分×5%
     */
    public List<QuotationRanking> compareQuotations(String inquiryId) {
        List<SupplierQuotation> quotations = quotationRepository
            .findByInquiryId(inquiryId);

        if (quotations.isEmpty()) {
            return Collections.emptyList();
        }

        // 找出最低价格(用于计算价格得分)
        BigDecimal minPrice = quotations.stream()
            .map(SupplierQuotation::getUnitPrice)
            .min(BigDecimal::compareTo)
            .orElse(BigDecimal.ONE);

        // 找出最短交期
        int minLeadTime = quotations.stream()
            .mapToInt(SupplierQuotation::getLeadTimeDays)
            .min()
            .orElse(1);

        List<QuotationRanking> rankings = new ArrayList<>();

        for (SupplierQuotation q : quotations) {
            QuotationRanking ranking = new QuotationRanking();
            ranking.setQuotationId(q.getId());
            ranking.setSupplierId(q.getSupplierId());
            ranking.setSupplierName(q.getSupplierName());

            // 价格得分:最低价/报价 × 100
            BigDecimal priceScore = minPrice
                .divide(q.getUnitPrice(), 4, RoundingMode.HALF_UP)
                .multiply(BigDecimal.valueOf(100));
            ranking.setPriceScore(priceScore);

            // 交期得分:最短交期/报价交期 × 100
            BigDecimal leadTimeScore = BigDecimal.valueOf(minLeadTime)
                .divide(BigDecimal.valueOf(q.getLeadTimeDays()), 4, RoundingMode.HALF_UP)
                .multiply(BigDecimal.valueOf(100));
            ranking.setLeadTimeScore(leadTimeScore);

            // 质量得分:取供应商历史质量评分
            BigDecimal qualityScore = getSupplierQualityScore(q.getSupplierId());
            ranking.setQualityScore(qualityScore);

            // 付款得分:账期越长越好
            BigDecimal paymentScore = calculatePaymentScore(q.getPaymentTerms());
            ranking.setPaymentScore(paymentScore);

            // 服务得分:取供应商服务评分
            BigDecimal serviceScore = getSupplierServiceScore(q.getSupplierId());
            ranking.setServiceScore(serviceScore);

            // 计算综合得分
            BigDecimal totalScore = priceScore.multiply(BigDecimal.valueOf(0.40))
                .add(leadTimeScore.multiply(BigDecimal.valueOf(0.25)))
                .add(qualityScore.multiply(BigDecimal.valueOf(0.20)))
                .add(paymentScore.multiply(BigDecimal.valueOf(0.10)))
                .add(serviceScore.multiply(BigDecimal.valueOf(0.05)));
            ranking.setTotalScore(totalScore);

            rankings.add(ranking);
        }

        // 按综合得分降序排列
        rankings.sort((a, b) -> b.getTotalScore().compareTo(a.getTotalScore()));

        // 设置排名
        for (int i = 0; i < rankings.size(); i++) {
            rankings.get(i).setRank(i + 1);
        }

        return rankings;
    }

    /**
     * 付款条件评分
     * - 月结60天:100分
     * - 月结30天:80分
     * - 货到付款:60分
     * - 预付30%:40分
     * - 预付50%:20分
     * - 全额预付:0分
     */
    private BigDecimal calculatePaymentScore(String paymentTerms) {
        Map<String, Integer> scoreMap = Map.of(
            "NET60", 100,
            "NET30", 80,
            "COD", 60,
            "30%_ADVANCE", 40,
            "50%_ADVANCE", 20,
            "100%_ADVANCE", 0
        );
        return BigDecimal.valueOf(scoreMap.getOrDefault(paymentTerms, 50));
    }
}

2.4 环节三:采购下单

采购订单状态流转

┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐
│  草稿   │────▶│ 待审批  │────▶│ 已审批  │────▶│ 已发货  │────▶│ 已完成  │
│ DRAFT  │     │ PENDING │     │ APPROVED│     │ SHIPPED │     │ COMPLETED│
└─────────┘     └─────────┘     └─────────┘     └─────────┘     └─────────┘
     │               │               │               │
     │               ▼               │               │
     │          ┌─────────┐          │               │
     │          │  已驳回  │          │               │
     │          │ REJECTED│          │               │
     │          └─────────┘          │               │
     │                               │               │
     └───────────────────────────────┴───────────────┘
                                 可取消
                              ┌─────────┐
                              │ 已取消  │
                              │ CANCELLED│
                              └─────────┘

采购订单创建服务

@Service
@Transactional
public class PurchaseOrderService {

    @Autowired
    private PurchaseOrderRepository poRepository;

    @Autowired
    private SupplierService supplierService;

    @Autowired
    private ApprovalService approvalService;

    @Autowired
    private ExchangeRateService exchangeRateService;

    /**
     * 创建采购订单
     */
    public PurchaseOrder createPurchaseOrder(PurchaseOrderCreateRequest request) {
        // 1. 生成PO编号
        String poNo = generatePoNo();

        // 2. 获取供应商信息
        Supplier supplier = supplierService.getById(request.getSupplierId());
        if (supplier == null || !"ACTIVE".equals(supplier.getStatus())) {
            throw new BusinessException("供应商不存在或已停用");
        }

        // 3. 创建采购订单主表
        PurchaseOrder po = new PurchaseOrder();
        po.setPoNo(poNo);
        po.setSupplierId(supplier.getId());
        po.setSupplierCode(supplier.getSupplierCode());
        po.setSupplierName(supplier.getSupplierName());
        po.setWarehouseId(request.getWarehouseId());
        po.setOrderDate(LocalDate.now());
        po.setExpectedDate(request.getExpectedDate());
        po.setCurrency(request.getCurrency());
        po.setPaymentTerms(request.getPaymentTerms());
        po.setStatus("DRAFT");
        po.setCreatedBy(SecurityUtils.getCurrentUser());

        // 4. 获取汇率(如果是外币采购)
        if (!"CNY".equals(request.getCurrency())) {
            BigDecimal exchangeRate = exchangeRateService
                .getRate(request.getCurrency(), "CNY", LocalDate.now());
            po.setExchangeRate(exchangeRate);
        } else {
            po.setExchangeRate(BigDecimal.ONE);
        }

        // 5. 创建采购订单明细
        List<PurchaseOrderItem> items = new ArrayList<>();
        BigDecimal totalAmount = BigDecimal.ZERO;

        for (PurchaseOrderItemRequest itemReq : request.getItems()) {
            PurchaseOrderItem item = new PurchaseOrderItem();
            item.setPoNo(poNo);
            item.setSeq(items.size() + 1);
            item.setSkuId(itemReq.getSkuId());
            item.setSkuName(itemReq.getSkuName());
            item.setQuantity(itemReq.getQuantity());
            item.setUnitPrice(itemReq.getUnitPrice());

            // 计算金额
            BigDecimal amount = itemReq.getUnitPrice()
                .multiply(BigDecimal.valueOf(itemReq.getQuantity()));
            item.setAmount(amount);
            item.setReceivedQty(0);

            items.add(item);
            totalAmount = totalAmount.add(amount);
        }

        po.setTotalAmount(totalAmount);
        po.setTotalAmountCny(totalAmount.multiply(po.getExchangeRate()));

        // 6. 保存
        poRepository.save(po);
        poRepository.saveItems(items);

        return po;
    }

    /**
     * 提交审批
     */
    public void submitForApproval(String poNo) {
        PurchaseOrder po = poRepository.findByPoNo(poNo);
        if (po == null) {
            throw new BusinessException("采购订单不存在");
        }
        if (!"DRAFT".equals(po.getStatus()) && !"REJECTED".equals(po.getStatus())) {
            throw new BusinessException("当前状态不允许提交审批");
        }

        // 更新状态
        po.setStatus("PENDING");
        poRepository.update(po);

        // 发起审批流程
        approvalService.startApproval(
            "PURCHASE_ORDER",
            poNo,
            po.getTotalAmountCny(),
            po.getCreatedBy()
        );
    }

    /**
     * 生成PO编号
     * 格式:PO + 年月日 + 4位流水号
     * 示例:PO202602040001
     */
    private String generatePoNo() {
        String dateStr = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        String prefix = "PO" + dateStr;

        // 获取当日最大流水号
        Integer maxSeq = poRepository.getMaxSeqByPrefix(prefix);
        int nextSeq = (maxSeq == null) ? 1 : maxSeq + 1;

        return prefix + String.format("%04d", nextSeq);
    }
}

2.5 环节四:到货验收

到货验收流程

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  ASN预约    │────▶│  到货登记   │────▶│  质检验收   │────▶│  差异处理   │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
       │                   │                   │                   │
       ▼                   ▼                   ▼                   ▼
   供应商提交ASN       WMS扫码收货        抽检/全检          多收/少收/破损
   预约到货时间        核对数量品名        记录质检结果        生成差异单

ASN(预到货通知)管理

@Service
public class AsnService {

    /**
     * 创建ASN(预到货通知)
     */
    public Asn createAsn(AsnCreateRequest request) {
        // 验证采购订单
        PurchaseOrder po = poRepository.findByPoNo(request.getPoNo());
        if (po == null || !"APPROVED".equals(po.getStatus())) {
            throw new BusinessException("采购订单不存在或未审批");
        }

        // 生成ASN编号
        String asnNo = generateAsnNo();

        Asn asn = new Asn();
        asn.setAsnNo(asnNo);
        asn.setPoNo(request.getPoNo());
        asn.setSupplierId(po.getSupplierId());
        asn.setWarehouseId(po.getWarehouseId());
        asn.setExpectedArrivalDate(request.getExpectedArrivalDate());
        asn.setCarrier(request.getCarrier());
        asn.setTrackingNo(request.getTrackingNo());
        asn.setStatus("PENDING");

        // 创建ASN明细
        List<AsnItem> items = new ArrayList<>();
        for (AsnItemRequest itemReq : request.getItems()) {
            // 验证是否超过PO可收货数量
            PurchaseOrderItem poItem = poRepository
                .findItemByPoNoAndSku(request.getPoNo(), itemReq.getSkuId());

            int remainingQty = poItem.getQuantity() - poItem.getReceivedQty();
            if (itemReq.getQuantity() > remainingQty) {
                throw new BusinessException(
                    String.format("SKU[%s]预到货数量[%d]超过可收货数量[%d]",
                        itemReq.getSkuId(), itemReq.getQuantity(), remainingQty)
                );
            }

            AsnItem item = new AsnItem();
            item.setAsnNo(asnNo);
            item.setSkuId(itemReq.getSkuId());
            item.setExpectedQty(itemReq.getQuantity());
            item.setReceivedQty(0);
            items.add(item);
        }

        asnRepository.save(asn);
        asnRepository.saveItems(items);

        // 通知WMS准备收货
        wmsNotifyService.notifyAsnCreated(asn, items);

        return asn;
    }
}

质检验收服务

@Service
public class QualityInspectionService {

    /**
     * 执行质检
     */
    public QualityInspectionResult inspect(QualityInspectionRequest request) {
        QualityInspectionResult result = new QualityInspectionResult();
        result.setAsnNo(request.getAsnNo());
        result.setSkuId(request.getSkuId());
        result.setInspectionDate(LocalDateTime.now());
        result.setInspector(SecurityUtils.getCurrentUser());

        // 获取质检标准
        QualityStandard standard = qualityStandardRepository
            .findBySkuId(request.getSkuId());

        // 记录质检项目
        List<InspectionItem> inspectionItems = new ArrayList<>();
        int passCount = 0;
        int failCount = 0;

        for (InspectionItemRequest itemReq : request.getItems()) {
            InspectionItem item = new InspectionItem();
            item.setItemName(itemReq.getItemName());
            item.setStandardValue(itemReq.getStandardValue());
            item.setActualValue(itemReq.getActualValue());

            // 判断是否合格
            boolean passed = evaluateInspectionItem(itemReq, standard);
            item.setPassed(passed);

            if (passed) {
                passCount++;
            } else {
                failCount++;
            }

            inspectionItems.add(item);
        }

        result.setInspectionItems(inspectionItems);
        result.setTotalItems(inspectionItems.size());
        result.setPassedItems(passCount);
        result.setFailedItems(failCount);

        // 计算合格率
        BigDecimal passRate = BigDecimal.valueOf(passCount)
            .divide(BigDecimal.valueOf(inspectionItems.size()), 4, RoundingMode.HALF_UP);
        result.setPassRate(passRate);

        // 判断整体是否合格(合格率 >= 95%)
        result.setOverallPassed(passRate.compareTo(BigDecimal.valueOf(0.95)) >= 0);

        // 保存质检结果
        qualityInspectionRepository.save(result);

        return result;
    }
}

2.6 环节五:入库结算

入库结算流程

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  WMS入库    │────▶│  ERP同步    │────▶│  成本核算   │────▶│  应付登记   │
└─────────────┘     └─────────────┘     └─────────────┘     └─────────────┘
       │                   │                   │                   │
       ▼                   ▼                   ▼                   ▼
   实物上架完成        更新账面库存        计算入库成本        生成应付凭证
   WMS发送入库回执     更新PO收货数量      移动加权平均        等待发票核对

入库结算服务

@Service
@Transactional
public class PurchaseSettlementService {

    /**
     * 处理WMS入库回执
     */
    @RocketMQMessageListener(topic = "wms-inbound-complete")
    public void onInboundComplete(InboundCompleteEvent event) {
        String asnNo = event.getAsnNo();
        String skuId = event.getSkuId();
        int receivedQty = event.getReceivedQty();

        // 1. 更新ASN状态
        Asn asn = asnRepository.findByAsnNo(asnNo);
        AsnItem asnItem = asnRepository.findItemByAsnNoAndSku(asnNo, skuId);
        asnItem.setReceivedQty(receivedQty);
        asnRepository.updateItem(asnItem);

        // 2. 更新PO收货数量
        PurchaseOrder po = poRepository.findByPoNo(asn.getPoNo());
        PurchaseOrderItem poItem = poRepository
            .findItemByPoNoAndSku(asn.getPoNo(), skuId);
        poItem.setReceivedQty(poItem.getReceivedQty() + receivedQty);
        poRepository.updateItem(poItem);

        // 检查PO是否全部收货完成
        checkAndUpdatePoStatus(po);

        // 3. 计算入库成本(移动加权平均)
        BigDecimal inboundAmount = poItem.getUnitPrice()
            .multiply(BigDecimal.valueOf(receivedQty))
            .multiply(po.getExchangeRate());  // 转换为本位币

        costCalculationService.calculateMovingAverageCost(
            skuId,
            receivedQty,
            inboundAmount
        );

        // 4. 更新ERP库存
        erpInventoryService.increaseStock(
            skuId,
            po.getWarehouseId(),
            receivedQty
        );

        // 5. 生成应付凭证
        createPayableVoucher(po, poItem, receivedQty, inboundAmount);
    }

    /**
     * 生成应付凭证
     *
     * 借:库存商品
     * 贷:应付账款
     */
    private void createPayableVoucher(PurchaseOrder po, PurchaseOrderItem item,
                                       int qty, BigDecimal amount) {
        Voucher voucher = new Voucher();
        voucher.setVoucherNo(generateVoucherNo());
        voucher.setVoucherDate(LocalDate.now());
        voucher.setPeriod(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM")));
        voucher.setVoucherType("转");
        voucher.setDescription(String.format("采购入库 PO:%s SKU:%s 数量:%d",
            po.getPoNo(), item.getSkuId(), qty));

        List<VoucherItem> voucherItems = new ArrayList<>();

        // 借方:库存商品
        VoucherItem debitItem = new VoucherItem();
        debitItem.setVoucherNo(voucher.getVoucherNo());
        debitItem.setSeq(1);
        debitItem.setAccountCode("1405");  // 库存商品科目
        debitItem.setDescription("采购入库-" + item.getSkuName());
        debitItem.setDebitAmount(amount);
        debitItem.setCreditAmount(BigDecimal.ZERO);
        voucherItems.add(debitItem);

        // 贷方:应付账款
        VoucherItem creditItem = new VoucherItem();
        creditItem.setVoucherNo(voucher.getVoucherNo());
        creditItem.setSeq(2);
        creditItem.setAccountCode("2202");  // 应付账款科目
        creditItem.setDescription("应付-" + po.getSupplierName());
        creditItem.setDebitAmount(BigDecimal.ZERO);
        creditItem.setCreditAmount(amount);
        voucherItems.add(creditItem);

        voucher.setTotalDebit(amount);
        voucher.setTotalCredit(amount);
        voucher.setStatus("DRAFT");

        voucherRepository.save(voucher);
        voucherRepository.saveItems(voucherItems);

        // 更新应付账款
        payableService.createPayable(
            po.getSupplierId(),
            po.getPoNo(),
            amount,
            po.getPaymentTerms()
        );
    }
}

三、核心功能详解

3.1 供应商管理

3.1.1 供应商档案

供应商信息维度

类别字段说明
基本信息名称、编码、类型贸易商/工厂/代理
联系信息联系人、电话、邮箱、地址多联系人支持
资质信息营业执照、税务登记、认证资质CE/FCC/FDA等
银行信息开户行、账号、Swift Code支持多币种账户
合作信息合作开始日期、主营品类、账期历史合作记录

3.1.2 供应商评级

评级维度与权重

@Service
public class SupplierRatingService {

    /**
     * 计算供应商综合评分
     *
     * 评分维度:
     * - 质量表现(30%):合格率、退货率
     * - 交期表现(25%):准时交货率
     * - 价格竞争力(20%):价格水平
     * - 服务响应(15%):响应速度、问题解决
     * - 合作稳定性(10%):合作年限、订单量
     */
    public SupplierRating calculateRating(String supplierId) {
        SupplierRating rating = new SupplierRating();
        rating.setSupplierId(supplierId);
        rating.setRatingDate(LocalDate.now());

        // 1. 质量表现(30%)
        BigDecimal qualityScore = calculateQualityScore(supplierId);
        rating.setQualityScore(qualityScore);

        // 2. 交期表现(25%)
        BigDecimal deliveryScore = calculateDeliveryScore(supplierId);
        rating.setDeliveryScore(deliveryScore);

        // 3. 价格竞争力(20%)
        BigDecimal priceScore = calculatePriceScore(supplierId);
        rating.setPriceScore(priceScore);

        // 4. 服务响应(15%)
        BigDecimal serviceScore = calculateServiceScore(supplierId);
        rating.setServiceScore(serviceScore);

        // 5. 合作稳定性(10%)
        BigDecimal stabilityScore = calculateStabilityScore(supplierId);
        rating.setStabilityScore(stabilityScore);

        // 计算综合得分
        BigDecimal totalScore = qualityScore.multiply(BigDecimal.valueOf(0.30))
            .add(deliveryScore.multiply(BigDecimal.valueOf(0.25)))
            .add(priceScore.multiply(BigDecimal.valueOf(0.20)))
            .add(serviceScore.multiply(BigDecimal.valueOf(0.15)))
            .add(stabilityScore.multiply(BigDecimal.valueOf(0.10)));
        rating.setTotalScore(totalScore);

        // 确定等级
        rating.setGrade(determineGrade(totalScore));

        return rating;
    }

    /**
     * 质量得分计算
     * 合格率 >= 99%:100分
     * 合格率 >= 98%:90分
     * 合格率 >= 95%:70分
     * 合格率 >= 90%:50分
     * 合格率 < 90%:30分
     */
    private BigDecimal calculateQualityScore(String supplierId) {
        // 获取近6个月的质检数据
        QualityStatistics stats = qualityRepository
            .getStatistics(supplierId, 6);

        BigDecimal passRate = stats.getPassRate();

        if (passRate.compareTo(BigDecimal.valueOf(0.99)) >= 0) {
            return BigDecimal.valueOf(100);
        } else if (passRate.compareTo(BigDecimal.valueOf(0.98)) >= 0) {
            return BigDecimal.valueOf(90);
        } else if (passRate.compareTo(BigDecimal.valueOf(0.95)) >= 0) {
            return BigDecimal.valueOf(70);
        } else if (passRate.compareTo(BigDecimal.valueOf(0.90)) >= 0) {
            return BigDecimal.valueOf(50);
        } else {
            return BigDecimal.valueOf(30);
        }
    }

    /**
     * 确定供应商等级
     * A级:90-100分(优选供应商)
     * B级:80-89分(合格供应商)
     * C级:60-79分(观察供应商)
     * D级:60分以下(淘汰供应商)
     */
    private String determineGrade(BigDecimal score) {
        if (score.compareTo(BigDecimal.valueOf(90)) >= 0) {
            return "A";
        } else if (score.compareTo(BigDecimal.valueOf(80)) >= 0) {
            return "B";
        } else if (score.compareTo(BigDecimal.valueOf(60)) >= 0) {
            return "C";
        } else {
            return "D";
        }
    }
}

3.1.3 价格协议管理

价格协议类型

类型说明适用场景
固定价格约定期限内价格不变价格稳定的标品
阶梯价格按采购量分档定价大批量采购
浮动价格随原材料价格浮动原材料类商品
框架协议约定年度采购量和价格长期合作供应商
@Service
public class PriceAgreementService {

    /**
     * 获取SKU的有效采购价格
     */
    public BigDecimal getEffectivePrice(String skuId, String supplierId, int quantity) {
        // 1. 查找有效的价格协议
        List<PriceAgreement> agreements = priceAgreementRepository
            .findValidAgreements(skuId, supplierId, LocalDate.now());

        if (agreements.isEmpty()) {
            // 没有协议,返回供应商最近报价
            return getLatestQuotationPrice(skuId, supplierId);
        }

        // 2. 按优先级选择协议
        PriceAgreement agreement = selectBestAgreement(agreements, quantity);

        // 3. 根据协议类型计算价格
        switch (agreement.getPriceType()) {
            case "FIXED":
                return agreement.getUnitPrice();

            case "TIERED":
                return calculateTieredPrice(agreement, quantity);

            case "FLOATING":
                return calculateFloatingPrice(agreement);

            default:
                return agreement.getUnitPrice();
        }
    }

    /**
     * 计算阶梯价格
     */
    private BigDecimal calculateTieredPrice(PriceAgreement agreement, int quantity) {
        List<PriceTier> tiers = agreement.getPriceTiers();

        // 按数量降序排列,找到适用的价格档
        tiers.sort((a, b) -> b.getMinQty() - a.getMinQty());

        for (PriceTier tier : tiers) {
            if (quantity >= tier.getMinQty()) {
                return tier.getUnitPrice();
            }
        }

        // 默认返回最高档价格
        return tiers.get(tiers.size() - 1).getUnitPrice();
    }
}

3.2 采购计划

采购计划生成策略

@Service
public class PurchasePlanService {

    /**
     * 生成周采购计划
     */
    @Scheduled(cron = "0 0 8 * * MON")  // 每周一早8点执行
    public void generateWeeklyPlan() {
        log.info("开始生成周采购计划...");

        // 1. 获取所有需要补货的SKU
        List<PurchaseSuggestion> suggestions = purchaseSuggestionService
            .generateAllSuggestions();

        // 2. 按供应商分组
        Map<String, List<PurchaseSuggestion>> bySupplier = suggestions.stream()
            .collect(Collectors.groupingBy(PurchaseSuggestion::getPreferredSupplierId));

        // 3. 生成采购计划
        List<PurchasePlan> plans = new ArrayList<>();

        for (Map.Entry<String, List<PurchaseSuggestion>> entry : bySupplier.entrySet()) {
            String supplierId = entry.getKey();
            List<PurchaseSuggestion> items = entry.getValue();

            PurchasePlan plan = new PurchasePlan();
            plan.setPlanNo(generatePlanNo());
            plan.setSupplierId(supplierId);
            plan.setPlanDate(LocalDate.now());
            plan.setStatus("DRAFT");

            // 计算计划总金额
            BigDecimal totalAmount = BigDecimal.ZERO;
            List<PurchasePlanItem> planItems = new ArrayList<>();

            for (PurchaseSuggestion suggestion : items) {
                PurchasePlanItem planItem = new PurchasePlanItem();
                planItem.setPlanNo(plan.getPlanNo());
                planItem.setSkuId(suggestion.getSkuId());
                planItem.setSuggestQty(suggestion.getSuggestQty());
                planItem.setConfirmedQty(suggestion.getSuggestQty());  // 默认确认建议数量

                // 获取价格
                BigDecimal price = priceAgreementService
                    .getEffectivePrice(suggestion.getSkuId(), supplierId, suggestion.getSuggestQty());
                planItem.setUnitPrice(price);
                planItem.setAmount(price.multiply(BigDecimal.valueOf(suggestion.getSuggestQty())));

                planItems.add(planItem);
                totalAmount = totalAmount.add(planItem.getAmount());
            }

            plan.setTotalAmount(totalAmount);
            plan.setItems(planItems);
            plans.add(plan);
        }

        // 4. 保存采购计划
        purchasePlanRepository.saveAll(plans);

        // 5. 发送通知给采购人员
        notifyPurchasers(plans);

        log.info("周采购计划生成完成,共{}个计划", plans.size());
    }
}

四、数据库设计

4.1 供应商相关表

-- 供应商主表
CREATE TABLE t_supplier (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    supplier_code VARCHAR(32) NOT NULL COMMENT '供应商编码',
    supplier_name VARCHAR(128) NOT NULL COMMENT '供应商名称',
    supplier_type VARCHAR(16) COMMENT '类型:FACTORY/TRADER/AGENT',
    country VARCHAR(32) COMMENT '国家',
    province VARCHAR(32) COMMENT '省份',
    city VARCHAR(32) COMMENT '城市',
    address VARCHAR(256) COMMENT '详细地址',
    contact_name VARCHAR(64) COMMENT '联系人',
    contact_phone VARCHAR(32) COMMENT '联系电话',
    contact_email VARCHAR(128) COMMENT '联系邮箱',
    payment_terms VARCHAR(64) COMMENT '付款条件:NET30/NET60/COD',
    currency VARCHAR(8) DEFAULT 'CNY' COMMENT '结算币种',
    tax_rate DECIMAL(5,2) COMMENT '税率',
    rating VARCHAR(8) DEFAULT 'B' COMMENT '评级:A/B/C/D',
    status VARCHAR(16) DEFAULT 'ACTIVE' COMMENT '状态',
    cooperation_start_date DATE COMMENT '合作开始日期',
    remark VARCHAR(512) COMMENT '备注',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_supplier_code (supplier_code),
    KEY idx_status (status),
    KEY idx_rating (rating)
) COMMENT '供应商主表';

-- 供应商银行账户表
CREATE TABLE t_supplier_bank_account (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    supplier_id BIGINT NOT NULL COMMENT '供应商ID',
    account_name VARCHAR(128) NOT NULL COMMENT '账户名称',
    bank_name VARCHAR(128) NOT NULL COMMENT '银行名称',
    bank_branch VARCHAR(128) COMMENT '支行名称',
    account_no VARCHAR(64) NOT NULL COMMENT '账号',
    currency VARCHAR(8) DEFAULT 'CNY' COMMENT '币种',
    swift_code VARCHAR(32) COMMENT 'SWIFT代码(国际汇款)',
    is_default TINYINT DEFAULT 0 COMMENT '是否默认账户',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    KEY idx_supplier_id (supplier_id)
) COMMENT '供应商银行账户表';

-- 供应商评级记录表
CREATE TABLE t_supplier_rating_record (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    supplier_id BIGINT NOT NULL COMMENT '供应商ID',
    rating_date DATE NOT NULL COMMENT '评级日期',
    quality_score DECIMAL(5,2) COMMENT '质量得分',
    delivery_score DECIMAL(5,2) COMMENT '交期得分',
    price_score DECIMAL(5,2) COMMENT '价格得分',
    service_score DECIMAL(5,2) COMMENT '服务得分',
    stability_score DECIMAL(5,2) COMMENT '稳定性得分',
    total_score DECIMAL(5,2) COMMENT '综合得分',
    grade VARCHAR(8) COMMENT '等级',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    KEY idx_supplier_date (supplier_id, rating_date)
) COMMENT '供应商评级记录表';

4.2 价格协议表

-- 价格协议主表
CREATE TABLE t_price_agreement (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    agreement_no VARCHAR(32) NOT NULL COMMENT '协议编号',
    supplier_id BIGINT NOT NULL COMMENT '供应商ID',
    sku_id VARCHAR(32) NOT NULL COMMENT 'SKU编码',
    price_type VARCHAR(16) NOT NULL COMMENT '价格类型:FIXED/TIERED/FLOATING',
    unit_price DECIMAL(12,4) COMMENT '单价(固定价格时使用)',
    currency VARCHAR(8) DEFAULT 'CNY' COMMENT '币种',
    effective_date DATE NOT NULL COMMENT '生效日期',
    expiry_date DATE NOT NULL COMMENT '失效日期',
    min_order_qty INT COMMENT '最小起订量',
    status VARCHAR(16) DEFAULT 'ACTIVE' COMMENT '状态',
    remark VARCHAR(256) COMMENT '备注',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_agreement_no (agreement_no),
    KEY idx_supplier_sku (supplier_id, sku_id),
    KEY idx_effective_date (effective_date, expiry_date)
) COMMENT '价格协议主表';

-- 阶梯价格表
CREATE TABLE t_price_tier (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    agreement_id BIGINT NOT NULL COMMENT '协议ID',
    min_qty INT NOT NULL COMMENT '最小数量',
    max_qty INT COMMENT '最大数量',
    unit_price DECIMAL(12,4) NOT NULL COMMENT '单价',
    KEY idx_agreement_id (agreement_id)
) COMMENT '阶梯价格表';

4.3 采购订单表

-- 采购订单主表
CREATE TABLE t_purchase_order (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    po_no VARCHAR(32) NOT NULL COMMENT 'PO编号',
    supplier_id BIGINT NOT NULL COMMENT '供应商ID',
    supplier_code VARCHAR(32) COMMENT '供应商编码',
    supplier_name VARCHAR(128) COMMENT '供应商名称',
    warehouse_id VARCHAR(32) COMMENT '目标仓库',
    order_date DATE NOT NULL COMMENT '下单日期',
    expected_date DATE COMMENT '预计到货日期',
    currency VARCHAR(8) DEFAULT 'CNY' COMMENT '币种',
    exchange_rate DECIMAL(10,6) DEFAULT 1 COMMENT '汇率',
    total_amount DECIMAL(16,2) COMMENT '订单总金额(原币)',
    total_amount_cny DECIMAL(16,2) COMMENT '订单总金额(本位币)',
    payment_terms VARCHAR(64) COMMENT '付款条件',
    shipping_method VARCHAR(32) COMMENT '运输方式:SEA/AIR/EXPRESS',
    status VARCHAR(16) DEFAULT 'DRAFT' COMMENT '状态',
    approved_by VARCHAR(64) COMMENT '审批人',
    approved_at DATETIME COMMENT '审批时间',
    remark VARCHAR(512) COMMENT '备注',
    created_by VARCHAR(64) COMMENT '创建人',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uk_po_no (po_no),
    KEY idx_supplier_id (supplier_id),
    KEY idx_status (status),
    KEY idx_order_date (order_date)
) COMMENT '采购订单主表';

-- 采购订单明细表
CREATE TABLE t_purchase_order_item (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    po_no VARCHAR(32) NOT NULL COMMENT 'PO编号',
    seq INT NOT NULL COMMENT '行号',
    sku_id VARCHAR(32) NOT NULL COMMENT 'SKU编码',
    sku_name VARCHAR(256) COMMENT 'SKU名称',
    quantity INT NOT NULL COMMENT '采购数量',
    unit_price DECIMAL(12,4) COMMENT '单价',
    amount DECIMAL(16,2) COMMENT '金额',
    received_qty INT DEFAULT 0 COMMENT '已收货数量',
    tax_rate DECIMAL(5,2) COMMENT '税率',
    remark VARCHAR(256) COMMENT '备注',
    KEY idx_po_no (po_no),
    KEY idx_sku_id (sku_id)
) COMMENT '采购订单明细表';

4.4 ASN和收货表

-- ASN预到货通知表
CREATE TABLE t_asn (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    asn_no VARCHAR(32) NOT NULL COMMENT 'ASN编号',
    po_no VARCHAR(32) NOT NULL COMMENT '关联PO编号',
    supplier_id BIGINT NOT NULL COMMENT '供应商ID',
    warehouse_id VARCHAR(32) NOT NULL COMMENT '目标仓库',
    expected_arrival_date DATE COMMENT '预计到货日期',
    actual_arrival_date DATE COMMENT '实际到货日期',
    carrier VARCHAR(64) COMMENT '承运商',
    tracking_no VARCHAR(64) COMMENT '物流单号',
    status VARCHAR(16) DEFAULT 'PENDING' COMMENT '状态',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_asn_no (asn_no),
    KEY idx_po_no (po_no),
    KEY idx_status (status)
) COMMENT 'ASN预到货通知表';

-- ASN明细表
CREATE TABLE t_asn_item (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    asn_no VARCHAR(32) NOT NULL COMMENT 'ASN编号',
    sku_id VARCHAR(32) NOT NULL COMMENT 'SKU编码',
    expected_qty INT NOT NULL COMMENT '预计数量',
    received_qty INT DEFAULT 0 COMMENT '实收数量',
    damaged_qty INT DEFAULT 0 COMMENT '破损数量',
    KEY idx_asn_no (asn_no)
) COMMENT 'ASN明细表';

-- 收货差异表
CREATE TABLE t_receiving_variance (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    variance_no VARCHAR(32) NOT NULL COMMENT '差异单号',
    asn_no VARCHAR(32) NOT NULL COMMENT 'ASN编号',
    sku_id VARCHAR(32) NOT NULL COMMENT 'SKU编码',
    expected_qty INT NOT NULL COMMENT '预计数量',
    actual_qty INT NOT NULL COMMENT '实际数量',
    variance_qty INT NOT NULL COMMENT '差异数量',
    variance_type VARCHAR(16) COMMENT '差异类型:OVER/SHORT/DAMAGED',
    status VARCHAR(16) DEFAULT 'PENDING' COMMENT '处理状态',
    handle_result VARCHAR(256) COMMENT '处理结果',
    handled_by VARCHAR(64) COMMENT '处理人',
    handled_at DATETIME COMMENT '处理时间',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_variance_no (variance_no),
    KEY idx_asn_no (asn_no)
) COMMENT '收货差异表';

五、业务规则设计

5.1 审批流程配置

审批规则引擎

@Service
public class ApprovalRuleEngine {

    /**
     * 获取采购订单审批流程
     *
     * 审批规则:
     * - 金额 <= 1万:采购主管审批
     * - 金额 <= 10万:采购经理审批
     * - 金额 <= 50万:采购总监审批
     * - 金额 > 50万:总经理审批
     */
    public List<ApprovalNode> getApprovalFlow(String businessType, BigDecimal amount) {
        List<ApprovalRule> rules = approvalRuleRepository
            .findByBusinessType(businessType);

        List<ApprovalNode> nodes = new ArrayList<>();

        for (ApprovalRule rule : rules) {
            if (amount.compareTo(rule.getMinAmount()) >= 0 &&
                amount.compareTo(rule.getMaxAmount()) < 0) {

                ApprovalNode node = new ApprovalNode();
                node.setNodeName(rule.getNodeName());
                node.setApproverRole(rule.getApproverRole());
                node.setApproverIds(getApproversByRole(rule.getApproverRole()));
                node.setSeq(rule.getSeq());
                nodes.add(node);
            }
        }

        // 按序号排序
        nodes.sort(Comparator.comparingInt(ApprovalNode::getSeq));

        return nodes;
    }

    /**
     * 处理审批
     */
    @Transactional
    public void processApproval(String approvalId, String action, String comment) {
        ApprovalInstance instance = approvalRepository.findById(approvalId);

        if ("APPROVE".equals(action)) {
            // 检查是否还有下一个审批节点
            ApprovalNode nextNode = getNextNode(instance);

            if (nextNode != null) {
                // 流转到下一节点
                instance.setCurrentNode(nextNode.getNodeName());
                instance.setCurrentApprovers(nextNode.getApproverIds());
            } else {
                // 审批完成
                instance.setStatus("APPROVED");
                instance.setCompletedAt(LocalDateTime.now());

                // 回调业务系统
                callbackBusinessSystem(instance, "APPROVED");
            }
        } else if ("REJECT".equals(action)) {
            instance.setStatus("REJECTED");
            instance.setCompletedAt(LocalDateTime.now());
            instance.setRejectReason(comment);

            // 回调业务系统
            callbackBusinessSystem(instance, "REJECTED");
        }

        // 记录审批日志
        saveApprovalLog(instance, action, comment);

        approvalRepository.update(instance);
    }
}

5.2 价格管控规则

@Service
public class PriceControlService {

    /**
     * 采购价格校验
     *
     * 规则:
     * 1. 不能高于历史最高价的110%
     * 2. 不能高于市场参考价的105%
     * 3. 超出需要特殊审批
     */
    public PriceCheckResult checkPrice(String skuId, String supplierId, BigDecimal price) {
        PriceCheckResult result = new PriceCheckResult();
        result.setSkuId(skuId);
        result.setInputPrice(price);

        // 1. 获取历史最高价
        BigDecimal historyMaxPrice = getHistoryMaxPrice(skuId, supplierId);
        BigDecimal historyThreshold = historyMaxPrice.multiply(BigDecimal.valueOf(1.10));

        if (price.compareTo(historyThreshold) > 0) {
            result.setExceedHistory(true);
            result.setHistoryMaxPrice(historyMaxPrice);
            result.setHistoryExceedRate(
                price.subtract(historyMaxPrice)
                    .divide(historyMaxPrice, 4, RoundingMode.HALF_UP)
            );
        }

        // 2. 获取市场参考价
        BigDecimal marketPrice = getMarketReferencePrice(skuId);
        if (marketPrice != null) {
            BigDecimal marketThreshold = marketPrice.multiply(BigDecimal.valueOf(1.05));

            if (price.compareTo(marketThreshold) > 0) {
                result.setExceedMarket(true);
                result.setMarketPrice(marketPrice);
                result.setMarketExceedRate(
                    price.subtract(marketPrice)
                        .divide(marketPrice, 4, RoundingMode.HALF_UP)
                );
            }
        }

        // 3. 判断是否需要特殊审批
        result.setNeedSpecialApproval(result.isExceedHistory() || result.isExceedMarket());

        return result;
    }
}

5.3 交期管理规则

@Service
public class DeliveryManagementService {

    /**
     * 交期预警检查
     *
     * 预警规则:
     * - 红色预警:已超期
     * - 橙色预警:3天内到期
     * - 黄色预警:7天内到期
     */
    @Scheduled(cron = "0 0 9 * * *")  // 每天早9点执行
    public void checkDeliveryWarning() {
        LocalDate today = LocalDate.now();

        // 获取所有未完成的采购订单
        List<PurchaseOrder> pendingOrders = poRepository
            .findByStatusIn(Arrays.asList("APPROVED", "SHIPPED"));

        List<DeliveryWarning> warnings = new ArrayList<>();

        for (PurchaseOrder po : pendingOrders) {
            LocalDate expectedDate = po.getExpectedDate();
            if (expectedDate == null) continue;

            long daysUntilDue = ChronoUnit.DAYS.between(today, expectedDate);

            DeliveryWarning warning = new DeliveryWarning();
            warning.setPoNo(po.getPoNo());
            warning.setSupplierId(po.getSupplierId());
            warning.setSupplierName(po.getSupplierName());
            warning.setExpectedDate(expectedDate);
            warning.setDaysUntilDue((int) daysUntilDue);

            if (daysUntilDue < 0) {
                warning.setWarningLevel("RED");
                warning.setMessage("已超期" + Math.abs(daysUntilDue) + "天");
            } else if (daysUntilDue <= 3) {
                warning.setWarningLevel("ORANGE");
                warning.setMessage("即将到期,剩余" + daysUntilDue + "天");
            } else if (daysUntilDue <= 7) {
                warning.setWarningLevel("YELLOW");
                warning.setMessage("临近到期,剩余" + daysUntilDue + "天");
            } else {
                continue;  // 不需要预警
            }

            warnings.add(warning);
        }

        // 发送预警通知
        if (!warnings.isEmpty()) {
            sendWarningNotifications(warnings);
        }
    }

    /**
     * 更新供应商交期表现
     */
    public void updateDeliveryPerformance(String poNo, LocalDate actualDate) {
        PurchaseOrder po = poRepository.findByPoNo(poNo);
        LocalDate expectedDate = po.getExpectedDate();

        // 计算交期偏差
        long variance = ChronoUnit.DAYS.between(expectedDate, actualDate);

        // 记录交期表现
        DeliveryPerformance performance = new DeliveryPerformance();
        performance.setSupplierId(po.getSupplierId());
        performance.setPoNo(poNo);
        performance.setExpectedDate(expectedDate);
        performance.setActualDate(actualDate);
        performance.setVarianceDays((int) variance);
        performance.setOnTime(variance <= 0);

        deliveryPerformanceRepository.save(performance);

        // 更新供应商准时交货率
        updateSupplierOnTimeRate(po.getSupplierId());
    }
}

六、实战案例

6.1 完整采购流程演示

场景:某跨境电商公司需要从1688供应商采购一批蓝牙耳机,用于亚马逊美国站销售。

Step 1:需求计划生成

系统自动检测到:
- SKU: BT-EARPHONE-001
- 当前库存:500件
- 安全库存:800件
- 日均销量:50件
- 采购周期:14天(含生产+物流)
- 目标库存:800 + 50×14 = 1500件
- 建议采购量:1500 - 500 = 1000件

系统生成采购建议,紧急程度:HIGH(库存低于安全库存)

Step 2:询价比价

向3家供应商发送询价:
- 供应商A(工厂):$8.5/件,交期15天,MOQ 500
- 供应商B(贸易商):$9.0/件,交期10天,MOQ 200
- 供应商C(工厂):$8.2/件,交期20天,MOQ 1000

比价结果(综合评分):
1. 供应商A:85分(价格适中,交期合适)
2. 供应商C:78分(价格最低,但交期太长)
3. 供应商B:72分(价格最高,但交期最短)

系统推荐:供应商A

Step 3:创建采购订单

PO编号:PO202602040001
供应商:供应商A
币种:USD
汇率:7.20

明细:
- BT-EARPHONE-001 × 1000件 × $8.5 = $8,500
- 运费:$200
- 总金额:$8,700(折合人民币:¥62,640)

付款条件:30%预付,70%发货前付清
预计到货:2026-02-18

Step 4:审批流程

金额:¥62,640(属于1万-10万区间)
审批流程:采购主管 → 采购经理

审批记录:
- 2026-02-04 10:30 采购主管张三 审批通过
- 2026-02-04 14:20 采购经理李四 审批通过

PO状态变更:DRAFT → PENDING → APPROVED

Step 5:付款申请

预付款申请:
- 金额:$8,700 × 30% = $2,610
- 付款方式:T/T
- 收款账户:供应商A美元账户

财务审批后付款,记录:
借:预付账款 ¥18,792
贷:银行存款 ¥18,792

Step 6:到货验收

2026-02-17 供应商发货,创建ASN:
- ASN编号:ASN202602170001
- 物流单号:SF1234567890
- 预计到货:2026-02-18

2026-02-18 仓库收货:
- 预计数量:1000件
- 实收数量:998件
- 破损数量:2件

质检结果:
- 抽检50件,合格49件
- 合格率:98%
- 判定:合格

差异处理:
- 短少2件,生成差异单
- 与供应商协商,下次补发

Step 7:入库结算

入库完成,系统自动处理:

1. 更新库存:
   - 原库存:500件
   - 入库:998件
   - 新库存:1498件

2. 成本核算(移动加权平均):
   - 原库存成本:500 × ¥60 = ¥30,000
   - 入库成本:998 × ¥61.2 = ¥61,077.6
   - 新单位成本:(30,000 + 61,077.6) / 1498 = ¥60.82

3. 生成凭证:
   借:库存商品 ¥61,077.6
   贷:应付账款 ¥61,077.6

4. 更新应付账款:
   - 已付预付款:¥18,792
   - 待付尾款:¥61,077.6 - ¥18,792 = ¥42,285.6

6.2 踩坑经验分享

踩坑1:汇率波动导致成本失控

问题:采购时汇率7.0,付款时汇率涨到7.3,导致成本增加4.3%。

解决方案

// 大额采购时锁定汇率
public void lockExchangeRate(String poNo, BigDecimal lockedRate) {
    PurchaseOrder po = poRepository.findByPoNo(poNo);

    // 记录锁定汇率
    po.setLockedRate(lockedRate);
    po.setRateLockDate(LocalDate.now());

    // 与银行签订远期结汇协议
    forexService.createForwardContract(
        po.getCurrency(),
        po.getTotalAmount(),
        lockedRate,
        po.getExpectedDate()
    );

    poRepository.update(po);
}

经验:金额超过10万美元的采购,建议锁定汇率。

踩坑2:供应商交期延误影响销售

问题:供应商承诺15天交货,实际25天才到,导致断货10天,损失销售额约5万美元。

解决方案

// 建立供应商交期信用体系
public int getAdjustedLeadTime(String supplierId, int promisedLeadTime) {
    // 获取供应商历史交期表现
    DeliveryStatistics stats = deliveryRepository.getStatistics(supplierId);

    // 计算平均延误天数
    int avgDelay = stats.getAvgDelayDays();

    // 调整后的交期 = 承诺交期 + 历史平均延误 + 安全缓冲
    int safetyBuffer = 3;  // 3天安全缓冲
    return promisedLeadTime + avgDelay + safetyBuffer;
}

经验:不要完全相信供应商承诺的交期,要根据历史数据调整。

踩坑3:质量问题导致大批退货

问题:一批货到货后发现质量问题,退货率高达15%,严重影响销售评分。

解决方案

// 建立供应商质量预警机制
public void checkSupplierQualityTrend(String supplierId) {
    // 获取近3个月质量数据
    List<QualityRecord> records = qualityRepository
        .findBySupplierId(supplierId, 3);

    // 计算质量趋势
    BigDecimal currentPassRate = records.get(0).getPassRate();
    BigDecimal avgPassRate = calculateAvgPassRate(records);

    // 如果当前合格率低于平均值5%以上,发出预警
    if (currentPassRate.compareTo(avgPassRate.subtract(BigDecimal.valueOf(0.05))) < 0) {
        sendQualityWarning(supplierId, currentPassRate, avgPassRate);

        // 自动调整该供应商的质检策略为全检
        updateInspectionStrategy(supplierId, "FULL_INSPECTION");
    }
}

经验:建立质量趋势监控,发现问题苗头及时干预。

踩坑4:MOQ限制导致库存积压

问题:供应商MOQ是1000件,但实际只需要500件,导致多采购500件积压。

解决方案

// 智能采购建议,考虑MOQ和库存周转
public PurchaseSuggestion generateSmartSuggestion(String skuId) {
    int suggestQty = calculateBaseSuggestion(skuId);
    int moq = getSupplierMoq(skuId);

    if (suggestQty < moq) {
        // 计算如果按MOQ采购,库存周转天数
        int avgDailySales = getAvgDailySales(skuId);
        int turnoverDays = moq / avgDailySales;

        if (turnoverDays > 90) {
            // 周转超过90天,建议寻找其他供应商或暂不采购
            return PurchaseSuggestion.builder()
                .skuId(skuId)
                .suggestQty(0)
                .reason("MOQ过高,建议寻找其他供应商")
                .alternativeSuppliers(findAlternativeSuppliers(skuId, suggestQty))
                .build();
        }
    }

    return PurchaseSuggestion.builder()
        .skuId(skuId)
        .suggestQty(Math.max(suggestQty, moq))
        .build();
}

经验:采购前评估MOQ对库存周转的影响,必要时寻找替代供应商。


七、总结

7.1 核心要点回顾

模块核心功能关键设计
需求计划采购建议生成安全库存+销售预测+在途库存
询价比价综合评分比价价格40%+交期25%+质量20%+付款10%+服务5%
采购下单PO创建与审批多币种支持+审批流程引擎
到货验收ASN+质检预到货通知+差异处理
入库结算成本核算+应付移动加权平均+自动生成凭证

7.2 跨境电商采购的特殊考量

  1. 多币种管理:支持USD、EUR等多币种采购,自动汇率转换
  2. 海外供应商:考虑时差、语言、付款方式差异
  3. 国际物流:海运/空运选择,清关时效预估
  4. 合规要求:CE/FCC/FDA等认证资质管理

7.3 实施建议

  1. 分阶段实施

    • 第一阶段:供应商管理+采购订单(2-3周)
    • 第二阶段:询价比价+审批流程(2周)
    • 第三阶段:到货验收+入库结算(2周)
  2. 数据迁移

    • 供应商档案从Excel导入
    • 历史采购数据用于初始化供应商评级
  3. 系统集成

    • 与WMS对接:ASN推送、入库回执
    • 与财务系统对接:凭证同步

系列文章导航

本文是《跨境电商数字化转型指南》系列的第11篇

ERP自研篇