Series Article

Day03 · PMS 核心业务面试准备

一、核心业务流程(面试必讲)

1. 采购完整流程

流程概述:

采购需求产生 → 创建采购申请 → 审批通过 → 发起询价 → 供应商报价 → 
比价选择 → 创建采购订单 → 供应商发货 → 仓库收货 → 质检入库 → 
生成应付账款 → 账期到期付款

关键环节详解:

环节1:采购需求产生(3种来源)

  • 库存预警触发:库存低于安全库存,系统自动生成采购建议
  • 销售预测触发:根据历史销量预测未来需求,提前备货
  • 人工手动申请:采购专员根据业务需要手动创建

环节2:询价比价

  • 向多家供应商发送询价单(同一个SKU可以询3-5家)
  • 供应商在Portal填写报价(单价、交货期、MOQ)
  • 系统自动计算综合得分:价格40% + 交货期30% + 供应商评级30%
  • 采购专员根据综合得分选择最优供应商

环节3:采购订单状态流转(8种状态)

草稿(0) → 待确认(1) → 已确认(2) → 部分收货(3) → 
已收货(4) → 部分付款(5) → 已付款(6) → 已结清(7)

环节4:收货入库(关键技术点)

  • WMS创建收货单,质检合格后确认入库
  • 分布式事务保证:采购订单状态更新 + WMS库存增加 + 在途库存减少
  • 支持部分收货:一个采购订单可以分多次收货

环节5:应付账款管理

  • 收货后自动生成应付账款
  • 根据供应商账期计算到期日期:due_date = 收货日期 + payment_days
  • 定时任务每天检查到期应付账款,提前3天发送提醒

面试时怎么讲: “采购流程从需求产生开始,有三种来源:库存预警、销售预测、人工申请。创建采购申请后,向多家供应商发询价,系统自动计算综合得分,选择最优供应商。创建采购订单后,供应商发货,仓库收货质检入库。收货时用分布式事务保证采购订单状态、WMS库存、在途库存同时更新。收货后自动生成应付账款,根据账期计算到期日期,定时任务提前提醒付款。“


二、核心技术方案(简历亮点)

1. 采购订单状态机(8状态流转)

为什么需要8种状态:

  • 草稿(0):初始创建,可以修改
  • 待确认(1):已提交,等待供应商确认
  • 已确认(2):供应商已确认,准备发货
  • 部分收货(3):已收到部分货物
  • 已收货(4):全部货物已收到
  • 部分付款(5):已支付部分款项
  • 已付款(6):全部款项已支付
  • 已结清(7):订单完结

状态流转白名单:

private static final Set<Transition> ALLOWED_TRANSITIONS = Set.of(
    transition(DRAFT, PENDING_CONFIRM),        // 草稿 → 待确认
    transition(PENDING_CONFIRM, CONFIRMED),    // 待确认 → 已确认
    transition(CONFIRMED, PARTIAL_RECEIVED),   // 已确认 → 部分收货
    transition(CONFIRMED, RECEIVED),           // 已确认 → 已收货(一次性收完)
    transition(PARTIAL_RECEIVED, RECEIVED),    // 部分收货 → 已收货
    transition(RECEIVED, PARTIAL_PAID),        // 已收货 → 部分付款
    transition(RECEIVED, PAID),                // 已收货 → 已付款(一次性付完)
    transition(PARTIAL_PAID, PAID),            // 部分付款 → 已付款
    transition(PAID, SETTLED)                  // 已付款 → 已结清
);

简历怎么写: “设计采购订单状态机,定义8种状态和9种合法流转路径。用状态机白名单校验流转合法性,用CAS乐观锁防止并发修改。支持部分收货和部分付款,灵活应对实际业务场景。“


2. 询价比价算法

业务场景: 同一个SKU向3家供应商询价,如何选择最优供应商?

综合评分算法:

综合得分 = 价格得分(40%) + 交货期得分(30%) + 供应商评级得分(30%)

价格得分 = (最低报价 / 当前报价) × 40
交货期得分 = (最短交货期 / 当前交货期) × 30
供应商评级得分 = (供应商评分 / 100) × 30

