谷歌支付配置及安卓集成2020-11-25

谷歌支付

1.流程

image-20201125151026147.png

2.谷歌后臺(tái)API配置

啟用API

網(wǎng)址:https://play.google.com/console/u/0/developers/6637358084117674478/api-access

image-20201125152618027.png

進(jìn)入谷歌管理后臺(tái)胸竞,當(dāng)未啟動(dòng)時(shí)需要啟動(dòng)對(duì)應(yīng)API舍肠,隨后會(huì)提示去創(chuàng)建服務(wù)賬號(hào)

創(chuàng)建服務(wù)賬號(hào)

網(wǎng)址:https://console.developers.google.com/iam-admin/serviceaccounts/create?project=wewatch-98a41

image-20201125151628369.png

選中應(yīng)用(例如選擇wewatch)>> 輸入服務(wù)賬號(hào)名稱 >> 輸入服務(wù)賬號(hào)ID(這時(shí)候會(huì)有提示 應(yīng)用名-應(yīng)用ID.@appspot.gserviceaccount.com蜂筹,注意核實(shí)應(yīng)用是否一致) >>當(dāng)創(chuàng)建完畢之后會(huì)得到一個(gè)p12文件廉涕,需要提供給后臺(tái),同時(shí)會(huì)需要記錄下對(duì)應(yīng)p12文件的密碼

查看對(duì)應(yīng)用戶以及權(quán)限

網(wǎng)址:https://console.cloud.google.com/iam-admin/iam?project=wewatch-98a41

image-20201125153633202.png

當(dāng)創(chuàng)建成功時(shí)捅暴,可以到IAM中查看對(duì)應(yīng)的賬號(hào)權(quán)限,為Editor權(quán)限

2.開啟消息通知(Cloud Pub/Sub

定義:Cloud Pub/Sub是一種托管式的消息服務(wù),當(dāng)應(yīng)用購買對(duì)于商品成功時(shí)奄侠,就會(huì)去找到對(duì)應(yīng)的谷歌服務(wù)用戶,去通知其狀態(tài)的改變载矿。

開啟Cloud Pub/Sub網(wǎng)址:https://cloud.google.com/pubsub/

點(diǎn)擊轉(zhuǎn)到控制臺(tái)垄潮,會(huì)到達(dá)該網(wǎng)址:https://console.cloud.google.com/cloudpubsub/topic/list?folder=&organizationId=&project=wewatch-98a41

image-20201125154545807.png

創(chuàng)建主題

image-20201125154657884.png

當(dāng)點(diǎn)擊創(chuàng)建主題時(shí),會(huì)要求輸入對(duì)應(yīng)的主題名稱

image-20201125155139663.png

創(chuàng)建成功后闷盔,會(huì)出現(xiàn)對(duì)應(yīng)的主題ID和主題名稱弯洗,名稱是用于測試的,選擇最右邊的編輯按鈕逢勾,或者直接點(diǎn)擊左側(cè)的訂閱專欄牡整,都能為主題創(chuàng)建訂閱

創(chuàng)建訂閱

image-20201125155255376.png

進(jìn)入之后輸入對(duì)應(yīng)的訂閱名稱,選擇傳送類型為拉取溺拱,用于測試主題消息的連通性逃贝,其余選項(xiàng)選擇默認(rèn),當(dāng)創(chuàng)建成功時(shí)會(huì)進(jìn)入訂閱界面迫摔,這時(shí)候復(fù)制對(duì)應(yīng)的主題名稱到測試頁面沐扳。正式訂閱時(shí),需要改為推送句占,這里拉取只是測試連通性沪摄。

網(wǎng)址:https://play.google.com/console/u/0/developers/6637358084117674478/app/4976005302300764813/monetization-setup

image-20201125155510110.png

權(quán)限授予和校驗(yàn)流程

原理圖:

image-20201125161016130.png

首先進(jìn)入Google play console >> 選擇應(yīng)用 >> 選擇獲利設(shè)置

當(dāng)進(jìn)入該頁面以后,輸入剛剛復(fù)制的主題名稱

image-20201125155918230.png

當(dāng)權(quán)限失敗時(shí)就會(huì)出現(xiàn)該情況纱烘,這時(shí)候需要檢查主題和訂閱的權(quán)限杨拐。

1.給谷歌云消息賬號(hào)添加推送的權(quán)限,例如:簽名是應(yīng)用的ID凹炸,后面格式一致91804594755@cloudservices.gserviceaccount.com

image-20201125160149887.png

點(diǎn)擊添加成員后戏阅,輸入賬號(hào),并給予推送權(quán)限

image-20201125160309620.png

2.給訂閱者添加權(quán)限

image-20201125160544704.png

與主題添加權(quán)限一致啤它,這里的賬號(hào)添加的是谷歌API后臺(tái)賬號(hào)奕筐,即一開始我們創(chuàng)建谷歌后臺(tái)賬號(hào),到此之后变骡,便能確保當(dāng)谷歌支付有狀態(tài)改變時(shí)离赫,谷歌能及時(shí)通知到后臺(tái),去對(duì)訂單做對(duì)應(yīng)的處理塌碌。

3.谷歌應(yīng)用測試人員配置

谷歌控制臺(tái)的測試人員配置

image-20201125161848417.png

在選項(xiàng)中輸入對(duì)頂?shù)膅mail郵箱

進(jìn)入應(yīng)用配置測試人員

網(wǎng)址:https://play.google.com/console/u/0/developers/6637358084117674478/app/4976005302300764813/tracks/internal-testing?tab=testers

image-20201125162247785.png

點(diǎn)擊應(yīng)用>> 測試 >> 內(nèi)部測試

