下午茶拼单系统—6. 交易锁单、支付宝支付与最终一致性
前言
拼单系统最容易出问题的地方是交易链路。用户点下单后,系统要创建本地订单、锁定拼团优惠、生成支付宝支付单;用户支付成功后,还要通知营销域做拼团结算。任何一步失败,都可能出现本地订单和营销单状态不一致。
所以这一篇重点写交易锁单和最终一致性。这里的关键不是一次 HTTP 调用成功,而是当网络抖动、支付回调重复、营销结算失败时,系统仍然能靠状态机和补偿任务恢复。
交易域为什么先落本地单
创建订单时,我先查询用户是否已有未支付订单,避免同一个用户对同一商品反复生成支付单。如果已有 PAY_WAIT,直接返回原支付链接;如果已有 CREATE 但没有支付单,则继续补创建支付单。
OrderEntity unpaidOrderEntity = repository.queryUnPayOrder(shopCartEntity);
if (null != unpaidOrderEntity && OrderStatusVO.PAY_WAIT.equals(unpaidOrderEntity.getOrderStatusVO())) {
return PayOrderEntity.builder()
.orderId(unpaidOrderEntity.getOrderId())
.payUrl(unpaidOrderEntity.getPayUrl())
.build();
}
ProductEntity productEntity = port.queryProductByProductId(shopCartEntity.getProductId());
OrderEntity orderEntity = CreateOrderAggregate.buildOrderEntity(
productEntity.getProductId(),
productEntity.getProductName(),
shopCartEntity.getMarketTypeVO().getCode());
this.doSaveOrder(orderAggregate);
先落本地单有两个好处。第一,后续锁单或支付失败时有状态可查,不会只剩一条日志。第二,可以通过 Job 扫描 CREATE 或 PAY_WAIT 状态做补偿。
营销锁单
如果订单走拼团营销,交易域调用营销域 lockMarketPayOrder,把 userId、teamId、activityId、productId、orderId 传过去。营销域校验活动和库存后返回优惠金额、支付金额和组队信息。
if (MarketTypeVO.GROUP_BUY_MARKET.equals(shopCartEntity.getMarketTypeVO())) {
marketPayDiscountEntity = this.lockMarketPayOrder(
shopCartEntity.getUserId(),
shopCartEntity.getTeamId(),
shopCartEntity.getActivityId(),
productEntity.getProductId(),
orderEntity.getOrderId());
}
这里 orderId 是非常关键的幂等键。营销域锁单不能只按 userId 和 activityId 判断,否则用户重试下单时很难识别是否同一笔交易。用 orderId 作为外部交易号,可以保证锁单、结算、退单都围绕同一个业务单据。
创建支付单
BigDecimal payAmount = null == marketPayDiscountEntity
? totalAmount
: marketPayDiscountEntity.getPayPrice();
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setNotifyUrl(notifyUrl);
request.setReturnUrl(returnUrl);
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", orderId);
bizContent.put("total_amount", payAmount);
bizContent.put("subject", productName);
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY");
request.setBizContent(bizContent.toString());
String form = alipayClient.pageExecute(request).getBody();
支付宝支付单里 out_trade_no 使用本地 orderId,回调时就可以直接定位订单。支付金额使用营销锁单返回的 payPrice,而不是前端传来的金额,避免客户端篡改。
支付成功后的结算
支付宝回调通过验签后,交易域更新订单支付成功。如果订单是拼团营销类型,还要调用营销域 settlementMarketPayOrder 做结算。这里同步调用可能失败,所以需要依赖支付宝回调重试、主动查询支付状态 Job 或独立补偿 Job。
if (MarketTypeVO.GROUP_BUY_MARKET.getCode().equals(orderEntity.getMarketType())) {
repository.changeMarketOrderPaySuccess(orderId);
port.settlementMarketPayOrder(orderEntity.getUserId(), orderId, payTime);
} else {
repository.changeOrderPaySuccess(orderId, payTime);
}
这段代码体现的是最终一致性:交易域先把支付成功事实落下来,再推动营销结算。如果营销结算失败,不应该回滚支付成功,而应该通过状态和补偿继续推进。
小结
交易锁单链路要把幂等键、状态机和补偿考虑在一起。先落本地单,营销锁单返回权威优惠金额,支付宝回调验签后更新支付状态,再推动营销结算。这样系统面对重复请求和异步失败时,有机会自己恢复,而不是依赖人工查库。





