支付宝是否支持同一商户下多笔订单合并支付
结论
可以用一次付款结清同一商户下的多笔业务订单,但支付宝侧通常只会生成一笔交易,不会自动拆成多笔独立交易。
常见的实现方式如下:
- 商户系统创建一笔”合并支付单”。
- 将多笔业务订单关联到这笔支付单。
- 使用合并支付单号作为支付宝接口中的
out_trade_no,支付金额为各订单待付金额之和。 - 支付成功后,商户系统根据异步通知,分别将关联的业务订单更新为已支付。
支付完成后,两侧的数据表现通常是:
- 支付宝侧只有一笔交易,对应一个
out_trade_no和一个trade_no。 - 商户侧仍有多笔相互独立的业务订单,这些订单共同关联到同一笔支付宝交易。
如果支付宝侧也必须产生多笔独立交易,就需要分别发起多次支付,此时无法同时保留”一次付款”的用户体验。
为什么需要合并支付单
支付宝支付接口通常以一笔商户支付订单为交易单位。一次接口请求会提交一个商户订单号和一个支付总金额,例如:
out_trade_no = PAY202609080001
total_amount = 300.00
支付成功后,支付宝会为这笔请求生成一个支付宝交易号:
trade_no = 202609082200...
即使这笔金额对应商户内部的三个订单,支付宝也不会仅凭商品明细自动拆分出三个独立的支付交易号。
商户系统可以建立以下关系:
合并支付单 PAY202609080001
├── 业务订单 ORDER001:100.00
├── 业务订单 ORDER002:80.00
└── 业务订单 ORDER003:120.00
支付宝实付金额:300.00
支付宝交易数量:1
商户业务订单数量:3
商品明细可以用于支付页面展示或商户内部记录,但不能替代业务订单与支付单之间的关联关系。
实现步骤
1. 校验待合并的业务订单
创建支付单前,至少需要确认:
- 所有订单属于同一个用户。
- 所有订单属于同一个收款商户。
- 所有订单均处于待支付状态。
- 订单的币种和支付渠道兼容。
- 订单未被其他支付单占用。
- 商品金额、优惠金额、运费和应付金额已经确定。
- 订单未过期、关闭或进入退款流程。
这里的”同一商户”应按实际的支付宝收款主体和支付应用配置判断,不能只看前端显示的店铺名称。
2. 创建合并支付单
合并支付单需要使用独立且全局唯一的编号,不应直接复用某一笔业务订单的订单号。
示例数据结构:
CREATE TABLE payment_order (
payment_no VARCHAR(64) PRIMARY KEY,
user_id VARCHAR(64) NOT NULL,
merchant_id VARCHAR(64) NOT NULL,
channel VARCHAR(32) NOT NULL,
total_amount DECIMAL(18, 2) NOT NULL,
status VARCHAR(32) NOT NULL,
channel_trade_no VARCHAR(64),
created_at TIMESTAMP NOT NULL,
paid_at TIMESTAMP NULL
);
CREATE TABLE payment_order_item (
payment_no VARCHAR(64) NOT NULL,
order_no VARCHAR(64) NOT NULL,
pay_amount DECIMAL(18, 2) NOT NULL,
PRIMARY KEY (payment_no, order_no)
);
其中:
payment_order表示一次实际支付。payment_order_item保存支付单与多笔业务订单之间的关系。channel_trade_no保存支付宝返回的trade_no。
3. 计算并固化支付金额
金额应由服务端使用精确的小数类型计算,不能使用 float 或 double。
BigDecimal totalAmount = orders.stream()
.map(Order::getPayableAmount)
.reduce(BigDecimal.ZERO, BigDecimal::add)
.setScale(2, RoundingMode.UNNECESSARY);
创建支付单时,还应保存每笔子订单分摊的支付金额。支付期间不要只依赖实时重新计算,否则一旦订单价格或优惠发生变化,可能导致通知金额与订单金额不一致。
4. 向支付宝发起一笔支付
以下代码只展示合并支付涉及的核心字段。具体客户端类、接口名称和配置方式,应以实际使用的支付宝产品及对应 SDK 文档为准。
String paymentNo = "PAY202609080001";
BigDecimal totalAmount = new BigDecimal("300.00");
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setNotifyUrl("https://merchant.example.com/alipay/notify");
request.setReturnUrl("https://merchant.example.com/payment/result");
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo(paymentNo);
model.setTotalAmount(totalAmount.toPlainString());
model.setSubject("多订单合并支付");
model.setProductCode("FAST_INSTANT_TRADE_PAY");
request.setBizModel(model);
AlipayTradePagePayResponse response = alipayClient.pageExecute(request);
if (!response.isSuccess()) {
throw new IllegalStateException(
"创建支付宝支付失败: " + response.getSubCode() + " " + response.getSubMsg()
);
}
提交给支付宝的是合并支付单号 PAY202609080001,不是 ORDER001、ORDER002、ORDER003 这三个业务订单号。
手机网站支付、App 支付、小程序支付或当面付的调用方式各不相同,但基本都遵循”一个支付请求对应一笔渠道交易”的建模原则。
5. 处理支付宝异步通知
不能只根据同步跳转页面判断支付成功。商户系统需要验签异步通知,并检查:
- 通知签名是否合法。
app_id、收款账号或商户身份是否符合预期。out_trade_no是否存在。- 通知金额是否等于合并支付单金额。
- 交易状态是否属于可确认成功的状态。
- 该通知是否已经处理。
确认支付成功后,应在同一个本地数据库事务中更新支付单及其子订单:
@Transactional
public void handlePaymentSuccess(
String paymentNo,
String alipayTradeNo,
BigDecimal notifiedAmount) {
PaymentOrder payment = paymentRepository.lockByPaymentNo(paymentNo)
.orElseThrow(() -> new IllegalArgumentException("支付单不存在"));
if (payment.isPaid()) {
return; // 异步通知可能重复,必须支持幂等处理
}
if (payment.getTotalAmount().compareTo(notifiedAmount) != 0) {
throw new IllegalStateException("支付通知金额不一致");
}
payment.markPaid(alipayTradeNo);
paymentRepository.update(payment);
List<PaymentOrderItem> items =
paymentItemRepository.findByPaymentNo(paymentNo);
for (PaymentOrderItem item : items) {
orderRepository.markPaid(
item.getOrderNo(),
paymentNo,
alipayTradeNo,
item.getPayAmount()
);
}
}
这些业务订单可以继续保留各自的订单号、商品、履约状态和售后状态,只是共用同一个支付宝 trade_no。
退款如何处理
合并支付后,如果某笔子订单需要退款,可以按该订单在合并支付单中的实付金额,对原支付宝交易发起部分退款。
例如:
原支付宝交易金额:300.00
ORDER001 实付金额:100.00
ORDER002 实付金额:80.00
ORDER003 实付金额:120.00
取消 ORDER002 时,可以尝试对原交易退款 80.00。商户系统需要为每次退款生成唯一的退款请求号,并记录累计退款金额。
退款时需要注意:
- 所有子订单的退款都引用同一笔原支付宝交易。
- 累计退款金额不能超过原交易的实付金额。
- 优惠、满减和运费的分摊必须在支付时确定,否则部分退款的金额可能无法准确计算。
- 具体产品是否支持部分退款,以及退款期限和次数限制,应以所使用支付宝产品的当前规则为准。
对账与售后注意事项
支付交易与业务订单不能放在同一张表中
一笔支付可能对应多笔业务订单。一笔业务订单也可能经历支付失败、重新支付或更换支付渠道。因此,更合理的关系是:
业务订单 N ── 1 合并支付单 1 ── 1 支付宝交易
如果允许重新支付,关系还可能变成:
一组业务订单 ── 多次支付尝试 ── 最终一笔成功交易
发起支付后应锁定支付单内容
创建支付后,不应再向这笔支付单追加或移除子订单。如果订单发生变化,应关闭或作废原支付单,再生成新的支付单和 out_trade_no。
支付宝账单通常只显示合并交易
支付宝账单及对账文件中通常只能看到合并支付单号、支付宝交易号和总金额。商户需要通过自己的支付单关联表,将总金额还原到各笔业务订单。
防止重复履约
异步通知、主动查询和补偿任务可能同时确认同一笔支付。更新业务订单时,必须通过状态条件、数据库锁或唯一约束保证幂等,防止重复发货、重复充值或重复记账。
推荐使用”一笔合并支付单关联多笔业务订单”的方案。用户只需完成一次支付宝付款,商户系统仍可保留多笔独立订单。这些订单会共享同一笔支付宝交易,不会分别获得独立的支付宝交易号。
备注:内容仅供参考。