示例:

供应商A:报价38元,交货期7天,评级S(95分)
  价格得分 = (36/38) × 40 = 37.89
  交货期得分 = (5/7) × 30 = 21.43
  评级得分 = (95/100) × 30 = 28.5
  综合得分 = 87.82

供应商B:报价36元,交货期10天,评级A(80分)
  价格得分 = (36/36) × 40 = 40
  交货期得分 = (5/10) × 30 = 15
  评级得分 = (80/100) × 30 = 24
  综合得分 = 79

供应商C:报价40元,交货期5天,评级S(90分)
  价格得分 = (36/40) × 40 = 36
  交货期得分 = (5/5) × 30 = 30
  评级得分 = (90/100) × 30 = 27
  综合得分 = 93

结论:选择供应商C(综合得分最高)

代码实现:

public List<InquiryScoreDTO> calculateScores(List<PurchaseInquiry> inquiries) {
    // 1. 找出最低报价和最短交货期
    BigDecimal minPrice = inquiries.stream()
        .map(PurchaseInquiry::getTotalQuoteAmt)
        .min(BigDecimal::compareTo)
        .orElse(BigDecimal.ZERO);
    
    int minDeliveryDays = inquiries.stream()
        .mapToInt(PurchaseInquiry::getDeliveryDays)
        .min()
        .orElse(1);
    
    // 2. 计算每家供应商的综合得分
    return inquiries.stream().map(inquiry -> {
        // 价格得分
        BigDecimal priceScore = minPrice
            .divide(inquiry.getTotalQuoteAmt(), 4, RoundingMode.HALF_UP)
            .multiply(BigDecimal.valueOf(40));
        
        // 交货期得分
        BigDecimal deliveryScore = BigDecimal.valueOf(minDeliveryDays)
            .divide(BigDecimal.valueOf(inquiry.getDeliveryDays()), 4, RoundingMode.HALF_UP)
            .multiply(BigDecimal.valueOf(30));
        
        // 供应商评级得分
        BigDecimal gradeScore = inquiry.getSupplierScore()
            .divide(BigDecimal.valueOf(100), 4, RoundingMode.HALF_UP)
            .multiply(BigDecimal.valueOf(30));
        
        // 综合得分
        BigDecimal totalScore = priceScore.add(deliveryScore).add(gradeScore);
        
        return new InquiryScoreDTO(inquiry, priceScore, deliveryScore, gradeScore, totalScore);
    })
    .sorted(Comparator.comparing(InquiryScoreDTO::getTotalScore).reversed())
    .collect(Collectors.toList());
}

简历怎么写: “设计询价比价算法,综合考虑价格、交货期、供应商评级三个维度。价格权重40%,交货期30%,供应商评级30%。系统自动计算综合得分,推荐最优供应商。避免了单纯比价导致的质量风险,实现了综合最优。“


3. 采购入库分布式事务

业务场景: 仓库收货确认入库时,需要同时更新三个系统的数据:

  1. PMS:采购订单状态更新为”已收货”
  2. WMS:库存数量增加
  3. PMS:在途库存减少

这三个操作必须同时成功或同时失败,否则会出现数据不一致。

技术方案:Seata AT模式

@GlobalTransactional(rollbackFor = Exception.class)
public void confirmReceipt(Long receiptId) {
    // 1. 查询收货单
    PurchaseReceipt receipt = receiptMapper.selectById(receiptId);
    
    // 2. 更新采购订单状态(PMS本地事务)
    updatePurchaseOrderStatus(receipt.getPoId());
    
    // 3. 调用WMS增加库存(远程调用)
    wmsInventoryService.increaseInventory(receipt.getItems());
    
    // 4. 减少在途库存(PMS本地事务)
    decreaseInTransitInventory(receipt.getItems());
    
    // 5. 生成应付账款
    generatePayable(receipt.getPoId());
}

Seata工作原理:

  1. TM(事务管理器)开启全局事务,生成全局事务ID(XID)
  2. PMS更新订单状态,Seata记录undo_log(回滚日志)
  3. WMS增加库存,Seata记录undo_log
  4. PMS减少在途库存,Seata记录undo_log
  5. 如果全部成功,TM提交全局事务,删除undo_log
  6. 如果任何一步失败,TM回滚全局事务,根据undo_log恢复数据