點(diǎn)擊圖上的右鍵頭渊胸,輸入對(duì)應(yīng)的郵箱地址,隨后將底部的復(fù)制鏈接台妆,發(fā)送給對(duì)應(yīng)的谷歌賬號(hào)翎猛,同意授權(quán)之后胖翰,需要等待一段時(shí)間,一般在24小時(shí)之內(nèi)切厘,該賬號(hào)就有權(quán)限萨咳。校驗(yàn)測試購買的權(quán)限之一,就是進(jìn)入到Play store商店中疫稿,查找對(duì)應(yīng)的APP名字培他,如果能查到就有對(duì)應(yīng)權(quán)限

測試補(bǔ)充說明

1.手機(jī)或平板需要有對(duì)應(yīng)的谷歌服務(wù)、谷歌商店遗座、穩(wěn)定的vpn舀凛,才能順利購買,通惩窘可以使用Google Go三方軟件進(jìn)行配置檢測猛遍,華為magic系統(tǒng)新手機(jī)除外

2.對(duì)于測試前提,需要上架對(duì)應(yīng)的應(yīng)用碎绎,當(dāng)前上架應(yīng)用需要審核螃壤,審核時(shí)間為7天,只有應(yīng)用審核通過之后筋帖,才能進(jìn)行以下測試

image-20201125170559803.png

3.對(duì)于測試周期奸晴,訂閱的周期測試為5分鐘,即自動(dòng)續(xù)訂是每5分鐘一次日麸,會(huì)續(xù)訂6次寄啼,對(duì)于消費(fèi)包,沒有限制代箭。對(duì)于訂閱的升降級(jí)墩划,谷歌內(nèi)部有提供多種結(jié)算方式:

BillingFlowParams.IMMEDIATE_WITH_TIME_PRORATION(立即生效,購買的新套餐首月價(jià)格會(huì)減去生效包剩余價(jià)格)

BillingFlowParams.DEFERRED(下一個(gè)結(jié)算周期生效)

其余結(jié)算方式請(qǐng)查看該網(wǎng)址: https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode

4.訂閱或消費(fèi)商品的創(chuàng)建

網(wǎng)址:https://play.google.com/console/u/0/developers/6637358084117674478/app/4976005302300764813/managed-products

image-20201125163439803.png

點(diǎn)擊應(yīng)用>> 商品 >> 應(yīng)用內(nèi)商品

消耗商品配置

該頁面為消費(fèi)商品嗡综,即一次性商品乙帮,通過最右側(cè)的創(chuàng)建商品,可以創(chuàng)建對(duì)應(yīng)的商品信息

image-20201125163706946.png

輸入產(chǎn)品ID和對(duì)應(yīng)名稱极景,點(diǎn)擊最底下的創(chuàng)建模板

網(wǎng)址: https://play.google.com/console/u/0/developers/6637358084117674478/pricing-templates

image-20201125163830211.png

該模板可以選擇不同國家察净,并且根據(jù)實(shí)際匯率進(jìn)行價(jià)格轉(zhuǎn)換

image-20201125164515135.png

選擇完價(jià)格之后,對(duì)商品進(jìn)行保存

訂閱商品配置

image-20201125164654082.png

點(diǎn)擊創(chuàng)建訂閱內(nèi)容

image-20201125164743186.png

訂閱相比消耗盼樟,多了結(jié)算周期氢卡,和首訂等功能添加,最后需要點(diǎn)擊右下角的保存按鈕,便可以生效。

5.安卓終端支付對(duì)接

a.邏輯圖

image-20201125165420414.png

訂閱流程:

1.判斷是否為預(yù)付費(fèi)用戶

2.判斷是否為訂閱裤纹,默認(rèn)未訂閱

3.判斷當(dāng)前用戶是否存在訂閱套餐逢捺,若不是Google方式購買則不允許購買筑悴,若是則繼續(xù)

4.判斷已購買的套餐们拙,對(duì)應(yīng)的谷歌訂單中的用戶是否為同一用戶,如果不是則不允許購買阁吝,若是則繼續(xù)

5.每次購買前會(huì)查詢谷歌訂單睛竣,用于補(bǔ)單處理

6.訪問后臺(tái)API創(chuàng)建對(duì)應(yīng)的訂單

7.調(diào)用谷歌的訂閱購買

8.調(diào)用谷歌的消費(fèi)接口

9.將訂單信息保存到本地中,并進(jìn)行加密處理求摇,防止漏單情況

10.將對(duì)應(yīng)訂單信息回調(diào)后臺(tái),通知后臺(tái)去開通訂單殊者,當(dāng)返回成功則与境,刪除訂單信息,若失敗或存儲(chǔ)猖吴,在應(yīng)用啟動(dòng)時(shí)進(jìn)行補(bǔ)單操作

11.當(dāng)升級(jí)訂閱時(shí)摔刁,會(huì)立馬提示刷新訂閱界面,當(dāng)降級(jí)時(shí)海蔽,會(huì)有通知彈窗告知用戶下個(gè)結(jié)算周期生效共屈,最終完成訂單處理

消費(fèi)流程:

1.判斷是否為預(yù)付費(fèi)用戶

2.判斷是否為消費(fèi)包

3.提示支付方式的彈窗

4.支付彈窗選擇谷歌支付

5.訪問后臺(tái)API創(chuàng)建對(duì)應(yīng)的訂單

6.調(diào)用谷歌消費(fèi)包購買

7.調(diào)用谷歌的消費(fèi)接口

8.將訂單信息保存到本地中,并進(jìn)行加密處理党窜,防止漏單情況

9.將對(duì)應(yīng)訂單信息回調(diào)后臺(tái)拗引,通知后臺(tái)去開通訂單,當(dāng)返回成功則幌衣,刪除訂單信息矾削,若失敗或存儲(chǔ),在應(yīng)用啟動(dòng)時(shí)進(jìn)行補(bǔ)單操作

