合并支付退款应调用什么接口,与普通退款有何区别?
明确结论
合并支付成功后,退款通常还是调用普通的“申请退款”接口,无需再次调用合并支付接口。退款一般按合并支付中的子订单逐笔处理,并传入该子订单对应的支付单号或商户订单号。
从请求格式看,合并支付退款与普通支付退款通常没有本质区别。区别主要在业务处理上:合并支付包含多笔子订单,退款时要明确具体订单和退款金额,并分别管理每笔退款的单号与状态。
各支付平台的规则可能不同。如果平台文档提供了“合并退款”或“批量退款”等专用接口,应以对应平台及接口版本的官方文档为准。
为什么不能直接按合并订单整体退款
合并支付用于一次支付多笔订单。支付渠道可能将其显示为一笔收款,但商户系统中仍有多个独立的子订单,例如:
合并支付单 C202609130001
├── 子订单 A:100.00 元
├── 子订单 B:50.00 元
└── 子订单 C:20.00 元
如果用户只退子订单 B,退款金额就应归到 B,不能直接对合并支付单发起一笔无法区分业务归属的退款。因此,退款通常按以下方式处理:
- 按子订单逐笔发起。
- 使用子订单对应的支付流水号或商户订单号。
- 每笔退款使用唯一的商户退款单号。
- 部分退款不能超过该子订单的可退金额。
- 多个子订单全部退款时,通常要分别提交退款请求。
合并关系主要影响退款的拆分和状态管理,未必会改变退款 API 本身。
推荐的对接流程
1. 查询并确认支付结果
先确认合并支付已成功,并取得各子订单对应的支付信息。除了合并支付单号,还应保存:
- 合并支付单号;
- 子订单商户订单号;
- 子订单支付平台交易号;
- 子订单支付金额;
- 子订单累计退款金额;
- 子订单当前退款状态。
如果支付回调没有返回完整信息,应先通过合并支付查询接口补齐,再发起退款。
2. 确定退款对象
根据售后申请找到对应的子订单,并计算可退金额:
可退金额 = 子订单实付金额 - 子订单已成功退款金额
退款金额必须大于零,且不能超过可退金额。优惠券、立减、积分或商户补贴的分摊方式,应按照支付平台的退款规则处理,不能只按商品原价计算。
3. 为每笔退款生成唯一退款单号
即使同一个子订单分多次退款,每次请求也应使用独立的商户退款单号,例如:
R20260913000101
R20260913000102
如果要重试同一笔退款,应继续使用原退款单号,不要生成新单号,否则平台可能将其识别为另一笔退款。
4. 调用普通退款接口
下面的示例只展示常见结构。字段名称和请求路径均为占位内容,不代表任何支付平台的正式定义:
POST /v1/refunds
Content-Type: application/json
Authorization: Bearer <access_token>
Idempotency-Key: R20260913000101
{
"transaction_id": "子订单对应的支付平台交易号",
"out_trade_no": "子订单商户订单号",
"out_refund_no": "R20260913000101",
"reason": "用户申请退款",
"amount": {
"refund": 5000,
"total": 5000,
"currency": "CNY"
},
"notify_url": "https://merchant.example.com/payment/refund/notify"
}
金额单位是否为“分”、transaction_id 与 out_trade_no 能否同时传入,以及是否需要证书签名,都应以实际支付平台的接口定义为准。
5. 异步确认最终退款状态
退款接口返回“受理成功”,不一定表示资金已经退回。商户系统应通过以下方式确认最终结果:
- 接收退款结果通知;
- 验证通知签名并解密报文;
- 按退款单号查询退款结果;
- 平台明确返回退款成功后,再更新订单的最终退款状态。
回调处理必须支持幂等,避免重复通知造成库存、余额或订单状态被多次修改。
与普通退款的主要区别
| 对比项 | 普通支付退款 | 合并支付退款 |
|---|---|---|
| 退款接口 | 通常使用普通退款接口 | 通常仍使用普通退款接口 |
| 退款对象 | 单个支付订单 | 合并支付中的具体子订单 |
| 订单识别 | 原支付交易号或商户订单号 | 子订单交易号或子订单商户订单号 |
| 全额退款 | 一次请求通常即可完成 | 可能需要对子订单逐笔退款 |
| 部分退款 | 校验订单剩余可退金额 | 还需分别校验每个子订单的可退金额 |
| 状态管理 | 管理单笔支付和退款 | 同时管理合并单、子订单和多笔退款 |
| 请求格式 | 由退款 API 定义 | 一般相同,个别平台可能增加关联字段 |
多个子订单全部退款的处理示例
如果需要退回整个合并支付单,在没有官方依据的情况下,不要将所有子订单金额合并成一笔普通退款。更稳妥的做法是逐笔提交:
async function refundCombinedOrder(combinedOrder) {
const results = [];
for (const subOrder of combinedOrder.subOrders) {
if (subOrder.refundableAmount <= 0) {
continue;
}
const result = await requestRefund({
transaction_id: subOrder.transactionId,
out_trade_no: subOrder.outTradeNo,
out_refund_no: createRefundNo(subOrder),
amount: {
refund: subOrder.refundableAmount,
total: subOrder.paidAmount,
currency: "CNY"
}
});
results.push({
subOrderNo: subOrder.outTradeNo,
refundNo: result.out_refund_no,
status: result.status
});
}
return results;
}
这类操作并不是严格意义上的数据库事务,可能出现部分子订单退款成功、部分失败的情况。因此,不能因为其中一笔失败,就将已经成功的退款回滚为未退款状态。系统应记录每笔退款的结果,并对失败项单独查询或重试。
API 对接时的注意事项
不要把合并支付单号当成子订单号
如果普通退款接口要求原支付交易号,应传入子订单对应的交易号。能否直接传入合并支付单号,必须查阅支付平台文档,不能自行替换。
不要重复创建退款
网络超时只表示商户没有收到明确响应,不代表平台没有受理退款。遇到超时时,应优先使用原退款单号查询或重试,以保证幂等。
分别核算每个子订单的金额
退款金额、原订单金额和累计已退金额,通常都以子订单为核算范围。不能把整个合并单的总金额作为某个子订单退款请求中的原订单金额。
合并单状态需要汇总计算
可以根据子订单状态计算合并单的退款状态,例如:
- 所有子订单均未退款:未退款;
- 部分子订单退款成功:部分退款;
- 所有子订单可退金额均为零:已全部退款;
- 存在处理中或失败的退款:退款处理中或部分失败。
不要仅依赖同步响应
系统应保存退款请求记录,并结合异步通知和主动查询完成状态闭环。支付平台的通知可能延迟、重复或乱序,因此更新状态时需要校验当前状态,防止旧通知覆盖新结果。
最终判断标准
对接前,应在支付平台文档中确认以下三个问题:
- 普通退款接口是否支持合并支付产生的子订单;
- 退款时要求传入合并支付单号、子订单号,还是子订单交易号;
- 全部退款是否需要逐笔调用,平台是否提供批量退款接口。
如果文档没有提供专用的合并退款接口,通常可以采用“查询合并支付结果 → 定位子订单 → 调用普通退款接口 → 分别确认退款状态”的流程。需要重点调整的是订单关联、金额校验、幂等重试和多笔退款状态汇总,而不是请求协议本身。
备注:内容仅供参考。