为什么不用本地事务:

  • 本地事务只能保证单个数据库的一致性
  • PMS和WMS是两个独立的服务,有各自的数据库
  • 必须用分布式事务保证跨服务的数据一致性

简历怎么写: “采购入库涉及PMS和WMS两个服务的数据更新,用Seata AT模式实现分布式事务。收货确认时,同时更新采购订单状态、WMS库存、在途库存。Seata自动记录undo_log,任何一步失败都能自动回滚,保证数据一致性。“


4. 应付账款账期管理

业务场景:

  • 供应商A:现款现货(payment_days = 0)
  • 供应商B:月结30天(payment_days = 30)
  • 供应商C:月结60天(payment_days = 60)

收货后自动生成应付账款,根据账期计算到期日期,定时任务提前提醒付款。

核心实现:

1. 生成应付账款

@Transactional(rollbackFor = Exception.class)
public void generatePayable(Long poId) {
    // 1. 查询采购订单
    PurchaseOrder po = purchaseOrderMapper.selectById(poId);
    
    // 2. 查询供应商账期
    Supplier supplier = supplierMapper.selectById(po.getSupplierId());
    int paymentDays = supplier.getPaymentDays();
    
    // 3. 计算到期日期
    LocalDate dueDate = LocalDate.now().plusDays(paymentDays);
    
    // 4. 创建应付账款
    FinancePayable payable = new FinancePayable();
    payable.setPoId(poId);
    payable.setPoNo(po.getPoNo());
    payable.setSupplierId(po.getSupplierId());
    payable.setSupplierName(po.getSupplierName());
    payable.setPayableAmount(po.getTotalAmount());
    payable.setPaidAmount(BigDecimal.ZERO);
    payable.setDueDate(dueDate);
    payable.setStatus(PayableStatus.UNPAID.getCode());
    financePayableMapper.insert(payable);
}

2. 定时任务提前提醒

@Scheduled(cron = "0 0 9 * * ?")  // 每天早上9点执行
public void checkPayableDue() {
    // 1. 查询3天内到期的应付账款
    LocalDate threeDaysLater = LocalDate.now().plusDays(3);
    List<FinancePayable> payables = financePayableMapper.selectList(
        new LambdaQueryWrapper<FinancePayable>()
            .eq(FinancePayable::getStatus, PayableStatus.UNPAID.getCode())
            .le(FinancePayable::getDueDate, threeDaysLater)
    );
    
    // 2. 发送提醒
    for (FinancePayable payable : payables) {
        long daysLeft = ChronoUnit.DAYS.between(LocalDate.now(), payable.getDueDate());
        
        // 发送站内消息
        messageService.send(
            payable.getTenantId(),
            "应付账款即将到期",
            String.format("采购单 %s 的应付账款将在 %d 天后到期,金额 %.2f 元,请及时付款",
                payable.getPoNo(), daysLeft, payable.getPayableAmount())
        );
        
        // 发送邮件(可选)
        emailService.send(payable.getFinanceEmail(), "应付账款到期提醒", ...);
    }
}

3. 付款操作

@Transactional(rollbackFor = Exception.class)
public void payPayable(Long payableId, BigDecimal payAmount) {
    // 1. 查询应付账款
    FinancePayable payable = financePayableMapper.selectById(payableId);
    
    // 2. 更新已付金额
    BigDecimal newPaidAmount = payable.getPaidAmount().add(payAmount);
    payable.setPaidAmount(newPaidAmount);
    
    // 3. 判断是否全部付清
    if (newPaidAmount.compareTo(payable.getPayableAmount()) >= 0) {
        payable.setStatus(PayableStatus.PAID.getCode());
        payable.setPayTime(LocalDateTime.now());
        
        // 4. 更新采购订单状态为"已付款"
        purchaseOrderMapper.update(null,
            new LambdaUpdateWrapper<PurchaseOrder>()
                .eq(PurchaseOrder::getId, payable.getPoId())
                .set(PurchaseOrder::getStatus, PurchaseOrderStatus.PAID.getCode())
        );
    } else {
        payable.setStatus(PayableStatus.PARTIAL_PAID.getCode());
    }
    
    financePayableMapper.updateById(payable);
    
    // 5. 写入付款记录
    insertPaymentLog(payableId, payAmount);
}

