PAYATHON 2026

如何在 Flutter Web 中集成第三方支付并获取回调?

支付小周

结论

Flutter Web 不能直接监听第三方支付页面内部的事件。要获取支付进度和结果,需要根据支付网关提供的能力选择相应方式:

  • 使用 JavaScript SDK 时,通过 Dart/JavaScript interop 接收 SDK 回调。
  • 使用 iframe 或弹窗时,通过 window.postMessage() 接收消息,但支付网关必须明确支持这种方式。
  • 使用页面跳转支付时,通过 return_url 返回 Flutter Web,再从 URL 中读取订单信息。
  • 最终支付结果必须由服务端接收 webhook,并通过验签或调用支付网关接口查询确认。

浏览器回调适合更新页面状态,不能作为发货、充值或开通服务的唯一依据。

为什么 Flutter Web 不能直接获得支付页面事件

第三方支付页面通常位于其他域名。受浏览器同源策略限制,Flutter Web 无法读取该页面的 DOM、JavaScript 状态和网络请求。

即使 Payment UI 已经正常显示,Flutter 的点击事件和组件状态也无法判断用户是否完成支付。业务系统只能通过支付网关提供的 SDK callback、页面跳转、postMessage 或服务端 webhook 获知结果。

完整流程通常是:

  1. Flutter Web 请求业务服务端创建支付订单。
  2. 服务端使用支付平台密钥创建支付会话。
  3. Flutter Web 打开支付 SDK、iframe、弹窗或支付页面。
  4. 浏览器回调用于显示“支付处理中”等状态。
  5. 支付平台向业务服务端发送 webhook。
  6. 服务端验签并更新订单。
  7. Flutter Web 查询业务服务端,获取最终订单状态。

通过 JavaScript SDK 接收回调

如果支付网关提供 Web JavaScript SDK,可以先在 web/index.html 中封装 SDK,再通过 dart:js_interop 把结果传给 Flutter。

以下代码中的 PaymentGateway、open 和回调名称都是示例,必须替换成实际支付网关提供的 API。

<script src="https://example-payment.com/sdk.js"></script>

<script>
  window.openPayment = function (paymentOptions, callback) {
    const options = JSON.parse(paymentOptions);

    PaymentGateway.open({
      orderId: options.orderId,

      onOpen: function () {
        callback(JSON.stringify({
          type: "opened"
        }));
      },

      onSuccess: function (result) {
        callback(JSON.stringify({
          type: "success",
          paymentId: result.paymentId,
          orderId: result.orderId
        }));
      },

      onFailure: function (error) {
        callback(JSON.stringify({
          type: "failure",
          message: error.message
        }));
      },

      onClose: function () {
        callback(JSON.stringify({
          type: "closed"
        }));
      }
    });
  };
</script>

在 Flutter Web 中声明这个 JavaScript 方法:

import 'dart:convert';
import 'dart:js_interop';

@JS('openPayment')
external void openPayment(JSString options, JSFunction callback);

void startPayment(String orderId) {
  final options = jsonEncode({
    'orderId': orderId,
  });

  openPayment(
    options.toJS,
    ((JSString payload) {
      final result =
          jsonDecode(payload.toDart) as Map<String, dynamic>;

      switch (result['type']) {
        case 'opened':
          print('支付窗口已打开');
          break;

        case 'success':
          print('浏览器端收到支付成功通知');
          verifyOrderWithBackend(orderId);
          break;

        case 'failure':
          print('支付失败:${result['message']}');
          break;

        case 'closed':
          print('用户关闭了支付窗口');
          break;
      }
    }).toJS,
  );
}

Future<void> verifyOrderWithBackend(String orderId) async {
  // 向业务服务端查询订单状态。
  // 不要仅根据 JavaScript 的 success 回调直接发货。
}

这里的 success 只说明 SDK 在浏览器端报告支付成功。收到回调后,Flutter Web 仍需向自己的服务端查询订单状态。

通过 iframe 或弹窗接收消息

如果支付页面运行在 iframe 或新窗口中,只有支付网关明确支持 window.postMessage(),父页面才能收到消息。

父页面可以监听 message:

