题目重点:设计用户余额与商品购买 API/表结构,解释 pending 的必要性、重复请求幂等和进程宕机时的事务/对账。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 训练版使用平台内余额购买虚拟商品,credit 充值可以来自外部支付;金额以整数最小单位保存。购买价由服务端商品表决定,客户端不能自行传最终总价作为权威。
- 购买请求携带 (item_id,quantity,idempotency_key);退款针对已完成 purchase,是否允许部分退款及资格由训练规则限定,题目未明确。
解题路线
- 表包含 wallet(user_id,balance,version)、wallet_ledger(id,user_id,amount,kind,reference_id)、item(id,price_version,price,stock?)、purchase(id,user_id,item_id,quantity,unit_price,total,status)、refund(id,purchase_id,amount,status),并给幂等键加 (user_id,operation,key) 唯一约束。
- 充值必须先确认外部支付,再在数据库事务里写 ledger 与增加余额;重复 PSP 回调靠 provider_event_id 唯一性去重。若充值是纯内部赠额,仍需授权和审计来源。
- 购买时服务端读取价/库存,校验数量和余额;在单个事务内条件扣余额、写负向 ledger、创建 PENDING purchase 与 outbox 交付任务。Pending 表示资金已预留或扣减但物品交付未确认,便于宕机后恢复;交付成功转 COMPLETED,失败补偿余额并标 FAILED,所有状态转换按版本防重复。
- 退款以原 purchase 为锚:锁定可退余额、创建 refund request,退余额的 ledger 与状态更新同事务。若外部支付逆转参与流程,不能把本地余额增加当外部钱已退;等待回调并对账。
- quantity 让服务器计算 total=quantity×当前承诺单价,可防客户端篡价;若订单有价格锁定,传 price_quote_id/version 而不是信任 total value。重复请求直接返回已创建结果,不再二次扣额。
手动走一遍
用户余额 100,物品单价 30,买 2 件:服务器算总价 60,事务写余额 40、ledger -60、purchase=PENDING。若服务器提交后但返回前宕机,用户用同一 idempotency_key 重试会得到原 purchase;后台 outbox 完成交付后状态变 COMPLETED。若交付最终失败,补偿 ledger +60,余额回 100。
规模与取舍
单次购买涉及常数个表的索引读取与事务写入,逻辑 O(1) 加库存/订单行数;主要风险是行锁争用、外部交付延迟和幂等保留期,题目无 QPS 或金额规模。
易错点与追问
容易失分的边界
- 客户端 total value 直接决定扣额。
- 扣余额与建 purchase 不在同一事务。
- 把 pending 当永久终态,不设恢复/补偿。
- HTTP 重试重复扣款。
- 充值与退款只改余额、不留不可篡改 ledger。
- 把进程宕机当成数据库事务一定回滚,忽略提交后未回包。
继续深挖
- 为什么要 PENDING,哪一步后可变 COMPLETED?
- quantity 与 total value 哪个由客户端提交?
- 同一幂等键但参数不同怎么办?
- 退款与交付并发怎样序列化?
- 外部 PSP 成功但本地提交失败怎样对账?
