/// 支付编排服务:7 种支付渠道的统一入口 /// /// 统一原则(与 docs 一致): /// - 客户端回调结果仅做 UI 提示 /// - 最终支付状态一律以 `GET /v1/payment/{payment_id}` 后端查询为准 /// - Mock 模式(无真实商户密钥)下,微信 Native 走扫码弹窗内「模拟支付成功」, /// 网页跳转渠道走「模拟支付成功(Mock)」按钮,实现端到端演示 library; import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; import 'package:url_launcher/url_launcher.dart'; import '../models/payment.dart'; import '../pages/wechat_qr_dialog.dart'; import '../utils/dialog.dart'; import '../utils/pay_result.dart'; import '../utils/platform.dart'; import '../utils/toast.dart'; import 'api_service.dart'; import 'platform/alipay_interface.dart'; import 'platform/stripe_flow.dart'; import 'platform/wechat_flow.dart'; /// 支付编排服务(单例) class PayService { PayService._() { // 注册微信支付结果回调(App 启动后即可接收) WechatFlow.instance.onPayResult = _onWechatResult; } static final PayService instance = PayService._(); final APIServer _api = APIServer(); /// 微信 App 支付结果等待器(pay 拉起后阻塞直到 SDK 回调) Completer? _wechatCompleter; /// 微信 SDK 回调:errCode 0=成功 / -1=失败 / -2=取消 void _onWechatResult(int errCode, String errStr) { final c = _wechatCompleter; if (c != null && !c.isCompleted) c.complete(errCode); } /// 打开外部链接(网页支付跳转) Future _launchUrl(String url) async { final ok = await launchUrl( Uri.parse(url), mode: LaunchMode.externalApplication, ); if (!ok) showErrorMessage('无法打开支付页面:$url'); } /// 最终状态确认:以后端查询为准(最多轮询约 12s 覆盖回调延迟), /// 成功后弹出「支付结果」对话框展示订单号与渠道,回到商品页。 Future _queryFinalStatus( BuildContext context, String paymentId, { required String channelName, }) async { for (int i = 0; i < 12; i++) { try { final status = await _api.queryPaymentStatus(paymentId); if (status.success) { if (context.mounted) { await showPayResultDialog( context, success: true, paymentId: paymentId, channelName: channelName, note: status.note, ); } return; } if ((status.note ?? '').contains('关闭')) { showErrorMessage('订单已关闭:${status.note}'); return; } } catch (_) { // 网络抖动重试 } await Future.delayed(const Duration(seconds: 1)); } // 轮询超时仍未入账:提示用户稍后查询 showErrorMessage('支付结果确认中,请稍后在订单中查询'); } /// 网页跳转支付确认弹窗 /// /// 用于支付宝 WAP / 收银台跳转类渠道: /// - 打开支付页:真实模式,用户完成付款后回来 /// - 模拟支付成功(Mock):演示环境直接走通 Future _handleWebRedirect({ required BuildContext context, required String url, required String paymentId, required String channelName, }) async { final choice = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('网页支付'), content: SingleChildScrollView( child: Text('请在支付页面完成付款。\n\n支付单号:$paymentId\n支付地址:$url'), ), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop('open'), child: const Text('打开支付页'), ), TextButton( onPressed: () => Navigator.of(ctx).pop('simulate'), child: const Text('模拟支付成功(Mock)'), ), TextButton( onPressed: () => Navigator.of(ctx).pop('cancel'), child: const Text('取消'), ), ], ), ); switch (choice) { case 'open': await _launchUrl(url); if (!context.mounted) return; await _queryFinalStatus(context, paymentId, channelName: channelName); case 'simulate': startLoading(status: '模拟支付中...'); try { await _api.simulatePaymentSuccess(paymentId); stopLoading(); showSuccessMessage('模拟支付成功'); if (!context.mounted) return; await _queryFinalStatus(context, paymentId, channelName: channelName); } on Exception catch (e) { stopLoading(); showErrorMessage(_api.resolveError(e)); } default: return; } } // ===== 1. 微信支付 ===== /// 微信支付 /// - 移动端:App 支付(fluwx 拉起微信) /// - Web/桌面:Native 扫码(二维码弹窗轮询) Future payWithWechat( BuildContext context, { required String productId, }) async { startLoading(status: '正在发起微信支付...'); try { final created = await _api.createWechatPayment( productId: productId, source: PlatformTool.isMobile ? 'app' : 'web', ); stopLoading(); if (PlatformTool.isMobile) { // ---- App 支付 ---- final installed = await WechatFlow.instance.isInstalled(); if (!installed) { showErrorMessage('未安装微信,请安装后重试'); return; } final completer = _wechatCompleter = Completer(); final invoked = await WechatFlow.instance.payWith(created); if (!invoked) { _wechatCompleter = null; showErrorMessage('拉起微信失败'); return; } // 等待微信 SDK 回调(超时哨兵值 -999,避免永远阻塞) final errCode = await completer.future .timeout(const Duration(seconds: 20), onTimeout: () => -999); _wechatCompleter = null; switch (errCode) { case WechatPayResultCode.success: showSuccessMessage('支付成功'); case WechatPayResultCode.cancelled: showErrorMessage('已取消支付'); case -999: showErrorMessage('支付结果确认中,请稍后查询'); default: showErrorMessage('支付失败($errCode)'); } // 最终以后端状态为准 if (!context.mounted) return; await _queryFinalStatus(context, created.paymentId, channelName: '微信支付'); } else { // ---- Web/桌面:Native 扫码 ---- final codeUrl = created.codeUrl; if (codeUrl == null || codeUrl.isEmpty) { showErrorMessage('未获取到支付二维码'); return; } if (!context.mounted) return; await openDialog( context, builder: (_) => WechatQrDialog( codeUrl: codeUrl, paymentId: created.paymentId, onClose: (success) { // 扫码弹窗关闭后回到商品页,弹出支付结果(失败时给出提示) if (!success) { showErrorMessage('订单已关闭或未完成支付'); } }, ), ); } } on Exception catch (e) { stopLoading(); showErrorMessage(_api.resolveError(e)); } } // ===== 2. 支付宝支付 ===== /// 支付宝支付 /// - 移动端:App 支付(alipay_kit 拉起支付宝) /// - Web:手机网站支付(后端返回 WAP 跳转链接) Future payWithAlipay( BuildContext context, { required String productId, }) async { startLoading(status: '正在发起支付宝支付...'); try { final created = await _api.createAlipayPayment( productId: productId, source: PlatformTool.isMobile ? 'app' : 'web', ); stopLoading(); if (PlatformTool.isMobile) { // ---- App 支付 ---- final result = await AlipayFlow.instance.pay(created.params); if (result.isSuccessful) { showSuccessMessage('支付成功'); } else if (result.isCancelled) { showErrorMessage('已取消支付'); } else { showErrorMessage('支付失败:${result.memo ?? '未知错误'}'); } // 最终以后端状态为准 if (!context.mounted) return; await _queryFinalStatus(context, created.paymentId, channelName: '支付宝支付'); } else { // ---- Web:WAP 跳转 ---- final url = created.redirectUrl; if (url == null || url.isEmpty) { showErrorMessage('未获取到支付跳转链接'); return; } if (!context.mounted) return; await _handleWebRedirect( context: context, url: url, paymentId: created.paymentId, channelName: '支付宝支付', ); } } on Exception catch (e) { stopLoading(); showErrorMessage(_api.resolveError(e)); } } // ===== 3. 银联云闪付 ===== /// 云闪付支付 /// - 移动端:App 支付(后端返回 tn,拉起云闪付 App) /// - Web/桌面:手机网站跳转(银联收银台) Future payWithUnionPay( BuildContext context, { required String productId, }) async { startLoading(status: '正在发起云闪付支付...'); try { final created = await _api.createUnionPayPayment( productId: productId, source: PlatformTool.isMobile ? 'app' : 'web', ); stopLoading(); if (PlatformTool.isMobile) { // ---- App 支付:tn 是调起云闪付 App 的唯一凭证 ---- final tn = created.tn; if (tn == null || tn.isEmpty) { showErrorMessage('未获取到云闪付交易流水号'); return; } // Mock 模式:tn 以 mock_ 开头,不拉起 App,直接模拟支付成功 if (tn.startsWith('mock_')) { startLoading(status: '模拟支付中...'); await _api.simulatePaymentSuccess(created.paymentId); stopLoading(); showSuccessMessage('模拟云闪付支付成功'); if (!context.mounted) return; await _queryFinalStatus(context, created.paymentId, channelName: '云闪付'); return; } // 真实模式:通过 uppay:// scheme 调起云闪付 App // (正式接入推荐集成银联官方 SDK 拉起,此处用 scheme 演示) final invoked = await launchUrl( Uri.parse('uppay://sdkpay?tn=$tn'), mode: LaunchMode.externalApplication, ); if (!invoked) { showErrorMessage('拉起云闪付失败,请确认已安装云闪付 App'); return; } // 用户支付完成后回到 App,轮询后端确认最终状态 if (!context.mounted) return; await _queryFinalStatus(context, created.paymentId, channelName: '云闪付'); } else { // ---- Web:银联收银台跳转 ---- final url = created.redirectUrl; if (url == null || url.isEmpty) { showErrorMessage('未获取到云闪付跳转链接'); return; } if (!context.mounted) return; await _handleWebRedirect( context: context, url: url, paymentId: created.paymentId, channelName: '云闪付', ); } } on Exception catch (e) { stopLoading(); showErrorMessage(_api.resolveError(e)); } } // ===== 5. Stripe PaymentSheet ===== /// Stripe 支付(PaymentSheet 卡片支付) Future payWithStripe( BuildContext context, { required String productId, }) async { startLoading(status: '正在创建 Stripe 支付...'); try { final created = await _api.createStripePayment(productId: productId); stopLoading(); // Mock 模式(pk_test_mock):不弹原生面板,直接模拟成功 if (StripeFlow.isMock(created)) { startLoading(status: '模拟支付中...'); await _api.simulatePaymentSuccess(created.paymentId); stopLoading(); showSuccessMessage('模拟 Stripe 支付成功'); return; } // 真实模式:弹出 PaymentSheet await StripeFlow.instance.presentPaymentSheet(created); if (!context.mounted) return; await _queryFinalStatus(context, created.paymentId, channelName: 'Stripe 支付'); } on StripeException catch (e) { stopLoading(); final cancelled = e.error.code == FailureCode.Canceled; showErrorMessage( cancelled ? '已取消支付' : '支付失败:${e.error.localizedMessage}', ); } on Exception catch (e) { stopLoading(); showErrorMessage(_api.resolveError(e)); } } // ===== 6. Apple Pay ===== /// Apple Pay(经 Stripe 确认) Future payWithApple( BuildContext context, { required PaymentProduct product, }) async { startLoading(status: '正在创建 Apple Pay...'); try { final created = await _api.createApplePayment(productId: product.id); stopLoading(); if (StripeFlow.isMock(created)) { startLoading(status: '模拟支付中...'); await _api.simulatePaymentSuccess(created.paymentId); stopLoading(); showSuccessMessage('模拟 Apple Pay 成功'); return; } if (!await StripeFlow.instance.isApplePaySupported()) { showErrorMessage('当前设备不支持 Apple Pay'); return; } await StripeFlow.instance.confirmApplePay( created, product.name, product.retailPriceUSD, ); if (!context.mounted) return; await _queryFinalStatus(context, created.paymentId, channelName: 'Apple Pay'); } on StripeException catch (e) { stopLoading(); showErrorMessage('Apple Pay 失败:${e.error.localizedMessage}'); } on Exception catch (e) { stopLoading(); showErrorMessage(_api.resolveError(e)); } } // ===== 7. Google Pay ===== /// Google Pay(经 Stripe 确认) Future payWithGoogle( BuildContext context, { required PaymentProduct product, }) async { startLoading(status: '正在创建 Google Pay...'); try { final created = await _api.createGooglePayment(productId: product.id); stopLoading(); if (StripeFlow.isMock(created)) { startLoading(status: '模拟支付中...'); await _api.simulatePaymentSuccess(created.paymentId); stopLoading(); showSuccessMessage('模拟 Google Pay 成功'); return; } if (!await StripeFlow.instance.isGooglePaySupported()) { showErrorMessage('当前设备不支持 Google Pay'); return; } await StripeFlow.instance.confirmGooglePay( created, product.name, product.retailPriceUSD, ); if (!context.mounted) return; await _queryFinalStatus(context, created.paymentId, channelName: 'Google Pay'); } on StripeException catch (e) { stopLoading(); showErrorMessage('Google Pay 失败:${e.error.localizedMessage}'); } on Exception catch (e) { stopLoading(); showErrorMessage(_api.resolveError(e)); } } // ===== 8. 网页收银台 ===== /// 网页收银台(聚合支付) /// - wechat:Native 扫码弹窗轮询 /// - alipay / stripe:跳转 + 确认 Future payWithCheckout( BuildContext context, { required String channel, required String productId, }) async { startLoading(status: '正在创建收银台会话...'); try { final session = await _api.createCheckoutSession( channel: channel, productId: productId, ); stopLoading(); if (channel == 'wechat') { final codeUrl = session.codeUrl; if (codeUrl == null || codeUrl.isEmpty) { showErrorMessage('未获取到支付二维码'); return; } if (!context.mounted) return; await openDialog( context, builder: (_) => WechatQrDialog( codeUrl: codeUrl, paymentId: session.paymentId, onClose: (success) { // 扫码弹窗关闭后回到商品页,失败时给出提示 if (!success) { showErrorMessage('订单已关闭或未完成支付'); } }, ), ); } else { final url = session.redirectUrl; if (url == null || url.isEmpty) { showErrorMessage('未获取到支付跳转链接'); return; } if (!context.mounted) return; // 收银台渠道展示名(与渠道选择弹窗一致) final displayName = switch (channel) { 'wechat' => '微信支付', 'alipay' => '支付宝支付', _ => 'Stripe 支付', }; await _handleWebRedirect( context: context, url: url, paymentId: session.paymentId, channelName: displayName, ); } } on Exception catch (e) { stopLoading(); showErrorMessage(_api.resolveError(e)); } } }