10.當(dāng)購買成功時(shí)豁护,會(huì)立馬提示刷新界面哼凯,并將消費(fèi)包按鈕置與不可點(diǎn)擊狀態(tài)

b.谷歌API對(duì)應(yīng)的提示碼

錯(cuò)誤碼提示.png

c.代碼

官方文檔網(wǎng)址:https://developer.android.google.cn/google/play/billing/billing_library_overview

1.GoogleUtils.java和PayUtils.kt

public class GooglePayUtils implements PurchasesUpdatedListener {
    //持弱引用仿內(nèi)存泄露
    private WeakReference<Context> weakContext;
    private BillingClient billingClient;
    private static final String TAG = "GooglePayUtils";

    public GooglePayUtils(Context context) {
        weakContext = new WeakReference<>(context);
    }

    public void init() {
        //為Utils添加產(chǎn)品包更新回調(diào)監(jiān)聽 PurchasesUpdatedListener
        billingClient = BillingClient.newBuilder(getContext()).setListener(this).enablePendingPurchases().build();
        //開始連接谷歌服務(wù)
        billingClient.startConnection(new BillingClientStateListener() {
            @Override
            public void onBillingSetupFinished(BillingResult billingResult) {
                if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                    LogUtil.e(TAG, "init Success");
                    if (payStateListener != null) {
                        payStateListener.onConnectionSuccess();
                    }
                } else {
                    XMToastUtil.showShortToastCenter(billingResult.getDebugMessage(), getContext());
                    LogUtil.e(TAG, "init fail");
                    if (payStateListener != null && getContext() instanceof Activity) {
                        Intent intent = new Intent();
                        intent.putExtra("data", "fail");
                        ((Activity) getContext()).setResult(Activity.RESULT_CANCELED, intent);
                        ((Activity) getContext()).finish();
                    }
                }
            }

            @Override
            public void onBillingServiceDisconnected() {
                // Try to restart the connection on the next request to
                // Google Play by calling the startConnection() method.
            }
        });

    }

    @Override
    public void onPurchasesUpdated(BillingResult billingResult, @Nullable List<Purchase> purchaseList) {
        if (payStateListener != null)
            payStateListener.onPurchasesUpdated(billingResult, purchaseList);
    }


    /**
     * 查詢已購買的產(chǎn)品
     */
    public void queryHavePurchased() {

        Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
        if (areSubscriptionsSupported()) {
            Purchase.PurchasesResult subscriptionResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
            if (subscriptionResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                purchasesResult.getPurchasesList().addAll(subscriptionResult.getPurchasesList());
            } else {
                LogUtil.e(TAG, "Got an error response trying to query subscription purchases");
            }
        }

        LogUtil.e(TAG, "queryHavePurchased: " + purchasesResult.getPurchasesList().toString());
        //payStateListener.onQueryPurchasedSuccess(purchasesResult);

    }

    /**
     * @return 是否有可訂閱的商品
     */
    private boolean areSubscriptionsSupported() {
        BillingResult result = billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS);
        int responseCode = result.getResponseCode();
        if (responseCode != BillingClient.BillingResponseCode.OK) {
            LogUtil.e(TAG, "areSubscriptionsSupported() got an error response: " + responseCode);
        }
        return responseCode == BillingClient.BillingResponseCode.OK;
    }

    /**
     * 下單購買消耗性商品的sku
     *
     * @param sku 產(chǎn)品的sku數(shù)組
     */
    public void purchase(String sku) {

        if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(getContext()) != 0) {
            XMToastUtil.showShortToastCenter("your phone can't support google service", getContext());
            return;
        }
        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        List<String> skuList = new ArrayList<>();
        skuList.add(sku);
        params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
        billingClient.querySkuDetailsAsync(params.build(),
                new SkuDetailsResponseListener() {
                    @Override
                    public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> list) {

                        if (list == null || list.size() == 0) {
                            Toast.makeText(getContext(), "not match" + billingResult.getDebugMessage() + billingResult.getResponseCode(), Toast.LENGTH_SHORT).show();
                            if (payStateListener != null)
                                payStateListener.onPurchasesFail(sku, billingResult.getResponseCode());
                            return;
                        }
                        if (list == null) return;
                        for (int i = 0; i < list.size(); i++) {
                            SkuDetails skuDetails = list.get(i);
                            BillingFlowParams flowParams = BillingFlowParams.newBuilder()
                                    .setSkuDetails(skuDetails)
                                    .build();
                            int code = billingClient.launchBillingFlow(getActivity(getContext()), flowParams).getResponseCode();
                            if (code != BillingClient.BillingResponseCode.OK && payStateListener != null) {
                                Toast.makeText(getContext(), "purchase fail:" + code, Toast.LENGTH_SHORT).show();
                                payStateListener.onPurchasesFail(skuDetails.getTitle(), code);
                            }
                            /*if (code == BillingClient.BillingResponseCode.OK && payStateListener != null) {
                                payStateListener.onSubscribeSuccess(ProductType.NORMAL);
                            }*/
                        }
                    }
                });
    }

    /**
     * @description 消耗所有可查詢的包
     * @param
     */
    public void consumePurchase() {
        Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
        List<Purchase> purchasesList = purchasesResult.getPurchasesList();
        if (purchasesList == null) return;

        for (int i = 0; i < purchasesList.size(); i++) {
            Purchase purchase = purchasesList.get(i);
            ConsumeParams consumeParams =
                    ConsumeParams.newBuilder()
                            .setPurchaseToken(purchase.getPurchaseToken())
                            .setDeveloperPayload(purchase.getDeveloperPayload())
                            .build();
            billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
                @Override
                public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
                    LogUtil.e(">>>>onConsumeResponse:", "message:" + billingResult.getDebugMessage() + billingResult.getResponseCode());
                    payStateListener.onConsumeFinished(purchase);
                }
            });
        }
    }

    private Purchase currentSusProduct;

    /**
     * @description 訂閱包的購買
     * @param  newSku 對(duì)應(yīng)的商品id
     * @param  productType 具體升降級(jí)類型
     */
    public void subscribe(String newSku, ProductType productType) {
        LogUtil.e(TAG, "newSku:" + newSku);
        if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(getContext()) != 0) {
            XMToastUtil.showShortToastCenter("your phone can't support google service", getContext());
            return;
        }

        ArrayList<String> skuList = new ArrayList<>();
        skuList.add(newSku);
        SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
        params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS);
        billingClient.querySkuDetailsAsync(params.build(),
                new SkuDetailsResponseListener() {
                    @Override
                    public void onSkuDetailsResponse(BillingResult billingResult, List<SkuDetails> list) {


                        Purchase.PurchasesResult subscriptionResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
                        //LogUtil.e(TAG, "當(dāng)前訂閱的套餐:" + subscriptionResult.getPurchasesList().toString());
                        if (subscriptionResult.getPurchasesList() != null && !subscriptionResult.getPurchasesList().isEmpty()) {
                            currentSusProduct = subscriptionResult.getPurchasesList().get(0);
                        }

                        if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) {
                            Toast.makeText(getContext(), "please check you network", Toast.LENGTH_SHORT).show();
                            return;
                        }

                        if (list == null || list.size() == 0) {
                            Toast.makeText(getContext(), "not match", Toast.LENGTH_SHORT).show();
                            return;
                        }
                        for (int i = 0; i < list.size(); i++) {
                            SkuDetails skuDetails = list.get(i);
                            BillingFlowParams.Builder builder;
                            //沒有訂閱記錄


                            if (currentSusProduct == null) {
                                //沒有有效期內(nèi)的訂閱記錄
                                builder = BillingFlowParams.newBuilder()
                                        .setSkuDetails(skuDetails);

                            } else {
                                //有訂閱記錄,根據(jù)type判斷是升級(jí)還是降級(jí)

                                builder = BillingFlowParams.newBuilder()
                                        .setSkuDetails(skuDetails)
                                        .setReplaceSkusProrationMode(productType == ProductType.UPGRADE ? IMMEDIATE_WITH_TIME_PRORATION : DEFERRED)
                                        .setOldSku(currentSusProduct.getSku(), currentSusProduct.getPurchaseToken());
                            }

                            int code = billingClient.launchBillingFlow(getActivity(getContext()), builder.build()).getResponseCode();
                            if (code != BillingClient.BillingResponseCode.OK && payStateListener != null) {
                                payStateListener.onPurchasesFail(skuDetails.getTitle(), code);
                            }
                            /*if (code == BillingClient.BillingResponseCode.OK && payStateListener != null) {
                                payStateListener.onSubscribeSuccess(productType);
                            }*/
                        }
                    }
                });
    }


    private Context getContext() {
        return weakContext.get();
    }

    private static Activity getActivity(Context context) {
        return context == null ? null : (context instanceof Activity ? (Activity) context : (context instanceof ContextWrapper ? getActivity(((ContextWrapper) context).getBaseContext()) : null));
    }

    /**
     * 消耗購買的內(nèi)購產(chǎn)品
     *
     * @param purchase     要消費(fèi)掉的產(chǎn)品類
     * @param purchaseType 產(chǎn)品類型
     */
    public void consumePurchase(Purchase purchase, String purchaseType) {
        LogUtil.e(TAG, "consumeAsync");

        if (purchaseType.equals(BillingClient.SkuType.INAPP)) {
            ConsumeParams consumeParams =
                    ConsumeParams.newBuilder()
                            .setPurchaseToken(purchase.getPurchaseToken())
                            .setDeveloperPayload(purchase.getDeveloperPayload())
                            .build();
            billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
                @Override
                public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
                    payStateListener.onConsumeFinished(purchase);
                }
            });
        } else if (purchaseType.equals(BillingClient.SkuType.SUBS)) {
            if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
                if (!purchase.isAcknowledged()) {
                    AcknowledgePurchaseParams acknowledgePurchaseParams =
                            AcknowledgePurchaseParams.newBuilder()
                                    .setPurchaseToken(purchase.getPurchaseToken())
                                    .build();

                    billingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
                        @Override
                        public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
                            if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
                                payStateListener.onConsumeFinished(purchase);
                            }
                        }
                    });

                } else {
                    payStateListener.onConsumeFinished(purchase);
                }
            }
        }
    }


    private GooglePayStateListener payStateListener;

    /**
     * @param payStateListener google pay支付相關(guān)回調(diào)額數(shù)據(jù)
     */
    public void setGooglePayStateListener(GooglePayStateListener payStateListener) {
        this.payStateListener = payStateListener;
    }


    /**
     * 跳轉(zhuǎn)到google play訂閱界面
     *
     * @param context
     */
    public static void jump2GooglePlaySub(Context context) {
        Intent intent = new Intent();
        intent.setAction("android.intent.action.VIEW");
        Uri content_url = Uri.parse("https://play.google.com/store/account/subscriptions");
        intent.setData(content_url);
        context.startActivity(intent);
    }

    /**
     * 跳轉(zhuǎn)到google play訂閱指定產(chǎn)品id界面
     *
     * @param context
     */
    public static void jump2GooglePlaySubById(Context context, String productId) {
        Intent intent = new Intent();
        intent.setAction("android.intent.action.VIEW");
        Uri content_url = Uri.parse("https://play.google.com/store/account/subscriptions?sku=" + productId + "&package=" + context.getPackageName());
        intent.setData(content_url);
        context.startActivity(intent);
    }

    public void endConnection() {
        if (billingClient != null && billingClient.isReady()) {
            billingClient.endConnection();
        }
    }


    public void dismissConnect() {
        if (billingClient != null) billingClient.endConnection();
    }

}
class PayUtils private constructor(context: Context) : PurchasesUpdatedListener {
    private val context: Context? = WeakReference(context).get()
    private var currentSusProduct: Purchase? = null

