支付模块实战:微信/支付宝/苹果内购链路(Flutter 全流程落地)
适用场景:虚拟币充值、会员订阅、道具购买、打赏礼物
技术关键词:FlutterMethodChannel支付宝微信支付Apple IAP服务端验签
1. 问题背景:业务场景 + 现象
在 Flutter 里做支付,最容易踩坑的不是“调不起 SDK”,而是链路不一致:
- 微信、支付宝是“客户端拉起 + 服务端回调确认”
- 苹果内购是“客户端发起购买 + App Store 回执 + 服务端验票”
- 业务又要求“统一订单状态”“统一失败提示”“统一补单机制”
典型线上现象:
- 用户支付成功了,但前端没收到成功态,页面还在“支付中”
- iOS 沙盒环境和正式环境回执混淆,验票失败
- Android 渠道包签名、包名、AppID 不匹配,微信无法拉起
- 重复回调导致重复发货(最严重)
2. 原因分析:核心原理 + 排查过程
核心原理(一定先统一认知)
支付必须拆成两层状态:
- 支付通道状态(SDK返回、回调事件)
- 业务订单状态(以服务端为准:待支付/支付中/已支付/关闭)
结论:客户端“成功”不等于业务成功,最终以服务端异步通知 + 主动查询为准。
常见问题根因
- 客户端直接把
SDK resultStatus == 9000当最终成功(支付宝) - 没做订单幂等,重复通知重复发货
- 苹果内购没做
pendingCompletePurchase收尾,导致重复交易 - 没有“轮询兜底”,只有回调,没有 query 接口确认
- 失败码无统一映射,业务层到处
if-else
推荐排查顺序
- 查订单号在服务端状态机流转日志
- 查第三方回调日志(微信/支付宝/苹果验票)
- 查客户端日志(拉起参数、返回码、超时)
- 查补单任务(定时任务是否扫到)
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 查单/推送刷新