简历怎么写: “实现应付账款账期管理,收货后自动生成应付账款,根据供应商账期计算到期日期。定时任务每天早上9点检查3天内到期的应付账款,自动发送站内消息和邮件提醒。支持部分付款,付款后自动更新采购订单状态。“


三、简历怎么写

项目描述

项目:跨境电商 SaaS 供应链管理平台 - 采购管理模块(PMS)
时间:2024.06 - 2024.12
角色:核心开发
技术:Spring Boot 3.2、MyBatis-Plus、MySQL 8.0、Seata、XXL-Job

这是个跨境电商的供应链 SaaS 平台,我负责采购管理模块。系统管理从采购需求产生
到入库付款的完整流程,包括采购申请、询价比价、采购订单、收货入库、应付账款等。
支持多租户,日均处理采购订单 500+,管理供应商 200+。

核心难点:
1. 采购订单8状态流转控制
2. 询价比价算法设计
3. 采购入库分布式事务
4. 应付账款账期管理
5. 部分收货和部分付款的业务逻辑

核心亮点

1. 采购订单8状态状态机设计

问题:采购订单状态复杂,支持部分收货和部分付款,状态流转难以控制

方案:设计8状态状态机,用白名单管理合法流转

具体实现:

  • 定义8种状态:草稿、待确认、已确认、部分收货、已收货、部分付款、已付款、已结清
  • 定义9种合法流转路径,用Set存储状态流转白名单
  • 每次状态变更前,先用状态机校验是否合法
  • 用CAS乐观锁更新状态,WHERE条件判断当前状态
  • 所有状态变更写入操作日志,可追溯

效果:

  • 完全避免了非法状态流转
  • 支持灵活的部分收货和部分付款
  • 状态流转逻辑集中管理,易于维护

2. 询价比价综合评分算法

问题:同一个SKU向多家供应商询价,如何选择最优供应商?单纯比价可能导致质量风险

方案:设计综合评分算法,考虑价格、交货期、供应商评级三个维度

具体实现:

  • 价格得分(40%)= (最低报价 / 当前报价) × 40
  • 交货期得分(30%)= (最短交货期 / 当前交货期) × 30
  • 供应商评级得分(30%)= (供应商评分 / 100) × 30
  • 综合得分 = 三项得分之和
  • 系统自动计算并排序,推荐综合得分最高的供应商

效果:

  • 避免了单纯比价导致的质量风险
  • 综合考虑多个维度,实现综合最优
  • 采购决策更科学,供应商选择更合理

3. 采购入库分布式事务方案

问题:收货入库涉及PMS和WMS两个服务,必须保证数据一致性

方案:用Seata AT模式实现分布式事务

具体实现:

  • 收货确认时,同时更新采购订单状态、WMS库存、在途库存
  • Seata自动记录undo_log(回滚日志)
  • 如果全部成功,提交全局事务,删除undo_log
  • 如果任何一步失败,根据undo_log自动回滚
  • 配置Seata Server作为事务协调器(TC)

效果:

  • 完全保证了跨服务的数据一致性
  • 任何一步失败都能自动回滚
  • 对业务代码侵入小,只需加@GlobalTransactional注解

4. 应付账款账期管理和到期提醒

问题:不同供应商账期不同,人工跟踪容易遗漏,导致逾期付款影响信用

方案:自动生成应付账款,定时任务提前提醒

具体实现:

  • 收货后自动生成应付账款,根据供应商账期计算到期日期
  • 定时任务每天早上9点检查3天内到期的应付账款
  • 自动发送站内消息和邮件提醒财务专员
  • 支持部分付款,付款后自动更新采购订单状态
  • 付款记录写入日志,可追溯

