PAYATHON 2026

支付宝 H5 收银台“完成支付”的具体含义是什么?

支付小周

明确结论

这里的“完成支付”,更准确地说,是用户结束了支付宝收银台中的支付操作,进入前台回跳阶段。它并不等于“支付宝已经成功扣款”。

跳转到 return_url,只说明浏览器从支付宝页面返回了商家页面,不能据此确认交易成功。支付失败、交易仍在处理中或出现异常时,都不能只根据是否回跳来判断结果。反过来,即使支付已经成功,用户直接关闭页面、网络中断或回跳被拦截,也可能导致商家收不到 return_url 请求。

最终支付结果应主要以支付宝发送到 notify_url 的服务端异步通知为准。必要时,再调用 alipay.trade.query 主动查询确认。

为什么不能依赖 return_url

return_url 是前台同步跳转地址,主要用于改善用户体验,比如展示“正在确认支付结果”,或者引导用户返回订单详情页。它有几个天然限制:

  • 用户可能在支付后直接关闭支付宝或浏览器,导致回跳没有发生。
  • 浏览器跳转可能因网络、系统 WebView 或页面状态而失败。
  • URL 参数可能被用户修改,未经验证签名不能信任。
  • 收到回跳,并不代表交易状态已经最终确定。
  • 支付失败后是否回跳取决于具体的收银台流程,不能假定所有失败场景都会访问 return_url。

因此,业务系统应把 return_url 理解为“用户回来了”,而不是“钱到账了”。

正确的处理流程

支付结果确认流程可以这样设计:

  1. 创建支付订单时,同时传入 return_url 和 notify_url。
  2. 用户从支付宝回到 return_url 后,前端只展示“支付结果确认中”。
  3. 商家后端验证同步返回参数的签名,但不直接据此发货或充值。
  4. 商家后端等待并处理支付宝发送到 notify_url 的异步通知。
  5. 收到通知后完成验签,并核对订单号、商户身份、金额等业务字段。
  6. 确认交易状态成功后,再把本地订单更新为已支付。
  7. 如果异步通知还没有到达,可以调用 alipay.trade.query 查询交易状态。
  8. 订单状态更新必须具备幂等性,避免重复通知引发重复发货。

三者的职责可以概括为:

return_url:负责页面跳转和用户体验
notify_url:负责服务端支付结果通知
alipay.trade.query:负责主动补查和状态确认

return_url 应如何处理

前台回跳页面不应直接显示“支付成功”,也不能因为参数中出现了 trade_no、out_trade_no 等字段,就修改订单状态。

比较稳妥的做法是让页面调用商家自己的订单查询接口:

async function checkPaymentResult(outTradeNo) {
  const response = await fetch(
    `/api/orders/${encodeURIComponent(outTradeNo)}/payment-status`,
    {
      credentials: "include"
    }
  );

  if (!response.ok) {
    throw new Error("查询订单状态失败");
  }

  const result = await response.json();

  if (result.status === "PAID") {
    window.location.href = `/payment/success?orderNo=${encodeURIComponent(outTradeNo)}`;
    return;
  }

  if (result.status === "CLOSED") {
    window.location.href = `/payment/failed?orderNo=${encodeURIComponent(outTradeNo)}`;
    return;
  }

  // WAITING 表示商家服务端尚未确认最终结果
  document.querySelector("#payment-status").textContent =
    "支付结果确认中,请稍候刷新";
}

这里查询的是商家数据库中保存的订单状态,而不是让浏览器自行认定支付宝交易已经成功。

异步通知处理示例

下面是一个简化的 Java 示例。具体类名和方法签名,应以项目实际使用的支付宝 Java SDK 版本为准。

