PAYATHON 2026

如何同步商户在B站后台发起的退款状态?

支付小周

结论

如果 B 站支付接口在 TRADE_CLOSED 等退款场景中不发送异步通知,业务系统就无法仅凭支付回调实时发现后台退款。但也没有必要每天遍历全部历史订单。

一般可以这样处理:

  1. 所有退款尽量统一从自有系统发起,避免直接在 B 站后台操作。
  2. 分别保存退款请求和处理结果,再通过退款查询接口确认最终状态。
  3. 对后台人工退款等异常路径,用增量轮询和定期对账补漏。
  4. 如果 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 站后台发起的退款

这是需要重点兜底的场景。由于自有系统中没有对应的退款单,只查询“处理中退款”无法发现这类操作。具体处理方式取决于平台提供的能力。

使用账单或退款明细对账

如果平台提供交易账单、退款账单或资金明细,可以定时下载增量数据,再通过平台订单号关联本地订单。

当平台记录的退款金额大于本地已记录的退款金额时:

  1. 创建一条来源为 PLATFORM_MANUAL 的补录退款记录;
  2. 保存平台退款单号和退款金额;
  3. 以幂等方式更新订单的累计退款金额;
  4. 将差异写入对账记录;
  5. 对无法自动匹配的数据触发告警。

对于后台人工退款,这是较为可靠的常见处理方式。

查询近期订单

如果平台没有可用账单,只能通过订单或退款查询接口补漏,但仍不必扫描全部历史订单。可以维护一个滑动时间窗口,只查询近期支付成功且仍处于退款有效期内的订单,并用游标保存扫描进度。

这种方式会带来查询延迟和接口调用量增加的问题,需要配合限流、失败重试和指数退避。

限制后台退款权限

如果平台既不发送退款通知,也没有账单或退款列表接口,技术上就无法完整发现后台退款。此时只能通过流程控制:

  • 收紧 B 站后台的退款权限;
  • 要求所有退款都从自有系统发起;
  • 在后台紧急退款后,必须到自有系统补录;
  • 由财务定期进行人工账单对账;
  • 发现状态差异时发送告警。

注意事项

  • 不要只根据 TRADE_CLOSED 判断退款成功,还应核对退款金额或退款单状态。
  • 正确处理部分退款、多次退款和退款失败。
  • 金额应以最小货币单位的整数保存,例如“分”,以免产生浮点数误差。
  • 应为平台订单号、商户退款单号和平台退款单号建立唯一约束。
  • 通知、主动查询和账单对账可能同时发现同一笔退款,因此更新逻辑必须幂等。
  • 不要因为一次查询超时就把退款标记为失败。网络异常通常应记录为 REFUND_UNKNOWN,之后再重试。
  • 发现对账差异后不要静默覆盖数据,应保留原始状态、差异金额和处理记录。
  • 查询频率必须符合 B 站接口的限流规则。如果文档没有明确说明,应向平台技术支持确认退款通知、退款查询和账单能力。

合理的方案不是每天轮询每一笔订单,而是统一退款入口,查询处理中的退款,用增量账单对账,并对异常情况告警。如果任何接口或账单都无法识别后台退款,就必须依靠权限和操作流程,避免退款绕过自有系统。

备注:内容仅供参考。