From f14474a39f4809e89c699a56e8b91198b2464d68 Mon Sep 17 00:00:00 2001 From: ivan Date: Tue, 14 Apr 2026 12:53:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E8=A1=A5=E5=8D=95?= =?UTF-8?q?=E5=8A=9F=E8=83=BDbug=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/src/services/payment_api.dart | 5 +- .../payment_flow/native_iap_coordinator.dart | 27 +++---- lib/src/services/payment_service.dart | 76 ++++++++++++++----- 3 files changed, 69 insertions(+), 39 deletions(-) diff --git a/lib/src/services/payment_api.dart b/lib/src/services/payment_api.dart index f70f176..b352317 100644 --- a/lib/src/services/payment_api.dart +++ b/lib/src/services/payment_api.dart @@ -180,7 +180,7 @@ abstract final class PaymentApi { ); } - /// 获取订单详情(query 使用逻辑字段 `id` 表示订单/支付 ID) + /// 获取订单详情(query 使用**逻辑字段** `id` 表示订单/支付 ID,经 [FieldMapping] 映射为线网 `timing`) static Future> getOrderDetail({ required String userId, required String orderId, @@ -199,7 +199,8 @@ abstract final class PaymentApi { /// Google 支付结果回调 /// /// **Query**:`app`、`userId`。 - /// **Body**(顺序与文档一致):`signature`、`purchaseData`、`id`、`userId`。 + /// **Body**(**逻辑字段**,与 [skin_config.json] `fieldMapping` 一致):`signature`、`purchaseData`、`id`、`userId`。 + /// 其中 `id` 对应 app_client 请求体里的订单标识(对方字段名常为 `federation`),在本工程映射为线网 V2 名 `timing`。 static Future> googlepay({ required String signature, required String purchaseData, diff --git a/lib/src/services/payment_flow/native_iap_coordinator.dart b/lib/src/services/payment_flow/native_iap_coordinator.dart index fb3115d..eec333c 100644 --- a/lib/src/services/payment_flow/native_iap_coordinator.dart +++ b/lib/src/services/payment_flow/native_iap_coordinator.dart @@ -61,9 +61,8 @@ abstract final class NativeIapCoordinator { if (!createRes.isSuccess || createRes.data == null) { sink.onPaymentSettled(PaymentSettlement.failure( - message: createRes.msg.isNotEmpty - ? createRes.msg - : 'createPayment failed', + message: + createRes.msg.isNotEmpty ? createRes.msg : 'createPayment failed', )); return; } @@ -76,8 +75,7 @@ abstract final class NativeIapCoordinator { app: app, ); } catch (e) { - sink.onPaymentSettled( - PaymentSettlement.failure(message: e.toString())); + sink.onPaymentSettled(PaymentSettlement.failure(message: e.toString())); } } @@ -118,8 +116,7 @@ abstract final class NativeIapCoordinator { app: app, ); } catch (e) { - sink.onPaymentSettled( - PaymentSettlement.failure(message: e.toString())); + sink.onPaymentSettled(PaymentSettlement.failure(message: e.toString())); } } @@ -134,6 +131,7 @@ abstract final class NativeIapCoordinator { final purchase = await PaymentService.launchPurchaseAndReturnData( storeProductId, + idFromCreatePayment: serverFederation, ); if (purchase == null) { sink.onPaymentSettled(PaymentSettlement.cancelled( @@ -142,16 +140,11 @@ abstract final class NativeIapCoordinator { return; } - final federation = (serverFederation != null && - serverFederation.isNotEmpty) + final federation = (serverFederation != null && serverFederation.isNotEmpty) ? serverFederation : purchase.orderId; - if (serverFederation != null && serverFederation.isNotEmpty) { - await PaymentService.saveFederationForGoogleOrderId( - purchase.orderId, - serverFederation, - ); - } + + // 映射已在 [PaymentService.launchPurchaseAndReturnData] 收到 Play 回调后、返回前写入。 final googlepayRes = await PaymentApi.googlepay( signature: purchase.payload.signature, @@ -181,9 +174,7 @@ abstract final class NativeIapCoordinator { } await PaymentService.completeAndConsumePurchase(purchase.purchaseDetails); - if (serverFederation != null && serverFederation.isNotEmpty) { - await PaymentService.removeFederationForGoogleOrderId(purchase.orderId); - } + await PaymentService.removeFederationForGoogleOrderId(purchase.orderId); sink.onPaymentSettled(PaymentSettlement.success( orderId: federation, diff --git a/lib/src/services/payment_service.dart b/lib/src/services/payment_service.dart index 2ee1ec6..73c7261 100644 --- a/lib/src/services/payment_service.dart +++ b/lib/src/services/payment_service.dart @@ -60,8 +60,7 @@ class PaymentService { static final _log = AppLogger('PaymentService'); - static const String _kFederationMapKey = - 'google_pay_google_order_to_federation'; + static const String _kFederationMapKey = 'google_pay_google_order_to_id'; static final Map _pendingFromStream = {}; static StreamSubscription>? _pendingStreamSub; @@ -92,8 +91,9 @@ class PaymentService { _log.d('Subscribed to purchaseStream'); } - static Future saveFederationForGoogleOrderId( + static Future saveIdForGoogleOrderId( String googleOrderId, String federation) async { + _log.d('保存订单信息: googleOrderId=$googleOrderId, federation=$federation'); try { final prefs = await SharedPreferences.getInstance(); final json = prefs.getString(_kFederationMapKey); @@ -108,8 +108,7 @@ class PaymentService { } } - static Future getFederationForGoogleOrderId( - String googleOrderId) async { + static Future getIdForGoogleOrderId(String googleOrderId) async { try { final prefs = await SharedPreferences.getInstance(); final json = prefs.getString(_kFederationMapKey); @@ -200,21 +199,36 @@ class PaymentService { } } + /// 先 **await** [InAppPurchase.completePurchase](Android 上为 acknowledge),再 [consumePurchase]。 + /// 若未等待 acknowledge 完成即 consume,易与 Billing 竞态,出现 `BillingResponse.error`。 static Future completeAndConsumePurchase( PurchaseDetails purchaseDetails) async { final iap = InAppPurchase.instance; try { - iap.completePurchase(purchaseDetails); - _log.d('completePurchase executed'); + await iap.completePurchase(purchaseDetails); + _log.d('completePurchase finished (awaited)'); if (defaultTargetPlatform == TargetPlatform.android) { final androidAddition = iap.getPlatformAddition(); - final result = await androidAddition.consumePurchase(purchaseDetails); - final ok = result.responseCode == BillingResponse.ok; + var result = await androidAddition.consumePurchase(purchaseDetails); + var ok = result.responseCode == BillingResponse.ok; + if (!ok) { + _log.w( + 'consumePurchase failed: ${result.responseCode} ' + '${result.debugMessage ?? ''}', + ); + await Future.delayed(const Duration(milliseconds: 350)); + result = await androidAddition.consumePurchase(purchaseDetails); + ok = result.responseCode == BillingResponse.ok; + if (!ok) { + _log.w( + 'consumePurchase retry failed: ${result.responseCode} ' + '${result.debugMessage ?? ''}', + ); + } + } if (ok) { _log.d('consumePurchase executed'); - } else { - _log.w('consumePurchase failed: ${result.responseCode}'); } return ok; } @@ -241,29 +255,43 @@ class PaymentService { _log.d('Order recovery: ${pending.length} pending'); bool needRefresh = false; - final iap = InAppPurchase.instance; for (final p in pending) { try { - final federation = await getFederationForGoogleOrderId(p.orderId); - if (federation != null && federation.isNotEmpty) { + // 与 app_client [GooglePlayPurchaseService.runOrderRecovery] 一致: + // 有本地「Google orderId → 建单时用于 googlepay 的 id」映射则先调服务端再核销; + // 无映射(如进程被杀在落库前)仅 complete+consume,避免永久「已拥有此内容」阻塞再次购买。 + final id = await getIdForGoogleOrderId(p.orderId); + if (id != null && id.isNotEmpty) { final res = await onPaymentCallback( - federation, + id, p.payload.signature, p.payload.purchaseData, userId, ); if (res.isSuccess) { - if (await completeAndConsumePurchase(p.purchaseDetails)) { + final consumed = + await completeAndConsumePurchase(p.purchaseDetails); + if (consumed) { _pendingFromStream.remove(p.orderId); await removeFederationForGoogleOrderId(p.orderId); needRefresh = true; - _log.d('Order recovery success: ${p.orderId}'); + _log.d('Order recovery success: ${p.orderId} id=$id'); + } else { + // 后端已核销成功时仍刷新本地账户,避免积分不更新;映射保留以便下次重试 consume + needRefresh = true; + _log.w( + 'Order recovery: googlepay ok but Play consume failed ' + 'orderId=${p.orderId}; account refresh still scheduled', + ); } } else { _log.w('Order recovery failed: ${p.orderId}, ${res.msg}'); } } else { + _log.d( + 'Order recovery: no id map, consume only orderId=${p.orderId}', + ); if (await completeAndConsumePurchase(p.purchaseDetails)) { _pendingFromStream.remove(p.orderId); needRefresh = true; @@ -276,8 +304,13 @@ class PaymentService { return needRefresh; } + /// [idFromCreatePayment]:建单接口返回的 federation / 业务订单 id。 + /// 若提供,会在收到 Play 的 `orderId` 后 **立即** 落库映射(在 [Completer.complete] 之前), + /// 降低用户支付后立刻杀进程导致补单无映射的概率。 static Future launchPurchaseAndReturnData( - String productId) async { + String productId, { + String? idFromCreatePayment, + }) async { _log.d('Purchase request for productId: "$productId"'); if (defaultTargetPlatform != TargetPlatform.android) { return null; @@ -297,13 +330,18 @@ class PaymentService { StreamSubscription>? sub; sub = iap.purchaseStream.listen( - (purchases) { + (purchases) async { for (final p in purchases) { if (p.productID != productId) continue; if (p.status == PurchaseStatus.purchased || p.status == PurchaseStatus.restored) { if (!completer.isCompleted && p is GooglePlayPurchaseDetails) { final b = p.billingClientPurchase; + final federation = (idFromCreatePayment != null && + idFromCreatePayment.isNotEmpty) + ? idFromCreatePayment + : b.orderId; + await saveIdForGoogleOrderId(b.orderId, federation); _pendingFromStream[b.orderId] = p; completer.complete(GooglePayPurchaseResult( orderId: b.orderId,