@PostMapping("/alipay/notify")
@ResponseBody
public String handleAlipayNotify(HttpServletRequest request) {
    Map<String, String> params = new HashMap<>();

    request.getParameterMap().forEach((key, values) -> {
        if (values != null && values.length > 0) {
            params.put(key, String.join(",", values));
        }
    });

    try {
        boolean verified = AlipaySignature.rsaCheckV1(
            params,
            alipayPublicKey,
            charset,
            signType
        );

        if (!verified) {
            return "failure";
        }

        String appId = params.get("app_id");
        String sellerId = params.get("seller_id");
        String outTradeNo = params.get("out_trade_no");
        String tradeNo = params.get("trade_no");
        String tradeStatus = params.get("trade_status");
        String totalAmount = params.get("total_amount");

        // 以下值必须与商家创建订单时保存的数据进行比较
        Order order = orderService.findByOrderNo(outTradeNo);
        if (order == null) {
            return "failure";
        }

        if (!expectedAppId.equals(appId)
                || !expectedSellerId.equals(sellerId)
                || new BigDecimal(totalAmount).compareTo(order.getAmount()) != 0) {
            return "failure";
        }

        if ("TRADE_SUCCESS".equals(tradeStatus)
                || "TRADE_FINISHED".equals(tradeStatus)) {
            // 必须采用幂等更新,重复通知不能重复发货或重复记账
            orderService.markPaidIfUnpaid(outTradeNo, tradeNo);
        } else if ("TRADE_CLOSED".equals(tradeStatus)) {
            orderService.markClosedIfUnpaid(outTradeNo);
        }

        // 只有通知已被正确处理后才返回 success
        return "success";
    } catch (Exception e) {
        return "failure";
    }
}

验签只能确认通知来自可信来源,不能替代业务校验。至少还要核对:

  • app_id 是否属于当前应用;
  • seller_id 是否属于当前收款商户;
  • out_trade_no 是否对应本系统创建的订单;
  • total_amount 是否与订单应付金额一致;
  • 订单是否已经处理,避免重复记账;
  • 通知中的交易状态是否符合当前业务的成功条件。

主动查询支付结果

如果用户已经返回商家页面,但异步通知尚未处理完成,可以由商家服务端调用 alipay.trade.query:

AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
request.setBizContent("""
{
  "out_trade_no": "%s"
}
""".formatted(outTradeNo));

AlipayTradeQueryResponse response = alipayClient.execute(request);

if (!response.isSuccess()) {
    // 查询失败不等于支付失败,应记录错误并稍后重试
    return;
}

String tradeStatus = response.getTradeStatus();

switch (tradeStatus) {
    case "TRADE_SUCCESS":
    case "TRADE_FINISHED":
        orderService.markPaidIfUnpaid(
            outTradeNo,
            response.getTradeNo()
        );
        break;

    case "TRADE_CLOSED":
        orderService.markClosedIfUnpaid(outTradeNo);
        break;

    case "WAIT_BUYER_PAY":
        // 尚未完成付款,保持待支付状态
        break;

    default:
        // 未知状态不要擅自认定成功或失败
        break;
}

常见交易状态可以这样理解:

  • WAIT_BUYER_PAY:等待买家付款;
  • TRADE_SUCCESS:交易支付成功;
  • TRADE_FINISHED:交易结束,通常不能再进行常规退款操作;
  • TRADE_CLOSED:交易关闭。

实际可用的状态及其业务含义,应以当前接入产品和支付宝开放平台文档为准。

需要特别注意的事项

不要把回跳当作成功凭证

下面这些判断都不可靠:

访问了 return_url,所以支付成功
URL 中有 trade_no,所以支付成功
浏览器显示支付宝成功页,所以支付成功
用户截图显示已付款,所以支付成功

发货、充值、开通会员等业务状态,必须由商家服务端判断。

异步通知可能重复到达

支付宝可能重复发送通知,因此商家的处理逻辑必须保证幂等。例如,可以使用条件更新:

UPDATE orders
SET status = 'PAID',
    alipay_trade_no = ?
WHERE order_no = ?
  AND status = 'UNPAID';

只有第一次更新成功时,才能执行后续发货。不能每收到一次通知就重新发货。

查询失败不等于支付失败

调用 alipay.trade.query 时,如果发生超时、网络错误或系统错误,只能说明本次查询没有得到可靠结果,不能立即把订单标记为失败。此时应保留订单原状态,再根据业务策略重试或定时补单。

前台参数也必须验签

如果业务需要读取 return_url 中的参数,应由服务端按照支付宝规则验签。即使验签通过,前台结果也更适合用于页面展示和触发查询,不能取代异步通知或主动查询。

因此,这里的“完成支付”不应被严格理解为“成功扣款”,它描述的是收银台流程结束后的前台回跳行为。商家确认最终支付结果时,应以经过验签和业务校验的异步通知为主,以 alipay.trade.query 作为补充。前端展示和履约操作,则应始终以商家服务端保存的订单状态为依据。

备注:内容仅供参考。