效果:

  • 完全避免了逾期付款
  • 提前3天提醒,留足付款准备时间
  • 支持灵活的部分付款

四、面试高频问题

Q1:采购订单为什么需要8种状态?

答:因为采购业务比较复杂,支持部分收货和部分付款。

比如采购1000个耳机,供应商可能分两次发货,第一次500个,第二次500个。这就需要”部分收货”状态。

付款也是,可能先付50%定金,收货后再付50%尾款。这就需要”部分付款”状态。

8种状态能完整覆盖采购订单的全生命周期,既灵活又清晰。

Q2:询价比价算法为什么不只看价格?

答:单纯比价可能导致质量风险。

比如供应商A报价最低,但评级是C级,经常延期发货、质量不稳定。供应商B报价略高,但评级是S级,质量稳定、准时发货。

如果只看价格选A,短期省钱,长期可能因为质量问题、延期发货导致更大损失。

所以我们综合考虑价格、交货期、供应商评级三个维度,实现综合最优。

Q3:分布式事务为什么用Seata而不是本地事务?

答:因为PMS和WMS是两个独立的服务,有各自的数据库。

本地事务只能保证单个数据库的一致性,无法保证跨服务的一致性。

Seata是分布式事务框架,支持AT模式(自动补偿)。它会自动记录undo_log,任何一步失败都能自动回滚,保证数据一致性。

Q4:应付账款到期提醒为什么提前3天?

答:提前3天是经过业务调研确定的。

如果提前太早(比如7天),财务可能忘记。如果提前太晚(比如1天),可能来不及准备资金。

提前3天刚好:财务有时间准备资金、走审批流程、安排付款。

而且我们是每天早上9点提醒,财务上班就能看到,不会遗漏。

Q5:部分收货和部分付款的业务逻辑是怎么实现的?

答:我们在采购订单明细表中记录了两个字段:

  • quantity:采购数量
  • received_qty:已收货数量

每次收货时,更新received_qty。如果received_qty < quantity,状态是”部分收货”;如果received_qty == quantity,状态是”已收货”。

付款也类似,应付账款表中记录:

  • payable_amount:应付金额
  • paid_amount:已付金额

每次付款时,更新paid_amount。如果paid_amount < payable_amount,状态是”部分付款”;如果paid_amount >= payable_amount,状态是”已付款”。

Q6:如果Seata事务回滚失败怎么办?

答:Seata有多层保障:

  1. 自动重试:回滚失败会自动重试,最多重试5次
  2. 人工介入:如果重试还是失败,Seata会记录到异常日志,运维人工处理
  3. 监控告警:我们配置了Seata监控,回滚失败会立即告警

实际上,Seata的回滚成功率非常高(99.9%以上),因为它只是根据undo_log恢复数据,操作很简单。


五、面试准备建议

必须能画的图

采购完整流程:

采购需求 → 采购申请 → 审批 → 询价 → 比价 → 
采购订单 → 发货 → 收货 → 入库 → 应付账款 → 付款

采购订单状态流转:

草稿 → 待确认 → 已确认 → 部分收货 → 已收货 → 
部分付款 → 已付款 → 已结清

必须能讲清楚的

  • 采购订单为什么需要8种状态?
  • 询价比价算法为什么不只看价格?
  • 分布式事务为什么用Seata?
  • 应付账款到期提醒为什么提前3天?
  • 部分收货和部分付款的业务逻辑?

表达技巧

  1. 先讲业务,再讲技术
  2. 用具体例子说明(如1000个耳机分两次发货)
  3. 主动展开:不要等面试官问,主动讲状态机、分布式事务、比价算法
  4. 准备追问:每个点都要准备2-3个追问
  5. 不要背书:要自然地表达

最后提醒:

面试前把这份笔记看2-3遍,重点记住:

  • 采购完整流程(12个环节)
  • 采购订单8状态流转
  • 询价比价算法(三维度综合评分)
  • 分布式事务方案(Seata AT模式)
  • 应付账款账期管理
  • 简历上的4个亮点

面试时不要背书,要像讲故事一样自然地表达。准备好画图,采购流程图、状态流转图一定要能画出来。