    /**
     * @description 使用by lazy方式楚里,在第一次使用的時(shí)候就會(huì)進(jìn)行初始化
     */
    private val billingClient by lazy {
        BillingClient.newBuilder(context)
            .setListener(this)
            .enablePendingPurchases()
            .build()
    }

    fun init() {
        //進(jìn)行谷歌服務(wù)器連接監(jiān)聽
        billingClient.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    LogUtil.e(TAG, "init Success")
                    payStateListener?.onConnectionSuccess()

                } else {
                    LogUtil.e(TAG, "init Fail")
                }

            }

            override fun onBillingServiceDisconnected() {
                // Try to restart the connection on the next request to
                // Google Play by calling the startConnection() method.
            }
        })
    }

    /**
     * @description 監(jiān)聽谷歌包發(fā)生改變断部,在by lazy時(shí)就做了監(jiān)聽處理
     * @param billingResult 購買包購買結(jié)果
     * @param purchaseList 為空代表降級(jí),不為空則為正常購買或升級(jí)
     */
    override fun onPurchasesUpdated(billingResult: BillingResult, purchaseList: List<Purchase>?) {
        payStateListener?.onPurchasesUpdated(billingResult, purchaseList)
    }

    /**
     * 查詢已購買的產(chǎn)品班缎,通常用于補(bǔ)單或購買前校驗(yàn)
     */
    fun queryHavePurchased() {
        val purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
        if (areSubscriptionsSupported()) {
            val subscriptionResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS)
            if (subscriptionResult.responseCode == BillingClient.BillingResponseCode.OK) {
                purchasesResult.purchasesList.addAll(subscriptionResult.purchasesList)
            } else {
                LogUtil.e(TAG, "Got an error response trying to query subscription purchases")
            }
            payStateListener?.onQueryPurchasedSuccess(subscriptionResult)
        }
        LogUtil.e(TAG, "queryHavePurchased: " + purchasesResult.purchasesList.toString())
    }

    /**
     * @return 是否有可訂閱的商品
     */
    private fun areSubscriptionsSupported(): Boolean {
        val result = billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS)
        val responseCode = result.responseCode
        if (responseCode != BillingClient.BillingResponseCode.OK) {
            LogUtil.e(TAG, "areSubscriptionsSupported() got an error response: $responseCode")
        }
        return responseCode == BillingClient.BillingResponseCode.OK
    }

    /**
     * 下單購買消耗性商品的sku
     *
     * @param sku 產(chǎn)品的sku數(shù)組
     */
    fun purchase(sku: String) {
        if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != 0) {
            XMToastUtil.showShortToastCenter(context!!.getString(R.string.pay_google_device_not_support),
                context)
            return
        }
        val params = SkuDetailsParams.newBuilder()
        val skuList: MutableList<String> = ArrayList()
        skuList.add(sku)
        params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP)
        billingClient.querySkuDetailsAsync(params.build(),
            SkuDetailsResponseListener { billingResult, list ->
                if (list == null || list.size == 0) {
                    LogUtil.e(TAG,
                        "not match" + billingResult.debugMessage + billingResult.responseCode)
                    XMToastUtil.showShortToastCenter(context!!.getString(R.string.pay_google_no_package),
                        context)
                    payStateListener?.onPurchasesFail(sku,
                        BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED)
                    return@SkuDetailsResponseListener
                }
                if (list == null) return@SkuDetailsResponseListener
                for (i in list.indices) {
                    val skuDetails = list[i]
                    val flowParams = BillingFlowParams.newBuilder()
                        .setSkuDetails(skuDetails)
                        .build()
                    val code =
                        billingClient.launchBillingFlow(getActivity(context),
                            flowParams).responseCode
                    if (code != BillingClient.BillingResponseCode.OK) {
                        payStateListener?.onPurchasesFail(skuDetails.title, code)
                    }
                    /*     if (code == BillingClient.BillingResponseCode.OK) {
                             payStateListener?.onSubscribeSuccess(ProductType.NORMAL)
                         }*/
                }
            })
    }

    /**
     * @description 消費(fèi)所有未消費(fèi)的產(chǎn)品包蝴光,含訂閱消費(fèi)包
     */
    fun consumePurchase() {
        val purchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP)
        val purchasesList = purchasesResult.purchasesList ?: return
        for (i in purchasesList.indices) {
            val purchase = purchasesList[i]
            val consumeParams = ConsumeParams.newBuilder()
                .setPurchaseToken(purchase.purchaseToken)
                .setDeveloperPayload(purchase.developerPayload)
                .build()
            billingClient.consumeAsync(consumeParams) { billingResult, purchaseToken ->
                LogUtil.e(">>>>onConsumeResponse:",
                    "message:" + billingResult.debugMessage + billingResult.responseCode)
                payStateListener?.onConsumeFinished(purchase)
            }
        }
    }

    /**
     * @description 訂閱包的購買
     * @param  newSku 對(duì)應(yīng)的商品id
     * @param  productType 具體升降級(jí)類型
     */
    fun subscribe(newSku: String, productType: ProductType) {
        LogUtil.e(TAG, "newSku:$newSku")
        if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != 0) {
            XMToastUtil.showShortToastCenter(context!!.getString(R.string.pay_google_device_not_support),
                context)
            return
        }
        val skuList = ArrayList<String>()
        skuList.add(newSku)
        val params = SkuDetailsParams.newBuilder()
        params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS)
        billingClient.querySkuDetailsAsync(params.build(),
            SkuDetailsResponseListener { billingResult, list ->
                val subscriptionResult = billingClient.queryPurchases(BillingClient.SkuType.SUBS)
                LogUtil.e(TAG, "當(dāng)前訂閱的套餐:" + subscriptionResult.purchasesList.toString());
                if (subscriptionResult.purchasesList != null && subscriptionResult.purchasesList.isNotEmpty()) {
                    currentSusProduct = subscriptionResult.purchasesList[0]
                }
                if (billingResult.responseCode == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) {
                    XMToastUtil.showShortToastCenter(context!!.getString(R.string.pay_check_network),
                        context)
                    return@SkuDetailsResponseListener
                }
                if (list.isNullOrEmpty()) {
                    XMToastUtil.showShortToastCenter(context!!.getString(R.string.pay_google_no_package),
                        context)
                    return@SkuDetailsResponseListener
                }
                for (i in list.indices) {
                    val skuDetails = list[i]
                    var builder: BillingFlowParams.Builder
                    //沒有訂閱記錄
                    builder = if (currentSusProduct == null) {
                        //沒有有效期內(nèi)的訂閱記錄
                        BillingFlowParams.newBuilder()
                            .setSkuDetails(skuDetails)
                    } else {
                        //有訂閱記錄,根據(jù)type判斷是升級(jí)還是降級(jí)
                        BillingFlowParams.newBuilder()
                            .setSkuDetails(skuDetails)
                            .setReplaceSkusProrationMode(if (productType == ProductType.UPGRADE) BillingFlowParams.ProrationMode.IMMEDIATE_WITH_TIME_PRORATION else BillingFlowParams.ProrationMode.DEFERRED)
                            .setOldSku(currentSusProduct!!.sku, currentSusProduct!!.purchaseToken)
                    }
                    val code =
                        billingClient.launchBillingFlow(getActivity(context),
                            builder.build()).responseCode
                    if (code != BillingClient.BillingResponseCode.OK) {
                        payStateListener?.onPurchasesFail(skuDetails.title, code)
                    }

                }
            })
    }


    /**
     * 消耗包購買
     * @param purchase     要消費(fèi)掉的產(chǎn)品類
     * @param purchaseType 產(chǎn)品類型
     */
    fun consumePurchase(purchase: Purchase, purchaseType: String) {
        LogUtil.e(TAG, "consumeAsync")
        if (purchaseType == BillingClient.SkuType.INAPP) {
            val consumeParams = ConsumeParams.newBuilder()
                .setPurchaseToken(purchase.purchaseToken)
                .setDeveloperPayload(purchase.developerPayload)
                .build()
            billingClient.consumeAsync(consumeParams) { billingResult, purchaseToken ->
                payStateListener?.onConsumeFinished(purchase)
            }
        } else if (purchaseType == BillingClient.SkuType.SUBS) {
            if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
                if (!purchase.isAcknowledged) {
                    val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
                        .setPurchaseToken(purchase.purchaseToken)
                        .build()
                    billingClient.acknowledgePurchase(acknowledgePurchaseParams) { billingResult ->
                        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                            payStateListener?.onConsumeFinished(purchase)
                        }
                    }
                } else {
                    payStateListener?.onConsumeFinished(purchase)
                }
            }
        }
    }

    //設(shè)置播放狀態(tài)回調(diào)監(jiān)聽
    private var payStateListener: GooglePayStateListener? = null
    fun setGooglePayStateListener(payStateListener: GooglePayStateListener?) {
        this.payStateListener = payStateListener
    }

    /**
     * @description 連接狀態(tài)關(guān)閉
     */
    fun endConnection() {
        if (billingClient?.isReady) {
            billingClient?.endConnection()
        }
    }

    /**
     * @description 強(qiáng)制關(guān)閉
     */
    fun dismissConnect() {
        billingClient?.endConnection()
    }

    companion object : SingletonHolder<PayUtils, Context>(::PayUtils) {
        private const val TAG = "GooglePayUtils"

        /**
         * @description 獲取當(dāng)前context的實(shí)際依附Activity
         */
        private fun getActivity(context: Context?): Activity? {
            return when (context) {
                null -> null
                is Activity -> context
                is ContextWrapper -> getActivity(
                    context.baseContext)
                else -> null
            }
        }

        /**
         * 跳轉(zhuǎn)到google play訂閱界面
         */
        fun jump2GooglePlaySub(context: Context) {
            val intent = Intent()
            intent.action = "android.intent.action.VIEW"
            val contentUrl = Uri.parse("https://play.google.com/store/account/subscriptions")
            intent.data = contentUrl
            context.startActivity(intent)
        }

        /**
         * 跳轉(zhuǎn)到google play訂閱指定產(chǎn)品id界面
         */
        fun jump2GooglePlaySubById(context: Context, productId: String) {
            val intent = Intent()
            intent.action = "android.intent.action.VIEW"
            val contentUrl =
                Uri.parse("https://play.google.com/store/account/subscriptions?sku=" + productId + "&package=" + context.packageName)
            intent.data = contentUrl
            context.startActivity(intent)
        }


    }

}