<script>
  window.addEventListener("message", function (event) {
    if (event.origin !== "https://secure.example-payment.com") {
      return;
    }

    const message = event.data;

    if (message && message.type === "payment_result") {
      window.dispatchEvent(
        new CustomEvent("payment-result", {
          detail: JSON.stringify(message)
        })
      );
    }
  });
</script>

必须严格验证 event.origin,不能像下面这样无条件接收消息:

// 不安全:任何页面都可以伪造支付消息
window.addEventListener("message", function (event) {
  handlePaymentResult(event.data);
});

如果支付网关没有实现 postMessage(),业务页面就无法自行读取跨域 iframe 中的支付结果。此时需要改用该网关的 JavaScript SDK、跳转回调或服务端 webhook。

通过 return_url 处理页面跳转

部分支付网关会在用户完成或取消支付后,把浏览器跳转到创建支付时指定的 return_url,例如:

https://shop.example.com/payment/result?orderId=ORDER_123

Flutter Web 可以读取当前 URL:

void handlePaymentReturn() {
  final uri = Uri.base;
  final orderId = uri.queryParameters['orderId'];

  if (orderId == null || orderId.isEmpty) {
    return;
  }

  verifyOrderWithBackend(orderId);
}

不要直接信任这类参数:

?status=success

用户可以修改 URL 参数。返回页面应该只使用 orderId 等业务标识向服务端查询,不能直接把订单改为已支付。

如果项目使用 hash 路由,还要确认查询参数的具体位置。例如:

https://shop.example.com/#/payment/result?orderId=ORDER_123

此时参数可能位于 Uri.base.fragment 中,需要按照项目实际使用的路由配置解析。

服务端 webhook 才是最终依据

可靠的支付集成必须接入服务端通知。典型的处理逻辑如下:

app.post(
  "/payment/webhook",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const signature = req.headers["x-payment-signature"];
    const rawBody = req.body;

    const valid = verifyGatewaySignature(rawBody, signature);

    if (!valid) {
      return res.status(400).send("invalid signature");
    }

    const event = JSON.parse(rawBody.toString("utf8"));

    if (event.type === "payment.succeeded") {
      await markOrderPaidIdempotently(
        event.data.orderId,
        event.data.paymentId
      );
    }

    res.status(200).send("ok");
  }
);

x-payment-signature、事件结构和验签算法只是占位示例,实际实现必须以所用支付网关的官方文档为准。

处理 webhook 时还要满足以下要求:

  • 使用原始请求体验签,避免 JSON 解析改变请求内容。
  • 校验订单号、金额、币种和商户身份。
  • 做好幂等处理,防止重复通知造成重复发货。
  • 保存支付平台的交易号和事件号。
  • 只有通过验证的成功事件才能改变订单状态。
  • webhook 地址必须是支付平台可以访问的 HTTPS 服务端地址,不能指向 Flutter 页面。

推荐实现

如果所用支付网关没有特殊限制,可以采用以下方案:

Flutter Web
    │
    ├── 请求服务端创建订单
    │
    ├── 打开支付 SDK 或支付页面
    │
    └── 根据 orderId 查询订单状态
             ▲
             │
业务服务端 ──┼── 创建支付会话
             ├── 接收并验证 webhook
             └── 保存最终支付状态

浏览器端 callback 用于改善交互体验,比如显示支付窗口已经打开、用户已经取消或系统正在确认。服务端 webhook 则负责确认真实的支付结果。

注意事项

  • 不要把商户密钥、签名密钥或私钥放入 Flutter Web。用户可能查看 Web 构建产物中的所有内容。
  • 不要把“支付弹窗关闭”直接判定为支付失败。用户可能已经完成支付,只是在回调到达前关闭了页面。
  • 不要根据 SDK 的 success 回调直接发货。
  • 不要尝试读取跨域 iframe 的 DOM,浏览器同源策略会阻止这种操作。
  • CORS 只能控制跨域 HTTP 请求,不能让 Flutter Web 读取第三方 iframe 的内部状态。
  • 支付成功页可以短时间轮询服务端的订单状态,但需要设置轮询间隔、超时和最大重试次数。
  • 如果支付网关同时提供前端 callback 和 webhook,应同时接入,因为两者用途不同。
  • 回调名称、签名方式和事件字段由所选支付网关决定,不能直接在不同平台之间套用。

备注:内容仅供参考。