平台能力与原生互通篇(3/6):支付模块实战:微信/支付宝/苹果内购链路

7 阅读6分钟

支付模块实战:微信/支付宝/苹果内购链路(Flutter 全流程落地)

适用场景:虚拟币充值、会员订阅、道具购买、打赏礼物
技术关键词:Flutter MethodChannel 支付宝 微信支付 Apple IAP 服务端验签


1. 问题背景:业务场景 + 现象

在 Flutter 里做支付,最容易踩坑的不是“调不起 SDK”,而是链路不一致

  • 微信、支付宝是“客户端拉起 + 服务端回调确认”
  • 苹果内购是“客户端发起购买 + App Store 回执 + 服务端验票”
  • 业务又要求“统一订单状态”“统一失败提示”“统一补单机制”

典型线上现象:

  • 用户支付成功了,但前端没收到成功态,页面还在“支付中”
  • iOS 沙盒环境和正式环境回执混淆,验票失败
  • Android 渠道包签名、包名、AppID 不匹配,微信无法拉起
  • 重复回调导致重复发货(最严重)

2. 原因分析:核心原理 + 排查过程

核心原理(一定先统一认知)

支付必须拆成两层状态:

  1. 支付通道状态(SDK返回、回调事件)
  2. 业务订单状态(以服务端为准:待支付/支付中/已支付/关闭)

结论:客户端“成功”不等于业务成功,最终以服务端异步通知 + 主动查询为准。

常见问题根因

  • 客户端直接把 SDK resultStatus == 9000 当最终成功(支付宝)
  • 没做订单幂等,重复通知重复发货
  • 苹果内购没做 pendingCompletePurchase 收尾,导致重复交易
  • 没有“轮询兜底”,只有回调,没有 query 接口确认
  • 失败码无统一映射,业务层到处 if-else

推荐排查顺序

  1. 查订单号在服务端状态机流转日志
  2. 查第三方回调日志(微信/支付宝/苹果验票)
  3. 查客户端日志(拉起参数、返回码、超时)
  4. 查补单任务(定时任务是否扫到)

3. 解决方案:方案对比 + 最终选择

方案对比

  • 方案A:三套支付逻辑分散在页面中

    • 优点:上手快
    • 缺点:维护灾难,无法统一补单与埋点
  • 方案B:统一 PaymentService + 各渠道 Adapter(推荐)

    • 优点:链路一致、可测试、可插拔扩展新渠道
    • 缺点:初期抽象成本略高

最终选择(生产可用)

采用 PaymentService + Adapter + OrderStateMachine

  • PaymentService:编排流程(创建订单 -> 拉起支付 -> 确认结果 -> 轮询兜底)
  • PayAdapter:微信/支付宝/苹果内购各自实现
  • OrderRepository:只与服务端订单接口交互
  • PaymentResultMapper:统一错误码与文案
  • ReconcileJob:补单(App前台触发 + 服务端定时任务)

4. 关键代码:最小必要代码片段(详细示例)

下面代码可直接作为你项目里的骨架,按包名替换。

4.1 统一支付渠道与结果模型(Flutter)

enum PayChannel { wechat, alipay, appleIap }

enum PayBizStatus {
  success,
  pending, // 等待服务端确认
  failed,
  cancelled,
}

class PayResult {
  final PayBizStatus status;
  final String orderNo;
  final String? message;
  final String? channelTradeNo;

  const PayResult({
    required this.status,
    required this.orderNo,
    this.message,
    this.channelTradeNo,
  });
}

4.2 统一 Adapter 接口

abstract class PayAdapter {
  Future<PayResult> pay({
    required String orderNo,
    required Map<String, dynamic> payParams, // 服务端下发的拉起参数
  });
}

4.3 支付宝 Adapter(示意)