2.PayUtils的調(diào)用方式

代碼邏輯圖

image-20201125201202941.png
//a.初始化
googlePayUtils = PayUtils.getInstance(activity)
googlePayUtils?.setGooglePayStateListener(this)
googlePayUtils?.init()
//b.setGooglePayStateListener的監(jiān)聽回調(diào)

/**
 * 谷歌服務(wù)連接成功吝梅,查詢谷歌訂單做無感補(bǔ)單
 */
override fun onConnectionSuccess() {
    googlePayUtils?.queryHavePurchased()
    isReady = true
}

/**
 * 查詢谷歌訂單做無感補(bǔ)單回調(diào)
 */
override fun onQueryPurchasedSuccess(purchasesResult: Purchase.PurchasesResult) {
    this.purchasesResult = purchasesResult
    for (purchase in purchasesResult.purchasesList) {
        if (!purchase.isAcknowledged) {
            productType = ProductType.UPGRADE
            closeNotification = true
            googlePayUtils?.consumePurchase(purchase, BillingClient.SkuType.SUBS)
        }
    }
}

/**
 * 購買成功做消費(fèi)處理虱疏,或降級(jí)成功彈窗處理
 */
override fun onPurchasesUpdated(billingResult: BillingResult,
    purchaseList: MutableList<Purchase>?) {
    when (billingResult.responseCode) {
        BillingClient.BillingResponseCode.OK -> {
            if (purchaseList != null && purchaseList.size > 0) {
                for (purchase in purchaseList) {
                    val type =
                        if (productType == ProductType.NORMAL) BillingClient.SkuType.INAPP else BillingClient.SkuType.SUBS
                    googlePayUtils?.consumePurchase(purchase, type)
                }
            } else {
                callback?.showSubscriptionDown()
            }
        }
        else -> {
            LogUtil.e(TAG, "message:" + billingResult.debugMessage)

        }
    }

}

