题目重点:区分业务支付意图、渠道尝试与账务记录,处理回调重复、超时和退款。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 只有Payment Systems LLD方向,未给具体网关、币种和退款。练习定义单订单单币种支付、可重试尝试和部分退款;不承担真实监管规则设计。
- 外部渠道可能超时但已经收款,回调可能重复/乱序。内部ledger是权威入账事实,客户端成功页面不构成结算证据。
解题路线
- PaymentIntent保存订单金额/币种/业务版本,Attempt保存providerRequestId与渠道状态,LedgerEntry记录不可变金额变化;用整数最小货币单位或明确Decimal,避免float。
- 创建intent用业务幂等键且校验重复请求参数一致;相同key换金额应冲突而非返回旧成功。外部请求用稳定渠道幂等键,内部状态通过版本CAS和事务改变。
- 状态pending→processing→succeeded/failed,网络超时保持unknown/reconciling;只在确认渠道失败后新建合规尝试。回调验证身份和金额,唯一providerEventId防重复,再写ledger和outbox。
- 账务和订单状态不能靠双写“碰巧成功”;在同事务写入ledger/outbox,由消费方幂等更新订单。退款先原子预留可退金额,再发渠道请求,成功减少预留并记退款事实,失败释放预留。
手动走一遍
100单位订单的首attempt超时:先按providerRequestId查询,不能直接再扣100。两个并发60退款请求最多一笔预留成功,否则总退120超出原款;重复退款回调不再次扣额度。
规模与取舍
主键/唯一索引操作通常O(log N),单订单总额和可退额度是串行化点;外部调用延迟不能算数据库事务复杂度。按intent分片可扩吞吐,跨币种/跨订单汇总需要清晰账本维度。
易错点与追问
容易失分的边界
- 把timeout记failed导致重复扣款。
- 只在应用层查询幂等键,没有数据库唯一约束。
- 退款总额检查与预留分开,在并发下超退。
继续深挖
- 若provider不提供幂等请求,怎样降低风险并明确无法保证的边界?
- 如何做定期对账并修正“渠道成功内部未知”?
