如何同步商户在B站后台发起的退款状态?
结论
如果 B 站支付接口在 TRADE_CLOSED 等退款场景中不发送异步通知,业务系统就无法仅凭支付回调实时发现后台退款。但也没有必要每天遍历全部历史订单。
一般可以这样处理:
- 所有退款尽量统一从自有系统发起,避免直接在 B 站后台操作。
- 分别保存退款请求和处理结果,再通过退款查询接口确认最终状态。
- 对后台人工退款等异常路径,用增量轮询和定期对账补漏。
- 如果 B 站提供退款通知、账单下载或退款单查询,应优先使用。具体支持哪些能力,以当前接入文档和商户后台的实际功能为准。
为什么支付回调解决不了这个问题
支付成功回调只能说明订单曾经支付成功,无法反映之后是否发生退款。
如果商户绕过自有系统,直接在 B 站后台退款,就会同时出现两种状态:
- B 站已经完成退款;
- 自有系统没有退款记录,也没有收到状态变更事件。
平台不向商户系统发送退款通知时,自有系统不可能自动、实时地知道这次变化,只能依靠主动查询、账单对账或操作流程约束来发现。
同时,不要把 TRADE_CLOSED 直接视为“退款成功”。这个状态还可能包含未支付关闭、超时关闭等情况,准确含义要以对应接口的状态说明为准。判断是否退款时,最好检查退款单、退款金额或账单明细,而不是只看支付订单状态。
推荐的处理流程
1. 将自有系统作为退款入口
运营人员需要退款时,先在自有系统创建退款申请,再由服务端调用 B 站退款接口。
建议保存这些信息:
- 业务订单号;
- B 站支付订单号;
- 商户退款单号;
- 申请退款金额;
- 已成功退款金额;
- 退款状态;
- 平台退款单号;
- 退款原因;
- 发起人和操作来源;
- 最近查询时间;
- 原始响应摘要。
退款状态可以设计为:
REFUND_PENDING
REFUND_PROCESSING
REFUND_SUCCESS
REFUND_FAILED
REFUND_UNKNOWN
订单状态和退款状态最好分开保存。发生部分退款时,不能直接把订单标记为“全额退款”。
2. 异步确认退款结果
退款接口返回“受理成功”,不一定表示资金已经退回。调用退款接口后,应先把退款单设为处理中,再通过平台通知或退款查询接口确认最终结果。
创建退款单
↓
调用 B 站退款接口
↓
记录受理结果
↓
退款通知或定时查询
↓
确认成功金额
↓
更新退款单和业务订单
更新逻辑必须具备幂等性,防止通知和查询任务同时到达时重复累计退款金额。
3. 对处理中退款进行有限轮询
不必查询所有订单,只需查询状态仍可能变化的数据,例如:
- 状态为
REFUND_PENDING或REFUND_PROCESSING的退款单; - 最近一段时间内支付成功,但还没有完成对账的订单;
- 查询失败或状态未知的异常订单;
- 对账时发现不一致的订单。
查询频率可以随时间逐步降低。退款刚发起时适当提高频率,长时间未确认后降低频率,超过合理期限再转入人工处理队列。具体间隔和期限要根据平台限流规则、退款时效和业务量确定。
代码示例
下面是一个简化的 TypeScript 示例。接口名称采用抽象命名,接入时需要替换为实际的 SDK 或 HTTP API。
type RefundStatus =
| "REFUND_PENDING"
| "REFUND_PROCESSING"
| "REFUND_SUCCESS"
| "REFUND_FAILED"
| "REFUND_UNKNOWN";
interface LocalRefund {
refundNo: string;
orderNo: string;
platformOrderNo: string;
refundAmount: number;
status: RefundStatus;
}
interface PlatformRefundResult {
status: "PROCESSING" | "SUCCESS" | "FAILED";
refundedAmount?: number;
platformRefundNo?: string;
}
async function syncRefund(refund: LocalRefund): Promise<void> {
const result: PlatformRefundResult =
await bilibiliPayClient.queryRefund({
merchantRefundNo: refund.refundNo,
platformOrderNo: refund.platformOrderNo,
});
await database.transaction(async (tx) => {
const current = await tx.refunds.lockByRefundNo(refund.refundNo);
if (!current || current.status === "REFUND_SUCCESS") {
return;
}
if (result.status === "SUCCESS") {
await tx.refunds.markSuccess({
refundNo: refund.refundNo,
platformRefundNo: result.platformRefundNo,
refundedAmount: result.refundedAmount,
});
const summary = await tx.refunds.sumSuccessfulAmount(
refund.orderNo
);
const order = await tx.orders.lockByOrderNo(refund.orderNo);
if (summary >= order.paidAmount) {
await tx.orders.updateStatus(
refund.orderNo,
"FULLY_REFUNDED"
);
} else if (summary > 0) {
await tx.orders.updateStatus(
refund.orderNo,
"PARTIALLY_REFUNDED"
);
}
return;
}
if (result.status === "FAILED") {
await tx.refunds.updateStatus(
refund.refundNo,
"REFUND_FAILED"
);
return;
}
await tx.refunds.updateStatus(
refund.refundNo,
"REFUND_PROCESSING"
);
});
}
定时任务只拉取等待确认的数据:
async function runRefundSyncJob(): Promise<void> {
const refunds = await database.refunds.findNeedSync({
statuses: [
"REFUND_PENDING",
"REFUND_PROCESSING",
"REFUND_UNKNOWN",
],
limit: 200,
});
for (const refund of refunds) {
try {
await syncRefund(refund);
} catch (error) {
await database.refunds.recordSyncFailure({
refundNo: refund.refundNo,
errorMessage:
error instanceof Error ? error.message : String(error),
});
}
}
}
如何发现直接在 B 站后台发起的退款
这是需要重点兜底的场景。由于自有系统中没有对应的退款单,只查询“处理中退款”无法发现这类操作。具体处理方式取决于平台提供的能力。
使用账单或退款明细对账
如果平台提供交易账单、退款账单或资金明细,可以定时下载增量数据,再通过平台订单号关联本地订单。
当平台记录的退款金额大于本地已记录的退款金额时:
- 创建一条来源为
PLATFORM_MANUAL的补录退款记录; - 保存平台退款单号和退款金额;
- 以幂等方式更新订单的累计退款金额;
- 将差异写入对账记录;
- 对无法自动匹配的数据触发告警。
对于后台人工退款,这是较为可靠的常见处理方式。
查询近期订单
如果平台没有可用账单,只能通过订单或退款查询接口补漏,但仍不必扫描全部历史订单。可以维护一个滑动时间窗口,只查询近期支付成功且仍处于退款有效期内的订单,并用游标保存扫描进度。
这种方式会带来查询延迟和接口调用量增加的问题,需要配合限流、失败重试和指数退避。
限制后台退款权限
如果平台既不发送退款通知,也没有账单或退款列表接口,技术上就无法完整发现后台退款。此时只能通过流程控制:
- 收紧 B 站后台的退款权限;
- 要求所有退款都从自有系统发起;
- 在后台紧急退款后,必须到自有系统补录;
- 由财务定期进行人工账单对账;
- 发现状态差异时发送告警。
注意事项
- 不要只根据
TRADE_CLOSED判断退款成功,还应核对退款金额或退款单状态。 - 正确处理部分退款、多次退款和退款失败。
- 金额应以最小货币单位的整数保存,例如“分”,以免产生浮点数误差。
- 应为平台订单号、商户退款单号和平台退款单号建立唯一约束。
- 通知、主动查询和账单对账可能同时发现同一笔退款,因此更新逻辑必须幂等。
- 不要因为一次查询超时就把退款标记为失败。网络异常通常应记录为
REFUND_UNKNOWN,之后再重试。 - 发现对账差异后不要静默覆盖数据,应保留原始状态、差异金额和处理记录。
- 查询频率必须符合 B 站接口的限流规则。如果文档没有明确说明,应向平台技术支持确认退款通知、退款查询和账单能力。
合理的方案不是每天轮询每一笔订单,而是统一退款入口,查询处理中的退款,用增量账单对账,并对异常情况告警。如果任何接口或账单都无法识别后台退款,就必须依靠权限和操作流程,避免退款绕过自有系统。
备注:内容仅供参考。