class AliPayAdapter implements PayAdapter {
  @override
  Future<PayResult> pay({
    required String orderNo,
    required Map<String, dynamic> payParams,
  }) async {
    // payParams["orderInfo"] 由服务端签名后返回
    final orderInfo = payParams['orderInfo'] as String;
    // 调用三方插件,这里仅示意
    final result = await AlipayPlugin.pay(orderInfo);

    // 常见状态:9000成功,6001取消,8000处理中
    final code = result['resultStatus']?.toString();
    if (code == '9000') {
      return PayResult(status: PayBizStatus.pending, orderNo: orderNo);
    }
    if (code == '6001') {
      return PayResult(status: PayBizStatus.cancelled, orderNo: orderNo, message: '用户取消支付');
    }
    if (code == '8000') {
      return PayResult(status: PayBizStatus.pending, orderNo: orderNo, message: '支付结果确认中');
    }
    return PayResult(status: PayBizStatus.failed, orderNo: orderNo, message: '支付宝支付失败($code)');
  }
}

4.4 微信 Adapter(示意)

class WechatPayAdapter implements PayAdapter {
  @override
  Future<PayResult> pay({
    required String orderNo,
    required Map<String, dynamic> payParams,
  }) async {
    // payParams 由服务端统一下发 prepay 参数
    final ok = await WechatPlugin.pay(
      appId: payParams['appId'],
      partnerId: payParams['partnerId'],
      prepayId: payParams['prepayId'],
      packageValue: payParams['package'],
      nonceStr: payParams['nonceStr'],
      timeStamp: payParams['timeStamp'],
      sign: payParams['sign'],
    );

    if (!ok) {
      return PayResult(status: PayBizStatus.failed, orderNo: orderNo, message: '微信拉起失败');
    }

    // 微信回调常见是异步事件,先返回 pending,后续统一查单确认
    return PayResult(status: PayBizStatus.pending, orderNo: orderNo);
  }
}

4.5 Apple IAP Adapter(in_app_purchase 核心示例)

class AppleIapAdapter implements PayAdapter {
  final InAppPurchase _iap = InAppPurchase.instance;

  @override
  Future<PayResult> pay({
    required String orderNo,
    required Map<String, dynamic> payParams,
  }) async {
    final productId = payParams['productId'] as String;

    final available = await _iap.isAvailable();
    if (!available) {
      return PayResult(status: PayBizStatus.failed, orderNo: orderNo, message: 'App Store 不可用');
    }

    final resp = await _iap.queryProductDetails({productId});
    if (resp.productDetails.isEmpty) {
      return PayResult(status: PayBizStatus.failed, orderNo: orderNo, message: '商品不存在');
    }

    final product = resp.productDetails.first;
    final purchaseParam = PurchaseParam(productDetails: product);

    final ok = await _iap.buyConsumable(purchaseParam: purchaseParam, autoConsume: true);
    if (!ok) {
      return PayResult(status: PayBizStatus.failed, orderNo: orderNo, message: '发起内购失败');
    }

    // 真实成功要等 purchaseStream + 服务端验票
    return PayResult(status: PayBizStatus.pending, orderNo: orderNo);
  }
}

4.6 IAP 回执上送与完成交易(关键防坑)

class AppleIapListener {
  final InAppPurchase _iap = InAppPurchase.instance;
  late final StreamSubscription<List<PurchaseDetails>> _sub;

  void start({
    required Future<void> Function({
      required String transactionId,
      required String receiptData,
    }) uploadReceipt,
  }) {
    _sub = _iap.purchaseStream.listen((purchases) async {
      for (final p in purchases) {
        if (p.status == PurchaseStatus.purchased || p.status == PurchaseStatus.restored) {
          final receipt = p.verificationData.serverVerificationData;
          await uploadReceipt(
            transactionId: p.purchaseID ?? '',
            receiptData: receipt,
          );
        }

        // 不 complete 会重复回调,造成重复处理
        if (p.pendingCompletePurchase) {
          await _iap.completePurchase(p);
        }
      }
    });
  }

  Future<void> dispose() => _sub.cancel();
}

4.7 PaymentService 编排全链路(推荐核心)

class PaymentService {
  PaymentService({
    required this.orderRepo,
    required this.adapterFactory,
  });

  final OrderRepository orderRepo;
  final PayAdapter Function(PayChannel channel) adapterFactory;