/**
 * 谷歌購買失敗提示
 */
override fun onPurchasesFail(sku: String, code: Int) {
 XMToastUtil.showShortToastCenter(activity.getString(R.string.pay_google_purchase_fail), activity)
}

/**
 * @description  谷歌產(chǎn)品消費(fèi)完畢,進(jìn)行本地存儲(chǔ)苏携,防止漏單
 */
override fun onConsumeFinished(purchase: Purchase) {
    LogUtil.e(TAG, "onConsumeFinished: google消耗商品" + purchase.packageName + "成功做瞪,開始服務(wù)器消耗")
    if (!closeNotification)
    XMToastUtil.showShortToastCenter(activity.getString(R.string.pay_google_wait_a_while), activity)

    //存儲(chǔ)谷歌訂單信息
    localData = GooglePlayLocalData(orderId, purchase, productType)
    GooglePayInfoUtils.saveGooglePayInfo(localData)
    //谷歌訂單信息上報(bào)后臺(tái)
    viewModel?.postGooglePayCallback(orderId,
                                     purchase.signature,
                                     purchase.originalJson,
                                     if (productType == ProductType.DOWNGRADE) "2" else "1")
}
//c.開始支付入口
    /**
     * @description 開始支付
     * @param
     * @time 2020/11/11 11:41
     */
    fun startPay(product: PurchaseData.Product,
        productType: ProductType,
        memberId: String,
        contentId: String = "") {
        this.product = product
        this.productType = productType
        this.contentId = contentId
        this.memberId = memberId

        LogUtil.e(TAG,
            "startPay product: $product>>>productType: $productType>>>contentId: $contentId>>>memberId: $memberId")
        if (!isReady) {
            callback?.showSubscriptionServiceError()
            return
        }

        closeNotification = false
        //是否為單點(diǎn)
        if (productType == ProductType.NORMAL) {
            callback?.showChooseDialog()
        } else {
            // 1.支付為訂閱,檢測是否存在其他平臺(tái)購買的訂閱套餐
            checkDeviceSupportPay(purchasesResult)
        }
    }

    /**
     * @description 檢測是否存在其他平臺(tái)購買的訂閱套餐
     * @param
     * @time 2020/11/11 11:42
     */
    private fun checkDeviceSupportPay(purchasesResult: Purchase.PurchasesResult?) {

        if (subscriptionResultData?.resultCode == XMediaErrorCode.CODE_SUCCESS) {
             // 2.當(dāng)前訂閱是否存在谷歌支付方式或沒有訂閱歷史
            if (isSubscriptionContainerGooglePay()) {
                // 3.檢測當(dāng)前谷歌賬號(hào)是否綁定過其他賬號(hào)
                queryGooglePayHistory(purchasesResult)
            } else {
                callback?.showOtherDevicePay()
            }
        } else {
            callback?.showSubscriptionServiceError()
        }
    }

    /**
     * @description 當(dāng)前訂閱是否存在谷歌支付方式或沒有訂閱歷史
     * @param
     * @time 2020/11/11 14:14
     */
    private fun isSubscriptionContainerGooglePay(): Boolean {
        subscriptionResultData?.subscription?.productTime?.paymentMethod?.let {
            return it == GOOGLE_PAY || it == OFFLINE_PAY
        }

        return true
    }

    /**
     * @description 檢測當(dāng)前谷歌賬號(hào)是否綁定過其他賬號(hào)
     * @param
     * @time 2020/11/11 14:13
     */
    private fun queryGooglePayHistory(purchasesResult: Purchase.PurchasesResult?) {
        purchasesResult?.let {
            val requestValue = StringBuffer()
            for (index in it.purchasesList.indices) {
                requestValue.append(it.purchasesList[index].orderId)
                if (index != it.purchasesList.size - 1) requestValue.append(",")
            }

            if (requestValue.isNotEmpty()) {
                 // 4.檢測當(dāng)前谷歌賬號(hào)是否綁定過其他賬號(hào)
                viewModel?.requestOrderCheck(requestValue.toString())
            } else {
                callback?.showChooseDialog()
            }
        }

        if (purchasesResult == null) {
            //無購買歷史,可以準(zhǔn)備下預(yù)訂單
            callback?.showChooseDialog()
        }
    }

    /**
     * @description 5.處理下單前的檢測結(jié)果
     */
    private fun handleOrderCheckData(orderCheckData: OrderCheckData) {
        if (orderCheckData.resultCode == XMediaErrorCode.CODE_SUCCESS) {
            if (orderCheckData.memberId == null) {
                callback?.showChooseDialog()
                return
            } else {
                //6.同一用戶装蓬,可以準(zhǔn)備下預(yù)訂單
                if (orderCheckData.memberId == memberId) {
                    viewModel?.requestOrder(product?.productId.toString(), contentId)
                } else {
                    callback?.showGoogleAccountConflict()
                }
            }
        } else {
            callback?.showOtherDevicePay()
        }
    }



