<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                微信支付 不得不說我們這一代程序員是幸運的,得益于國內移動支付的迅猛發展,微信支付的流程閉環比iOS完善了N倍(iOS的槽點一篇文章都寫不完,稍后我再來吐);同時微信官方所提供的服務,至少在國內網絡中,可以認定為是百分百可靠的。 微信支付的流程相對簡單: 客戶端向業務后臺發起一個購買請求 業務后臺到微信服務端生成一個訂單 將微信訂單信息和自身系統所需的業務數據整合后返回給客戶端 客戶端拿到微信支付信息后,通過WeChatOpensdk調起支付 在頁面中訂閱支付回調,接受支付信息并做業務流程處理**(如:進入支付結果頁等流程)** 最后請求后臺,由后臺主動去微信系統中查詢最終支付狀態,交回給前端顯示結果。 (ps:后端在微信系統中主動查詢訂單轉態是同步的,可以馬上拿到支付結果) 接下來講講開發,Flutter使用的是fluwx插件,簡單易用。在項目中,我對微信支付進行了封裝,代碼見下: ``` import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:fluwx/fluwx.dart' as fluwx; class WechatPayment { StreamSubscription _wxPay; /// 關閉微信消息訂閱 void wxSubscriptionClose() => _wxPay?.cancel(); /// 調起微信支付面板 /// 這里的WxPayModel是業務層的數據,即后臺返回的有關微信支付訂單的信息 void wxPay(WxPayModel wxPayModel, {VoidCallback onWxPaying, VoidCallback onSuccess, Function(String data) onError}) async { // 跳轉微信支付前,告訴頁面進入微信支付,頁面層可以做一些關閉加載框等的操作 onWxPaying?.call(); // 一些異常情況的處理 if (!await fluwx.isWeChatInstalled) return onError?.call('請安裝微信完成支付或使用蘋果手機支付'); if (wxPayModel.appId != Config.WX_APP_ID) return onError?.call('AppID不一致,請聯系管理員'); // 此方法筆者沒有做單例,因此支付前嘗試注銷監聽,避免重復回調 _wxPay?.cancel(); // 支付回調 _wxPay = fluwx.weChatResponseEventHandler.listen((event) { _wxPay?.cancel(); if (event is fluwx.WeChatPaymentResponse) { if (event.isSuccessful) { return onSuccess?.call(); } else { return onError?.call(event.errCode == -1 ? '系統錯誤,請聯系管理員' : '您取消了支付'); } } }); // 發起支付 fluwx.payWithWeChat( appId: wxPayModel.appId, partnerId: wxPayModel.partnerId, prepayId: wxPayModel.prepayId, packageValue: wxPayModel.packageValue, nonceStr: wxPayModel.nonceStr, timeStamp: wxPayModel.timeStamp, sign: wxPayModel.sign, signType: wxPayModel.signType, extData: wxPayModel.extData, ); } } ``` 頁面端是這樣調用的 WechatPayment paymentUtils = new WechatPayment(); paymentUtils.wxPay( state.model.wxPayModel, onError: (String err) { if (!mounted) return; // 微信支付錯誤,設置支付狀態為false,彈框即可 _isPaying = false; SchedulerBinding.instance.addPostFrameCallback((_) { CommonUtils.showToast(err, backgroundColor: Theme.of(context).errorColor); }); }, onSuccess:(){ _isPaying = true; }, onWxPaying: () { // 啟動微信支付,設置支付狀態為true,關閉加載框 _isPaying = true; SchedulerBinding.instance.addPostFrameCallback((_) { Navigator.pop(context); }); }, ); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 但是需要注意,微信的回調是異步的,并且有很多種情況是接收不到回調的,以下是確定收不到會調的情況。 微信調起支付頁面時,其實是跳轉到新的應用,對于我們的應用而言是觸發了前后臺切換的生命周期。 因此在檢測到應用返回前臺,并且支付狀態還在進行中時,可以證明是收不到微信的支付狀態回調,需要特殊處理下。 收不到的情況有: // ① 彈出支付框后使用系統返回鍵關閉; // ② 進入微信支付密碼框后不輸入使用系統導航切回app或者系統返回鍵返回; // ③ 進入微信后直接返回桌面再回到應用; // ④ 彈出支付框后鎖屏再開屏; // ⑤ 彈出支付款后下拉任務欄; // ⑥ 輸入密碼成功后,直接返回桌面或者使用系統導航或者使用返回鍵返回app // ⑦ 退出微信登錄,進行支付后直接登錄微信,在登錄過程中回到app // ⑧ 在系統應用管理中雙開微信后,調起支付后不點擊任一個微信端,而是點擊取消 1 2 3 4 5 6 7 8 9 10 11 12 13 現在主流的做法是再支付頁面監聽app的生命周期,即由后臺切回前臺的時候,檢測下狀態,若還在支付中,直接進入查詢結果頁面,由后臺去檢驗訂單,拿到結果顯示即可。(后臺主動查詢理論上還是存在微信服務端延時的問題,因此后臺進行查詢的時候,建議采取輪詢機制,若是沒有支付成功的話,延時5秒后再確認下更保險) class _XXXPageState extends State<XXXPage> with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); //添加觀察者 } @override void dispose() { WidgetsBinding.instance.removeObserver(this); //銷毀觀察者 super.dispose(); } /// 應用狀態監聽 @override void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.resumed: { if (Platform.isAndroid && _isPaying) { _isPaying = false; // 監聽到時安卓設備并且支付還在進行中,程序員要根據業務做一下處理 break; } default: break; } super.didChangeAppLifecycleState(state); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 到此,微信支付很愉快的解決了,以上代碼是抽象出來的工具類,可以直接使用;但是不涉及任何業務流程的開發,這個需要使用者自己去補充。 綜上,微信支付流程主線可簡單粗暴總結為:服務端生成訂單 → 客戶端調起支付 → 客戶端通知服務端核驗訂單 → 客戶端拿到最終結果 → 客戶端final支付。 整個過程形成閉環,有理有據,數據都由后端去操作安全合理。(最重點是前端工作量簡直不要太少)。 可是,iOS就不一樣了,簡直不要太惡心! iOS IAP應用內支付 IAP,即in-app Purchase,蘋果推出的App內購買虛擬商品的方式,基于AppStore賬戶的支付方式。由于iOS整個體系都是基于自己的一套系統的(不像上面的微信支付,是第三方支付平臺),因此在開發之前,我們需要到Apple開發者中心完成以下步驟: 1. 簽署協議和銀行業務 2. 在后臺創建App內購買項目,這里所有的價格都是Apple規定好的,我們只有選擇的資格,沒辦法自定價格。創建完成后,每個項目會有sku和productId 3. 添加沙盒測試員Apple 以上步驟參考內容引自站內大神:Geniune 支付流程:應用通過sku向服務端獲取商品列表 → 列表中取出對應產品請求支付 → 進入appStore支付 → 頁面監聽支付回調拿到驗證票據 → 業務后臺拿到應用接收到的票據后去Apple官網進行校驗即可。 流程很簡單,簡單到幾乎不用跟業務后臺打交代,但是坑卻隨之而來: ① 支付數據完全依賴前端應用,很難跟業務后臺的訂單系統一一對應; ② 針對①的問題,IAP支付支持傳遞skPayment對象,里面的applicationUsername經常用來保存系統的OrderId; 但是應用支付成功后收到的回調中,applicationUsername卻偶爾會出現為null的情況,沒有了對應關系,就沒辦法核銷業務系統中的訂單從而為用戶充值; ③ iOS支付回調非常不穩定,有時延遲嚴重;且沒有任何注定查詢的方法; ④ iOS應用內支付有很多異常情況要處理,最常見的就是沒有登錄、沒有同意最新的iOS支付協議等,都會發送給app支付失敗的回調; 但是當用戶登錄或是同意后,iOS系統又會觸發新的支付,導致舊的附帶業務訂單號的支付無效,莫名又多出一個沒有訂單號的新支付; ⑤ 國內網上資料極度缺乏,基本都是19年以前的,Flutter的文章更是少的可憐,可參考性不強。 ⑥ 測試文檔對于中斷購買的測試流程有巨坑,后面菜單一定不要錯過~ 1 2 3 4 5 6 7 8 通過查看文檔和不斷調試,我們發現: ① 支付錯誤的回調,基本能馬上收到; ② 上面流程說到IAP支付需要手動結束支付流程。同時iOS規定不能對同一個skuId重復發起多次支付的,只要當前skuId有沒有final的支付,再次發起都會失敗; ② 無論支付成功或失敗,只要app沒有主動對當前支付進行final,每次啟動app后,app都會收到這個支付信息的通知; ③ 關于applicationUsername,只有在支付完成馬上收到回調的情況下,回調信息才會有這個信息;到②中的情況,肯定不會返回applicationUsername; ④ 沒有applicationUsername就意味著訂單對不上,因此我們需要進行湊單機制。 綜上,我們對異常處理有了確定方案: ① app發起支付后,需要將業務OrderId和skuId進行持久化存儲(即卸載應用都不會刪除的數據); ②只要持久化存儲不為空,啟動app就需要馬上啟動監聽,以接收iOS系統的訂單推送; ③ 支付出錯可以final當前支付,但是支付成功必須明確接收到iOS推送并且后臺核驗成功后,才能final,并刪除持久化存儲。 最終,結合到業務系統和特殊情況的處理后,支付流程應該如下: 業務后臺返回商品列表時,需要附加返回對應的skuId app通過skuId請appStore請求商品信息 app對商品發起支付,并將業務訂單號存儲在applicationUsername中,發起成功寫入持久化存儲,狀態為pending 接收iOS系統回調,失敗馬上final支付,更改對應持久化存儲狀態為cancle;成功拿到票據和業務OrderId發送給后臺 后臺調取Apple服務端接口,傳入票據(票據其實儲存著最新的時間,appStore用戶信息等) 后臺獲取到Apple返回的當前appStore用戶所有支付的前100條記錄,拿到productId到數據庫有中匹配該用戶是否有未核銷的訂單,并對應修改業務訂單狀態 app確認核銷成功,final支付,并且刪除持久化存儲 同時還需要做一些特殊處理: app剛啟動時,若是持久化存儲不為空,需要馬上啟動iOS支付訂閱監聽,以接收iOS對未完成訂單的推送; 由于iOS限制了同一個skuId不能重復發起支付,因此持久化存儲中,一個skuId永遠只會有一條記錄。因此當app接收到的支付推送applicationUsername為null,采取湊單機制,原則是:通過skuId找到存儲記錄,拿到其對應的OrderId,發給后臺核驗。 接下來進入開發,Futter采用的是in_app_purchase插件,官方提供的,支持google和IAP支付;而持久化存儲用的是flutter_secure_storage插件。 依據上面的流程,我同樣封裝了工具類。而且由于可能會在多個地方調用起監聽,所有必須是單例模式,代碼如下: ``` import 'dart:async'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; // iOS支付單一實例 final iOSPayment = IOSPayment(); class IOSPayment { /// 單例模式 static final IOSPayment _iosPayment = IOSPayment.init(); factory IOSPayment() { return _iosPayment; } IOSPayment.init(); // 應用內支付實例 InAppPurchaseConnection purchaseConnection = InAppPurchaseConnection.instance; FlutterSecureStorage storage = new FlutterSecureStorage(); // iOS訂閱監聽 StreamSubscription<List<PurchaseDetails>> subscription; /// 判斷是否可以使用支付 Future<bool> isAvailable() async => await purchaseConnection.isAvailable(); // 開始訂閱 void startSubscription() async { if (subscription != null) return; print('>>> start subscription'); // 支付消息訂閱 Stream purchaseUpdates = purchaseConnection.purchaseUpdatedStream; subscription = purchaseUpdates.listen( (purchaseDetailsList) { purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { if (purchaseDetails.status == PurchaseStatus.pending) { print('>>> pending'); // 業務代碼略:有訂單開始支付,向外部發出通知,并記錄到緩存中; } else { if (purchaseDetails.status == PurchaseStatus.error) { print('>>> error'); // 業務代碼略:有訂單支付錯誤,向外部發出通知 // 下面是刪除 String value = await storage.read(key: purchaseDetails.productID); String orderId = value.split('¥')[0]; writeStorage(purchaseDetails.productID, orderId, 'cancel'); finalTransaction(purchaseDetails); } else if (purchaseDetails.status == PurchaseStatus.purchased) { print('>>> purchased'); String orderId = purchaseDetails.skPaymentTransaction.payment.applicationUsername; if (orderId == null || orderId.isEmpty) { // 如果applicationUsername為空,執行湊單 orderId = await foundRecentOrder(purchaseDetails.productID); } if (orderId.isEmpty) { // 湊單失敗,找不到業務單號,結束 finalTransaction(purchaseDetails); BlocProvider.of<PaymentUtilsBloc>(Application.navigatorState.currentContext).add(IosPayFailureEvent(errorMessage: '支付出錯啦,請稍后再試~')); return; } // 業務代碼略:支付成功,向外部發出通知 // 業務代碼略:開始核驗訂單,核驗結果由外部監聽 ); } } }); }, onDone: () { stopListen(); }, onError: (error) { stopListen(); }, ); } /// 檢查sku是否有對應商品 Future<bool> checkProductBySku(String sku, {Function(String err) onError}) async { if (!await isAvailable()) { onError?.call('無法連接AppStore,請稍后再試'); return false; } ProductDetailsResponse appStoreProducts = await purchaseConnection.queryProductDetails([sku].toSet()); if (appStoreProducts.productDetails.length == 0) { onError?.call('沒有找到相關產品,請聯系管理員'); return false; } return true; } /// 啟動支付 void iosPay(String sku, String orderId, {Function(String err) onError}) async { // 獲取商品列表 ProductDetailsResponse appStoreProducts = await purchaseConnection.queryProductDetails([sku].toSet()); // 發起支付 purchaseConnection .buyNonConsumable( purchaseParam: PurchaseParam( productDetails: appStoreProducts.productDetails.first, applicationUserName: orderId, ), ) .then((value) { if (value) { // 只要能發起,就寫入 writeStorage(sku, orderId, 'pending'); } }).catchError((err) { onError?.call('當前商品您有未完成的交易,請等待iOS系統核驗后再次發起購買。'); print(err); }); } writeStorage(String key, String value, String status) { storage.write(key: key, value: '$value¥$status'); } // 關閉交易 void finalTransaction(PurchaseDetails purchaseDetails) async { await purchaseConnection.completePurchase(purchaseDetails); // 每完成一張訂單進行緩存的清除 if (!await checkStorage()) { stopListen(); } } // 湊單機制 Future<String> foundRecentOrder(String sku) async { String orderId = ''; String values = await storage.read(key: sku); if (values != null) { orderId = values.split('¥')[0]; } return orderId; } // 校驗是否還有緩存 Future<bool> checkStorage() async { Map<String, String> remainingValues = await storage.readAll(); return remainingValues.isNotEmpty; } // 關閉監聽 stopListen() async { subscription?.cancel(); subscription = null; } } ``` 頁面調用時,建議啟用定時器,**因為iOS回調不穩定,所以監聽到應用回到前臺時開始30秒計時;30秒內沒有收到支付回調,需要做對應提示,這一塊也是存業務流程,我這里不做代碼展示。**下面代碼是如何調用上面工具類的: ``` iOSPayment.startSubscription(); iOSPayment.iosPay( state.skuId, state.model.orderId, onError: (String err) { if (!mounted) return; // 支付遇到錯誤,馬上停止定時器,并且關掉彈框 }, ); ``` ``` // 應用啟動時 if (Platform.isIOS && await iOSPayment.checkStorage()) { // 啟動訂閱:支付緩存未清除完畢、機型可使用應用內支付 iOSPayment.startSubscription(needDelayed: true); } ``` 測試IAP中斷購買的測試 這個測試是模擬用戶點擊購買協議的操作,當彈出系統協議彈框時,iOS會發出一個支付錯誤的消息;這個時候我們的代碼會final這個支付,并且將持久化中對應skuId的信息狀態改為cancel; 然后用戶同意后,iOS會再發起一個同樣的不帶OdrerId(是的,被弄丟了。。。。)的訂單,用戶支付成功后,我們的代碼就會收到支付成功的沒有OdrerId的推送,在持久化存儲中執行湊單機制后,再發給后臺核銷。 如何模擬這個流程呢?看看官方文檔描述,下面是譯文: #### 設置測試 通過[登錄App Store Connect](https://help.apple.com/app-store-connect/#/devcd5016d31)啟用對Sandbox Apple ID的中斷購買,然后: 1. 在“用戶和訪問”中,單擊邊欄中沙箱下的“測試器”。在右側,您可以查看您的Sandbox Apple ID。 2. 選擇您要為其啟用中斷購買的Sandbox Apple ID。如果已啟用,則會在“中斷購買”列下看到一個復選標記。 3. 在出現的對話框中,選擇“此測試儀的中斷購買”。 #### 開始測試 ``` 1. 在測試設備上,使用已中斷購買的沙盒Apple ID登錄。 2. 在您的應用中,選擇“購買”或“訂閱”進行應用內購買。 3. 觀察到系統顯示付款單。 4. 在您的代碼中,驗證付款隊列在狀態下是否收到新交易。 5. 在設備上,驗證付款單。 6. 在您的代碼中,觀察到付款失敗。付款隊列在狀態中接收更新的交易。 7. 檢查您的代碼調用是否將其從隊列中刪除。 8. 在設備上,觀察到系統顯示“條款和條件”,從而中斷了購買(因為您已配置了沙盒環境)。 9. 在設備上,點擊以同意條款和條件。 10. 在您的代碼中,驗證付款隊列接收到的新交易處于與失敗交易相同且數量相同的狀態. 11. 在您的代碼中,驗證收據。檢查您的應用是否提供了服務或產品,然后致電。 12. 在設備上,用戶應觀察到購買成功。 ``` 也就是說在Apple后臺把沙盒測試賬號設置為中斷即可。但是無論我怎么同意,收到的還是支付失敗的訂閱。其實是因為文檔寫漏了,中斷后app彈出同意協議彈框,也就是上面第8步,這個時候必須在后臺把中斷測試關了,然后再執行第九步。(就是這么狗血,官方文檔不給力,網上也沒有任何資料,最后還是在官方論壇,看到某個QA的評論才找到的靈感。。。這里也感謝公司大佬花了半天專門找這方面的資料。) ##寫在最后 感謝大家孜孜不倦看到最后,這篇長文希望能幫助開發支付的小伙伴少踩一些坑。 IAP的支付確實是很坑,但如果站在iOS開發者的角度來看。其實也能理解:他們是做手機系統的,他們能保證系統內部的所有支付流程,根本不care開發者的業務邏輯。 但無論如何,這種方式對于開發者,確實是極度不友善的;另外,還有一種流程,app發起支付后,只要有回調就馬上final,成功就發給后臺,由后臺去執行湊單機制,這種對于前端其實更合理,畢竟數據存在客戶端永遠是不夠安全,但是這樣app就有可能對同一個skuId瘋狂發起購買,后臺湊單時,就做不到一一對應。有利有弊吧~~~
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看