  Future<PayResult> startPay({
    required PayChannel channel,
    required String skuId,
    required int amountFen,
  }) async {
    // 1) 服务端创建业务订单
    final order = await orderRepo.createOrder(
      channel: channel.name,
      skuId: skuId,
      amountFen: amountFen,
    );

    // 2) 拿服务端下发的签名参数拉起支付
    final adapter = adapterFactory(channel);
    final payResult = await adapter.pay(
      orderNo: order.orderNo,
      payParams: order.payParams,
    );

    // 3) 任何“看似成功”先进入 pending,再向服务端查单确认
    if (payResult.status == PayBizStatus.pending) {
      final finalStatus = await orderRepo.pollOrderFinalStatus(
        orderNo: order.orderNo,
        timeout: const Duration(seconds: 12),
      );
      return PayResult(
        status: finalStatus ? PayBizStatus.success : PayBizStatus.failed,
        orderNo: order.orderNo,
        message: finalStatus ? '支付成功' : '支付确认超时,请稍后在订单页查看',
      );
    }

    return payResult;
  }
}

4.8 Android/iOS MethodChannel(当三方插件不满足时)

Flutter 侧
class NativePayChannel {
  static const _channel = MethodChannel('com.yourapp/pay');

  static Future<Map<dynamic, dynamic>> invokeAliPay(String orderInfo) async {
    final result = await _channel.invokeMethod('aliPay', {'orderInfo': orderInfo});
    return result as Map<dynamic, dynamic>;
  }

  static Future<Map<dynamic, dynamic>> invokeWxPay(Map<String, dynamic> params) async {
    final result = await _channel.invokeMethod('wxPay', params);
    return result as Map<dynamic, dynamic>;
  }
}
Android 侧(Kotlin,示意)
class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.yourapp/pay"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "aliPay" -> {
                        val orderInfo = call.argument<String>("orderInfo") ?: ""
                        // TODO: 调支付宝SDK
                        result.success(mapOf("code" to "9000", "msg" to "success"))
                    }
                    "wxPay" -> {
                        // TODO: 调微信SDK
                        result.success(mapOf("code" to "0", "msg" to "ok"))
                    }
                    else -> result.notImplemented()
                }
            }
    }
}
iOS 侧(Swift,示意)
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller = window?.rootViewController as! FlutterViewController
    let channel = FlutterMethodChannel(name: "com.yourapp/pay", binaryMessenger: controller.binaryMessenger)

    channel.setMethodCallHandler { call, result in
      if call.method == "aliPay" {
        // TODO: 调支付宝SDK
        result(["code": "9000", "msg": "success"])
      } else if call.method == "wxPay" {
        // TODO: 调微信SDK
        result(["code": "0", "msg": "ok"])
      } else {
        result(FlutterMethodNotImplemented)
      }
    }

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

5. 效果验证:数据/截图/日志

建议你在文章里给出这 4 类验证数据(最有说服力):

  • 支付成功率:改造前后(例如 92.1% -> 97.8%)
  • “已扣款未到账”工单量:按周对比下降趋势
  • 补单命中率pending -> success 的恢复比例
  • 重复发货率:幂等改造后趋近 0

日志示例建议统一字段:

pay_start orderNo=... channel=wechat amount=6800
pay_sdk_callback orderNo=... status=pending code=0
pay_query_final orderNo=... final=success source=server_notify

6. 可复用结论:通用经验 + 避坑清单

通用经验

  • 永远把“支付成功”定义为:服务端确认成功
  • 客户端职责是“拉起 + 展示 + 兜底查询”,不是最终判定者
  • 支付能力必须平台化:统一入口 + 统一状态 + 统一埋点 + 统一补单

避坑清单(高频)

  • 订单号是否全局唯一,且服务端做幂等
  • 微信/支付宝参数是否完全由服务端签名下发
  • iOS 内购是否处理 pendingCompletePurchase
  • 回调失败时是否有主动查单轮询
  • 沙盒/正式环境验票地址是否自动切换
  • 取消支付是否与失败文案区分
  • 是否有补单入口(启动时、订单页、定时任务)

架构图说明(文字版)后期补充图文版

Flutter UI -> PaymentService -> PayAdapter(WeChat/AliPay/AppleIAP) -> SDK/Store
Flutter UI -> OrderRepository -> 业务服务端 -> 第三方回调 -> 服务端确认 -> Flutter 查单/推送刷新