//d.與后臺(tái)的接口回調(diào)
viewModel?.run {
    //接口著拭,當(dāng)前已訂閱產(chǎn)品
    subscriptionData.observe(activity) {
        subscriptionResultData = it
    }
    //接口,5.下單前檢測回調(diào)通知牍帚,防止同一賬號(hào)用不同方式購買儡遮,導(dǎo)致續(xù)訂混亂
    orderCheckData.observe(activity) {
        handleOrderCheckData(it)
    }

    //接口,7.下單回調(diào)通知暗赶,提交一個(gè)預(yù)訂單給后臺(tái)
    orderData.observe(activity) {
        if (it.resultCode == XMediaErrorCode.CODE_SUCCESS) {
            orderId = it.orderId
            when (payType) {
                GOOGLE_PAY -> {
                    closeNotification = false
                    if (productType == ProductType.NORMAL) {
                        //8.下單消費(fèi)包
                        startGoogleConsume()
                    } else {
                        //9.下單訂閱包
                        startGoogleSubscription()
                    }
                }
                else -> {

                }
            }
        } else {
            callback?.showOrderServiceError()
        }
    }

    //接口鄙币,10.支付回調(diào)通知,谷歌購買已成功蹂随,需將數(shù)據(jù)上報(bào)給后臺(tái)開套餐
    googlePayResultData.observe(activity) {
        if (it.resultCode == XMediaErrorCode.CODE_SUCCESS) {
            localData?.let { localData ->               GooglePayInfoUtils.removeGooglePayInfo(localData) }

            if (!closeNotification){
                // 11.回調(diào)刷新訂閱界面
                callback?.showSubscriptionSuccessful()
                closeNotification = false
            }
        }
    }
}
//e.開始使用PayUtils進(jìn)行谷歌的訂閱和消費(fèi)包購買
/**
 * @description 開始谷歌單次購買
 * @param
 * @time 2020/11/13 10:34
 */
