一、核心业务流程(面试必讲)
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. 采购入库分布式事务
业务场景: 仓库收货确认入库时,需要同时更新三个系统的数据:
- PMS:采购订单状态更新为”已收货”
- WMS:库存数量增加
- 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工作原理:
- TM(事务管理器)开启全局事务,生成全局事务ID(XID)
- PMS更新订单状态,Seata记录undo_log(回滚日志)
- WMS增加库存,Seata记录undo_log
- PMS减少在途库存,Seata记录undo_log
- 如果全部成功,TM提交全局事务,删除undo_log
- 如果任何一步失败,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有多层保障:
- 自动重试:回滚失败会自动重试,最多重试5次
- 人工介入:如果重试还是失败,Seata会记录到异常日志,运维人工处理
- 监控告警:我们配置了Seata监控,回滚失败会立即告警
实际上,Seata的回滚成功率非常高(99.9%以上),因为它只是根据undo_log恢复数据,操作很简单。
五、面试准备建议
必须能画的图
采购完整流程:
采购需求 → 采购申请 → 审批 → 询价 → 比价 →
采购订单 → 发货 → 收货 → 入库 → 应付账款 → 付款
采购订单状态流转:
草稿 → 待确认 → 已确认 → 部分收货 → 已收货 →
部分付款 → 已付款 → 已结清
必须能讲清楚的
- 采购订单为什么需要8种状态?
- 询价比价算法为什么不只看价格?
- 分布式事务为什么用Seata?
- 应付账款到期提醒为什么提前3天?
- 部分收货和部分付款的业务逻辑?
表达技巧
- 先讲业务,再讲技术
- 用具体例子说明(如1000个耳机分两次发货)
- 主动展开:不要等面试官问,主动讲状态机、分布式事务、比价算法
- 准备追问:每个点都要准备2-3个追问
- 不要背书:要自然地表达
最后提醒:
面试前把这份笔记看2-3遍,重点记住:
- 采购完整流程(12个环节)
- 采购订单8状态流转
- 询价比价算法(三维度综合评分)
- 分布式事务方案(Seata AT模式)
- 应付账款账期管理
- 简历上的4个亮点
面试时不要背书,要像讲故事一样自然地表达。准备好画图,采购流程图、状态流转图一定要能画出来。