项目优化 ver.2 / Day6
我们今天将我们hmdp的这个支付过程上一个沙箱模拟一下真正流程

新建了一个「PayOrderStatus」类 目的是确认支付这一链路的状态
public enum PayOrderStatus {
INIT(1, "已创建"),
PAYING(2, "支付中"),
SUCCESS(3, "支付成功"),
FAIL(4, "支付失败"),
CLOSED(5, "已关闭"),
REFUND(6, "已退款");
}
新建了一个支付表 加了这些字段:
- id, order_id, user_id, voucher_id
- out_trade_no, third_trade_no
- channel, amount, status, notify_status
- notify_content, fail_reason
- pay_time, create_time, update_time
还加了这些索引:
UNIQUE KEY `uk_out_trade_no` (`out_trade_no`) USING BTREE, UNIQUE KEY `uk_order_id` (`order_id`) USING BTREE,
KEY `idx_voucher_id` (`voucher_id`) USING BTREE,
KEY `idx_create_time` (`create_time`) USING BTREE,
KEY `idx_status_create_time` (`status`, `create_time`) USING BTREE, KEY `idx_user_create_time` (`user_id`, `create_time`) USING BTREE
先补分库分表配置
再补entity、mapper、service、impl的骨架。
现在项目里订单状态的 NORMAL 实际承担了两种含义:
- “订单存在,可视为已抢到”
- “订单完成,可视为成功订单”
接入支付后,这两种含义必须拆开,所以应该分阶段迁移查询条件。
先把所有用到 OrderStatus.NORMAL 的地方分类,再分别替换。
第一类:防重复下单
这类地方原来查 NORMAL,现在应该改成查:
- PENDING
- PAID
含义是:
- 待支付单也算已占坑,不能再抢
- 已支付单更不能再抢
在createVoucherOrderV2()内我们的查已抢过的逻辑是
VoucherOrder normalVoucherOrder = lambdaQuery()
.eq(VoucherOrder::getVoucherId, messageBody.getVoucherId())
.eq(VoucherOrder::getUserId, userId)
.eq(VoucherOrder::getStatus,OrderStatus.NORMAL.getCode())
.one();
那应该把
.eq(VoucherOrder::getStatus, OrderStatus.NORMAL.getCode())
改成
.in(VoucherOrder::getStatus,
OrderStatus.PENDING.getCode(),
OrderStatus.PAID.getCode())
第二类:查询“用户是否已买过该券”
如果页面想表达“这个券你已经占单了,不能再抢”,那也应该查:
- PENDING
- PAID
第三类:取消订单
现在 cancel() 是按 NORMAL 查
接支付后,这里要先明确业务规则:
- 如果“取消领取”其实是取消待支付订单,那就只允许 PENDING
- 如果“已支付后也允许取消”,那就不是这个接口的语义了,应该是退款链路
最后总结:
枚举:PENDING/CANCEL/PAID/TIMEOUT
查询:按 PENDING/PAID 防重
取消:只允许 PENDING
建单:显式写 PENDING
目前抢券这一块的流程是前端点“抢购”后,后端先在 Redis 层抢资格和扣库存,再异步发 Kafka,消费者落库创建订单。整个流程里没有真实支付阶段,所以“订单创建成功”就等同于“抢购成功”。
它当前的步骤是:
- 查秒杀券信息
seckillVoucherService.queryByVoucherId(voucherId) - 预热/加载库存到 Redis
seckillVoucherService.loadVoucherStock(voucherId) - 取当前用户 ID
- 校验用户等级是否满足参与条件
verifyUserLevel(...) - 生成两个 ID
- orderId:订单号
- traceId:追踪号
- 组装 Redis Lua 脚本的 keys 和 args
包括:- 库存 key
- 用户防重 key
- trace 日志 key
- 开始时间/结束时间
- 券状态
- orderId
- traceId
- 日志类型
- TTL
- 执行 Lua 脚本
seckillVoucherOperate.execute(keys, args) - Lua 成功后,组装 Kafka 消息 SeckillVoucherMessage
消息里带: - 立刻返回 orderId 给前端
- userId
- voucherId
- orderId
- traceId
- beforeQty
- changeQty
- afterQty
- autoIssue
- 发送 Kafka
seckillVoucherProducer.sendPayload(...)
Kafka 消费阶段
收到消息后流程是:
- beforeConsume() 先检查消息是否超时
如果消息延迟超过阈值:- 回滚 Redis 里的库存和用户抢购标记
- 记录对账日志
- 丢弃消息
- 没超时就进入 doConsume()
调:
voucherOrderService.createVoucherOrderV2(message)
真正落库
- 从消息里拿出 userId / voucherId / orderId
- 查数据库里是否已有同用户同券且状态为 NORMAL 的订单
如果有,说明重复,直接抛异常 - 扣减 DB 库存
stock = stock - 1 and stock > 0 - 创建 VoucherOrder
当前只设置了:- id
- userId
- voucherId
- createTime
- save(voucherOrder) 写入订单表
- 创建 VoucherOrderRouter
用于后续分片路由查询 - 把订单写到 Redis 缓存,供前端轮询查订单
- 记录对账日志
"order created" - 返回 true
我们先把支付这一整条状态流转连起来,添加以下方法
saveInitPayOrder、startPay、markPaySuccess、markPayFail、closePayOrder、refundPayOrder,外加 getByOrderId 和 getByOutTradeNo 查询入口。每一个状态都要检测前面的状态是否正确确保不会随意跳转出问题
在更新时候用了id+oldStatus的CAS的判断,避免并发把状态跳乱。重复回调/重复操作如果已经是目标状态,会直接返回当前单据,保持幂等。
然后把生产的消息字段扩充了支付金额 因为金额是在订购的时候有一个快照 然后还有一个支付方式以便后续扩展 现在先写死支付宝的支付
消费消息的时候直接调支付最顶接口然后去init我们的payorder存入 然后我继续接了“取消单”和“超时单”的逻辑 具体上是cancel()方法不仅仅是rollback和改order状态了 是再加了个我开的closePayOrder去关支付订单
补了一个新的服务入口 IVoucherOrderService.java:
timeoutCloseVoucherOrder(Long orderId)
这条方法会做和取消单同一套关闭流程,只是把业务订单状态改成 TIMEOUT,并记录 BusinessType.TIMEOUT 的对账日志。也就是说后面不管是接定时任务、延迟队列,还是支付超时扫描,都可以直接调这个方法,不需要再重复写一套库存回滚和支付关单逻辑。
- 在 VoucherOrderServiceImpl.java 里,createVoucherOrderV2() 事务提交后会注册一个 afterCommit 钩子,投递支付超时检查消息。
- 延迟消息体是新加的 DelayedPayOrderTimeoutMessage.java,带了 orderId/userId/voucherId/orderCreateTime。
- 延迟消费者是新加的 ConsumerDelayedPayOrderTimeout.java。它消费后会先查订单:
如果订单不存在,跳过。
如果订单已经不是 PENDING,跳过。
只有仍然 PENDING 时,才调用 voucherOrderService.timeoutCloseVoucherOrder(orderId)。 - topic 常量也补了,在 Constant.java 里新增了 DELAY_PAY_ORDER_TIMEOUT。
然后现在去打通支付流程
mock打通了测试完改一下 我开始接支付宝沙箱,生成了一下公私钥配了个环境,预期流程应该是
- 秒杀下单,生成 orderId
- 调 /pay-order/start
- 后端调用支付宝沙箱下单接口,前端跳支付宝收银台
- 用沙箱买家账号登录付款
- 支付宝回调 notify
- 系统把 pay_order 改成 SUCCESS,把 voucher_order 改成 PAID
- 前端再查 /pay-order/{orderId} 或订单状态
然后他回调的时候要notify到一个公网ip我正好买了这个ecs所以直接穿透打一下就可以

生成支付单之后直接去调支付宝下单
前后端好难调。。。呃呃呃先吃饭 等等再更新
一点问题总结
支付宝端超时关单问题
发现本地超时和支付宝侧交易没有闭环对齐。下单模型里没设置 timeout_express,而超时关单只做了本地 VoucherOrderServiceImpl.java:781 的 closePayOrder,没有 alipay.trade.close。结果就是本地订单 15 分钟后可能已经CLOSED/TIMEOUT,但支付宝收银台还可付款;一旦用户用旧页面付成功,回调会被拒掉,形成“真付了钱,本地不认”。解决的方法是发起支付时会把 pay.order.timeout.seconds 转成 timeout_express 带给支付宝,本地取消/超时关单后,还会在事务提交后补一次 alipay.trade.close。
voucher_order 的路由现在和支付链路的访问方式错位
因为我们这个支付天然只能拿到orderid 但是我们直接想拿到订单order信息 真正的路由键不是 orderid,而是 user_id + voucher_id。我们有一个路由表忘了用
在建单时写了 tb_voucher_order_router,里面存的是:
- orderId
- userId
- voucherId
真实应该是orderid to user+voucher
再去userId + voucherId + orderId -> voucher_order
没理清导致的