修复:补单功能bug修复
This commit is contained in:
parent
b55faf0db5
commit
f14474a39f
@ -180,7 +180,7 @@ abstract final class PaymentApi {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取订单详情(query 使用逻辑字段 `id` 表示订单/支付 ID)
|
/// 获取订单详情(query 使用**逻辑字段** `id` 表示订单/支付 ID,经 [FieldMapping] 映射为线网 `timing`)
|
||||||
static Future<EntityResponse<OrderDetailResponse>> getOrderDetail({
|
static Future<EntityResponse<OrderDetailResponse>> getOrderDetail({
|
||||||
required String userId,
|
required String userId,
|
||||||
required String orderId,
|
required String orderId,
|
||||||
@ -199,7 +199,8 @@ abstract final class PaymentApi {
|
|||||||
/// Google 支付结果回调
|
/// Google 支付结果回调
|
||||||
///
|
///
|
||||||
/// **Query**:`app`、`userId`。
|
/// **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<EntityResponse<GooglePayCallbackResponse>> googlepay({
|
static Future<EntityResponse<GooglePayCallbackResponse>> googlepay({
|
||||||
required String signature,
|
required String signature,
|
||||||
required String purchaseData,
|
required String purchaseData,
|
||||||
|
|||||||
@ -61,9 +61,8 @@ abstract final class NativeIapCoordinator {
|
|||||||
|
|
||||||
if (!createRes.isSuccess || createRes.data == null) {
|
if (!createRes.isSuccess || createRes.data == null) {
|
||||||
sink.onPaymentSettled(PaymentSettlement.failure(
|
sink.onPaymentSettled(PaymentSettlement.failure(
|
||||||
message: createRes.msg.isNotEmpty
|
message:
|
||||||
? createRes.msg
|
createRes.msg.isNotEmpty ? createRes.msg : 'createPayment failed',
|
||||||
: 'createPayment failed',
|
|
||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -76,8 +75,7 @@ abstract final class NativeIapCoordinator {
|
|||||||
app: app,
|
app: app,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sink.onPaymentSettled(
|
sink.onPaymentSettled(PaymentSettlement.failure(message: e.toString()));
|
||||||
PaymentSettlement.failure(message: e.toString()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,8 +116,7 @@ abstract final class NativeIapCoordinator {
|
|||||||
app: app,
|
app: app,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sink.onPaymentSettled(
|
sink.onPaymentSettled(PaymentSettlement.failure(message: e.toString()));
|
||||||
PaymentSettlement.failure(message: e.toString()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +131,7 @@ abstract final class NativeIapCoordinator {
|
|||||||
|
|
||||||
final purchase = await PaymentService.launchPurchaseAndReturnData(
|
final purchase = await PaymentService.launchPurchaseAndReturnData(
|
||||||
storeProductId,
|
storeProductId,
|
||||||
|
idFromCreatePayment: serverFederation,
|
||||||
);
|
);
|
||||||
if (purchase == null) {
|
if (purchase == null) {
|
||||||
sink.onPaymentSettled(PaymentSettlement.cancelled(
|
sink.onPaymentSettled(PaymentSettlement.cancelled(
|
||||||
@ -142,16 +140,11 @@ abstract final class NativeIapCoordinator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final federation = (serverFederation != null &&
|
final federation = (serverFederation != null && serverFederation.isNotEmpty)
|
||||||
serverFederation.isNotEmpty)
|
|
||||||
? serverFederation
|
? serverFederation
|
||||||
: purchase.orderId;
|
: purchase.orderId;
|
||||||
if (serverFederation != null && serverFederation.isNotEmpty) {
|
|
||||||
await PaymentService.saveFederationForGoogleOrderId(
|
// 映射已在 [PaymentService.launchPurchaseAndReturnData] 收到 Play 回调后、返回前写入。
|
||||||
purchase.orderId,
|
|
||||||
serverFederation,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final googlepayRes = await PaymentApi.googlepay(
|
final googlepayRes = await PaymentApi.googlepay(
|
||||||
signature: purchase.payload.signature,
|
signature: purchase.payload.signature,
|
||||||
@ -181,9 +174,7 @@ abstract final class NativeIapCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await PaymentService.completeAndConsumePurchase(purchase.purchaseDetails);
|
await PaymentService.completeAndConsumePurchase(purchase.purchaseDetails);
|
||||||
if (serverFederation != null && serverFederation.isNotEmpty) {
|
await PaymentService.removeFederationForGoogleOrderId(purchase.orderId);
|
||||||
await PaymentService.removeFederationForGoogleOrderId(purchase.orderId);
|
|
||||||
}
|
|
||||||
|
|
||||||
sink.onPaymentSettled(PaymentSettlement.success(
|
sink.onPaymentSettled(PaymentSettlement.success(
|
||||||
orderId: federation,
|
orderId: federation,
|
||||||
|
|||||||
@ -60,8 +60,7 @@ class PaymentService {
|
|||||||
|
|
||||||
static final _log = AppLogger('PaymentService');
|
static final _log = AppLogger('PaymentService');
|
||||||
|
|
||||||
static const String _kFederationMapKey =
|
static const String _kFederationMapKey = 'google_pay_google_order_to_id';
|
||||||
'google_pay_google_order_to_federation';
|
|
||||||
|
|
||||||
static final Map<String, PurchaseDetails> _pendingFromStream = {};
|
static final Map<String, PurchaseDetails> _pendingFromStream = {};
|
||||||
static StreamSubscription<List<PurchaseDetails>>? _pendingStreamSub;
|
static StreamSubscription<List<PurchaseDetails>>? _pendingStreamSub;
|
||||||
@ -92,8 +91,9 @@ class PaymentService {
|
|||||||
_log.d('Subscribed to purchaseStream');
|
_log.d('Subscribed to purchaseStream');
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> saveFederationForGoogleOrderId(
|
static Future<void> saveIdForGoogleOrderId(
|
||||||
String googleOrderId, String federation) async {
|
String googleOrderId, String federation) async {
|
||||||
|
_log.d('保存订单信息: googleOrderId=$googleOrderId, federation=$federation');
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final json = prefs.getString(_kFederationMapKey);
|
final json = prefs.getString(_kFederationMapKey);
|
||||||
@ -108,8 +108,7 @@ class PaymentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<String?> getFederationForGoogleOrderId(
|
static Future<String?> getIdForGoogleOrderId(String googleOrderId) async {
|
||||||
String googleOrderId) async {
|
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final json = prefs.getString(_kFederationMapKey);
|
final json = prefs.getString(_kFederationMapKey);
|
||||||
@ -200,21 +199,36 @@ class PaymentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 先 **await** [InAppPurchase.completePurchase](Android 上为 acknowledge),再 [consumePurchase]。
|
||||||
|
/// 若未等待 acknowledge 完成即 consume,易与 Billing 竞态,出现 `BillingResponse.error`。
|
||||||
static Future<bool> completeAndConsumePurchase(
|
static Future<bool> completeAndConsumePurchase(
|
||||||
PurchaseDetails purchaseDetails) async {
|
PurchaseDetails purchaseDetails) async {
|
||||||
final iap = InAppPurchase.instance;
|
final iap = InAppPurchase.instance;
|
||||||
try {
|
try {
|
||||||
iap.completePurchase(purchaseDetails);
|
await iap.completePurchase(purchaseDetails);
|
||||||
_log.d('completePurchase executed');
|
_log.d('completePurchase finished (awaited)');
|
||||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||||
final androidAddition =
|
final androidAddition =
|
||||||
iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
iap.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
|
||||||
final result = await androidAddition.consumePurchase(purchaseDetails);
|
var result = await androidAddition.consumePurchase(purchaseDetails);
|
||||||
final ok = result.responseCode == BillingResponse.ok;
|
var ok = result.responseCode == BillingResponse.ok;
|
||||||
|
if (!ok) {
|
||||||
|
_log.w(
|
||||||
|
'consumePurchase failed: ${result.responseCode} '
|
||||||
|
'${result.debugMessage ?? ''}',
|
||||||
|
);
|
||||||
|
await Future<void>.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) {
|
if (ok) {
|
||||||
_log.d('consumePurchase executed');
|
_log.d('consumePurchase executed');
|
||||||
} else {
|
|
||||||
_log.w('consumePurchase failed: ${result.responseCode}');
|
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
@ -241,29 +255,43 @@ class PaymentService {
|
|||||||
|
|
||||||
_log.d('Order recovery: ${pending.length} pending');
|
_log.d('Order recovery: ${pending.length} pending');
|
||||||
bool needRefresh = false;
|
bool needRefresh = false;
|
||||||
final iap = InAppPurchase.instance;
|
|
||||||
|
|
||||||
for (final p in pending) {
|
for (final p in pending) {
|
||||||
try {
|
try {
|
||||||
final federation = await getFederationForGoogleOrderId(p.orderId);
|
// 与 app_client [GooglePlayPurchaseService.runOrderRecovery] 一致:
|
||||||
if (federation != null && federation.isNotEmpty) {
|
// 有本地「Google orderId → 建单时用于 googlepay 的 id」映射则先调服务端再核销;
|
||||||
|
// 无映射(如进程被杀在落库前)仅 complete+consume,避免永久「已拥有此内容」阻塞再次购买。
|
||||||
|
final id = await getIdForGoogleOrderId(p.orderId);
|
||||||
|
if (id != null && id.isNotEmpty) {
|
||||||
final res = await onPaymentCallback(
|
final res = await onPaymentCallback(
|
||||||
federation,
|
id,
|
||||||
p.payload.signature,
|
p.payload.signature,
|
||||||
p.payload.purchaseData,
|
p.payload.purchaseData,
|
||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
if (res.isSuccess) {
|
if (res.isSuccess) {
|
||||||
if (await completeAndConsumePurchase(p.purchaseDetails)) {
|
final consumed =
|
||||||
|
await completeAndConsumePurchase(p.purchaseDetails);
|
||||||
|
if (consumed) {
|
||||||
_pendingFromStream.remove(p.orderId);
|
_pendingFromStream.remove(p.orderId);
|
||||||
await removeFederationForGoogleOrderId(p.orderId);
|
await removeFederationForGoogleOrderId(p.orderId);
|
||||||
needRefresh = true;
|
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 {
|
} else {
|
||||||
_log.w('Order recovery failed: ${p.orderId}, ${res.msg}');
|
_log.w('Order recovery failed: ${p.orderId}, ${res.msg}');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
_log.d(
|
||||||
|
'Order recovery: no id map, consume only orderId=${p.orderId}',
|
||||||
|
);
|
||||||
if (await completeAndConsumePurchase(p.purchaseDetails)) {
|
if (await completeAndConsumePurchase(p.purchaseDetails)) {
|
||||||
_pendingFromStream.remove(p.orderId);
|
_pendingFromStream.remove(p.orderId);
|
||||||
needRefresh = true;
|
needRefresh = true;
|
||||||
@ -276,8 +304,13 @@ class PaymentService {
|
|||||||
return needRefresh;
|
return needRefresh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [idFromCreatePayment]:建单接口返回的 federation / 业务订单 id。
|
||||||
|
/// 若提供,会在收到 Play 的 `orderId` 后 **立即** 落库映射(在 [Completer.complete] 之前),
|
||||||
|
/// 降低用户支付后立刻杀进程导致补单无映射的概率。
|
||||||
static Future<GooglePayPurchaseResult?> launchPurchaseAndReturnData(
|
static Future<GooglePayPurchaseResult?> launchPurchaseAndReturnData(
|
||||||
String productId) async {
|
String productId, {
|
||||||
|
String? idFromCreatePayment,
|
||||||
|
}) async {
|
||||||
_log.d('Purchase request for productId: "$productId"');
|
_log.d('Purchase request for productId: "$productId"');
|
||||||
if (defaultTargetPlatform != TargetPlatform.android) {
|
if (defaultTargetPlatform != TargetPlatform.android) {
|
||||||
return null;
|
return null;
|
||||||
@ -297,13 +330,18 @@ class PaymentService {
|
|||||||
StreamSubscription<List<PurchaseDetails>>? sub;
|
StreamSubscription<List<PurchaseDetails>>? sub;
|
||||||
|
|
||||||
sub = iap.purchaseStream.listen(
|
sub = iap.purchaseStream.listen(
|
||||||
(purchases) {
|
(purchases) async {
|
||||||
for (final p in purchases) {
|
for (final p in purchases) {
|
||||||
if (p.productID != productId) continue;
|
if (p.productID != productId) continue;
|
||||||
if (p.status == PurchaseStatus.purchased ||
|
if (p.status == PurchaseStatus.purchased ||
|
||||||
p.status == PurchaseStatus.restored) {
|
p.status == PurchaseStatus.restored) {
|
||||||
if (!completer.isCompleted && p is GooglePlayPurchaseDetails) {
|
if (!completer.isCompleted && p is GooglePlayPurchaseDetails) {
|
||||||
final b = p.billingClientPurchase;
|
final b = p.billingClientPurchase;
|
||||||
|
final federation = (idFromCreatePayment != null &&
|
||||||
|
idFromCreatePayment.isNotEmpty)
|
||||||
|
? idFromCreatePayment
|
||||||
|
: b.orderId;
|
||||||
|
await saveIdForGoogleOrderId(b.orderId, federation);
|
||||||
_pendingFromStream[b.orderId] = p;
|
_pendingFromStream[b.orderId] = p;
|
||||||
completer.complete(GooglePayPurchaseResult(
|
completer.complete(GooglePayPurchaseResult(
|
||||||
orderId: b.orderId,
|
orderId: b.orderId,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user