private fun startGoogleConsume() {
    product?.let {
        var sku = ""
        for (payGateway in it.payGateways) {
            if (payGateway.gatewayCode == GOOGLE_PAY) {
                sku = payGateway.externalCode
            }
        }
        googlePayUtils?.purchase(sku)
    }

}

/**
 * @description 開始谷歌訂閱
 * @param
 * @time 2020/11/11 15:19
 */
private fun startGoogleSubscription() {
    //訂閱下單回調(diào)
    product?.let { product ->
        var sku = ""
        for (payGateway in product.payGateways) {
            if (payGateway.gatewayCode == GOOGLE_PAY) {
                sku = payGateway.externalCode
            }
        }

        subscriptionResultData?.subscription?.product?.grade?.let {
            if (it.isNotEmpty() && it.toInt() > product.grade) {
                productType = ProductType.DOWNGRADE
            }
        }
        googlePayUtils?.subscribe(sku, productType)
    }
}
//f.UI相關(guān)的回調(diào)接口
interface PayControllerCallback {
    fun showChooseDialog()

    fun showOtherDevicePay()

    fun showSubscriptionServiceError()

    fun showGoogleAccountConflict()

    fun showOrderServiceError()

    fun showSubscriptionSuccessful()

    fun showExitNoActiveOrder()

    fun showSubscriptionDown()
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末十嘿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子岳锁,更是在濱河造成了極大的恐慌绩衷,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件激率,死亡現(xiàn)場離奇詭異咳燕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)乒躺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門招盲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人聪蘸,你說我怎么就攤上這事宪肖。” “怎么了健爬?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵控乾,是天一觀的道長。 經(jīng)常有香客問我娜遵,道長蜕衡,這世上最難降的妖魔是什么设拟? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮纳胧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘万皿。我一直安慰自己,他們只是感情好牢硅,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著综苔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪如筛。 梳的紋絲不亂的頭發(fā)上抒抬,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼可免。 笑死抓于,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的浇借。 我是一名探鬼主播捉撮,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼妇垢!你這毒婦竟也來了巾遭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤闯估,失蹤者是張志新(化名)和其女友劉穎灼舍,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體涨薪,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡骑素,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了刚夺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片献丑。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖侠姑,靈堂內(nèi)的尸體忽然破棺而出创橄,到底是詐尸還是另有隱情,我是刑警寧澤莽红,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布妥畏,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏咖熟。R本人自食惡果不足惜圃酵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一郭赐、第九天 我趴在偏房一處隱蔽的房頂上張望捌锭。 院中可真熱鬧观谦,春花似錦豁状、人聲如沸泻红。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽惨恭。三九已至喉恋,卻和暖如春轻黑,著一層夾襖步出監(jiān)牢的瞬間氓鄙,已是汗流浹背抖拦。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國打工噩茄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绩聘,地道東北人凿菩。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓衅谷,卻偏偏與公主長得像获黔,于是被迫代替她去往敵國和親肢执。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容