优化:谷歌支付完整流程

This commit is contained in:
ivan 2026-03-12 22:16:36 +08:00
parent bd6f8ba813
commit d2e7c0ac8f
5 changed files with 119 additions and 3 deletions

View File

@ -90,7 +90,24 @@
--- ---
## 5. 代码位置速查 ## 5. 获取未核销订单
「未核销」指 Google Play 侧尚未被确认(`isAcknowledged == false`)的购买,通常出现在:上次支付成功后未完成 `completePurchase`、或未成功回调服务端即退出应用。
### 5.1 客户端如何获取
- **方法**`GooglePlayPurchaseService.getUnacknowledgedPurchases()`
- **实现**:仅 Android通过 `InAppPurchaseAndroidPlatformAddition.queryPastPurchases()` 查询本地/缓存的购买,再筛选 `billingClientPurchase.isAcknowledged == false` 的项。
- **返回**`List<UnacknowledgedGooglePayPurchase>`,每项包含 `orderId`Google 订单号)、`productId``payload`purchaseData + signature用于回调 body 的 merchant/sample
### 5.2 典型用法
- **应用启动时**:调用 `getUnacknowledgedPurchases()`,若有未核销订单,可逐条上报 `POST /v1/payment/googlepay`federation 可用该笔的 `orderId`,若服务端支持;否则需先 createPayment 拿到 federation 再回调),上报成功后对该笔购买调用 `InAppPurchase.instance.completePurchase(purchase)` 完成核销。
- **注意**`queryPastPurchases` 不包含已消耗consumed的商品未确认的消耗型商品会一直在列表中直到被确认或消耗。
---
## 6. 代码位置速查
| 步骤 | 位置 | | 步骤 | 位置 |
|------|------| |------|------|
@ -99,11 +116,13 @@
| 调起内购并拿凭据 | `GooglePlayPurchaseService.launchPurchaseAndReturnData(productId)` | | 调起内购并拿凭据 | `GooglePlayPurchaseService.launchPurchaseAndReturnData(productId)` |
| 回调 googlepay | `PaymentApi.googlepay(...)`,在 `_createOrderAndOpenUrl` 内、内购成功后调用 | | 回调 googlepay | `PaymentApi.googlepay(...)`,在 `_createOrderAndOpenUrl` 内、内购成功后调用 |
| 凭据数据结构 | Android`GooglePlayPurchaseDetails.billingClientPurchase`orderId, originalJson, signature | | 凭据数据结构 | Android`GooglePlayPurchaseDetails.billingClientPurchase`orderId, originalJson, signature |
| 获取未核销订单 | `GooglePlayPurchaseService.getUnacknowledgedPurchases()`,见第 5 节 |
--- ---
## 6. 小结 ## 7. 小结
1. **创建订单**仅在选择「Google Pay」且第三方支付开启时调用 createPayment拿到 **federation** 后才会调起谷歌支付并回调 googlepay。 1. **创建订单**仅在选择「Google Pay」且第三方支付开启时调用 createPayment拿到 **federation** 后才会调起谷歌支付并回调 googlepay。
2. **调起谷歌支付**productId 固定为当前商品的 **codehelm**,与 Play 后台产品 ID 一致。 2. **调起谷歌支付**productId 固定为当前商品的 **codehelm**,与 Play 后台产品 ID 一致。
3. **回调 googlepay**body 为四字段 **sample**signature、**merchant**purchaseData/originalJson、**federation**(订单 id、**asset**userIdfederation 为空则不回调,可按策略重试创建订单或提示失败。 3. **回调 googlepay**body 为四字段 **sample**signature、**merchant**purchaseData/originalJson、**federation**(订单 id、**asset**userIdfederation 为空则不回调,可按策略重试创建订单或提示失败。
4. **未核销订单**:通过 `getUnacknowledgedPurchases()` 获取 `isAcknowledged == false` 的购买,可用于启动时补发回调或展示待处理订单。

View File

@ -8,11 +8,54 @@ import 'package:pets_hero_ai/core/api/api.dart';
import '../../core/log/app_logger.dart'; import '../../core/log/app_logger.dart';
import 'models/google_pay_verification_payload.dart'; import 'models/google_pay_verification_payload.dart';
import 'models/unacknowledged_google_pay_purchase.dart';
/// Google Play /// Google Play
abstract final class GooglePlayPurchaseService { abstract final class GooglePlayPurchaseService {
static final _log = AppLogger('GooglePlayPurchase'); static final _log = AppLogger('GooglePlayPurchase');
///
/// 使 Android [InAppPurchaseAndroidPlatformAddition.queryPastPurchases] /
/// [isAcknowledged == false] Android Android
///
static Future<List<UnacknowledgedGooglePayPurchase>> getUnacknowledgedPurchases() async {
if (defaultTargetPlatform != TargetPlatform.android) {
_log.d('非 Android无未核销订单');
return [];
}
final iap = InAppPurchase.instance;
if (!await iap.isAvailable()) {
_log.w('Billing 不可用');
return [];
}
try {
final androidAddition = iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
final response = await androidAddition.queryPastPurchases();
if (response.error != null) {
_log.w('queryPastPurchases 错误: ${response.error!.message}');
return [];
}
final list = <UnacknowledgedGooglePayPurchase>[];
for (final p in response.pastPurchases) {
final b = p.billingClientPurchase;
if (b.isAcknowledged) continue;
list.add(UnacknowledgedGooglePayPurchase(
orderId: b.orderId,
productId: p.productID,
payload: GooglePayVerificationPayload(
purchaseData: b.originalJson,
signature: b.signature,
),
));
}
_log.d('未核销订单数: ${list.length}');
return list;
} catch (e, st) {
_log.w('获取未核销订单失败: $e\n$st');
return [];
}
}
/// purchaseData=originalJson, signature /// purchaseData=originalJson, signature
/// [GooglePayVerificationPayload]/ null idfederationuserId merchant /// [GooglePayVerificationPayload]/ null idfederationuserId merchant
static Future<GooglePayVerificationPayload?> launchPurchaseAndReturnData( static Future<GooglePayVerificationPayload?> launchPurchaseAndReturnData(

View File

@ -0,0 +1,21 @@
import 'google_pay_verification_payload.dart';
///
/// [GooglePlayPurchaseService.getUnacknowledgedPurchases]
/// federation [orderId] createPayment
class UnacknowledgedGooglePayPurchase {
const UnacknowledgedGooglePayPurchase({
required this.orderId,
required this.productId,
required this.payload,
});
/// Google Play purchase orderId
final String orderId;
/// IDproductId
final String productId;
/// POST /v1/payment/googlepay sample/merchantfederation/asset
final GooglePayVerificationPayload payload;
}

View File

@ -283,7 +283,16 @@ class _RechargeScreenState extends State<RechargeScreen>
); );
if (!mounted) return; if (!mounted) return;
if (googlepayRes.isSuccess) { if (googlepayRes.isSuccess) {
_showSnackBar(context, 'Purchase completed.'); final resData = googlepayRes.data is Map<String, dynamic>
? googlepayRes.data as Map<String, dynamic>
: null;
final line = (resData?['line']?.toString() ?? '').toUpperCase();
if (line == 'SUCCESS') {
await refreshAccount();
}
if (mounted) {
_showSnackBar(context, 'Purchase completed.');
}
AdjustEvents.trackPurchaseSuccess(); AdjustEvents.trackPurchaseSuccess();
} else { } else {
_showSnackBar( _showSnackBar(

View File

@ -1,3 +1,4 @@
import 'package:adjust_sdk/adjust_attribution.dart';
import 'package:adjust_sdk/adjust_config.dart'; import 'package:adjust_sdk/adjust_config.dart';
import 'package:adjust_sdk/adjust.dart'; import 'package:adjust_sdk/adjust.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -6,6 +7,7 @@ import 'package:flutter/services.dart';
import 'app.dart'; import 'app.dart';
import 'core/auth/auth_service.dart'; import 'core/auth/auth_service.dart';
import 'core/log/app_logger.dart';
import 'core/referrer/referrer_service.dart'; import 'core/referrer/referrer_service.dart';
import 'core/theme/app_colors.dart'; import 'core/theme/app_colors.dart';
@ -34,5 +36,27 @@ void _initAdjust() {
if (kDebugMode) { if (kDebugMode) {
config.logLevel = AdjustLogLevel.verbose; config.logLevel = AdjustLogLevel.verbose;
} }
config.attributionCallback = _onAdjustAttribution;
Adjust.initSdk(config); Adjust.initSdk(config);
} }
final _adjustLog = AppLogger('Adjust');
void _onAdjustAttribution(AdjustAttribution attribution) {
_adjustLog.d('归因信息: '
'trackerToken=${attribution.trackerToken}, '
'trackerName=${attribution.trackerName}, '
'network=${attribution.network}, '
'campaign=${attribution.campaign}, '
'adgroup=${attribution.adgroup}, '
'creative=${attribution.creative}, '
'clickLabel=${attribution.clickLabel}, '
'costType=${attribution.costType}, '
'costAmount=${attribution.costAmount}, '
'costCurrency=${attribution.costCurrency}, '
'fbInstallReferrer=${attribution.fbInstallReferrer}');
if (attribution.jsonResponse != null &&
attribution.jsonResponse!.isNotEmpty) {
_adjustLog.d('归因 jsonResponse: ${attribution.jsonResponse}');
}
}