unity 3D + Google Play In-app Billing (IAB)(转) 热度 3

 

最近由于工作需要,研究unity如何接入Google Play以实现游戏内购买。目前IAB的实现,prime31做的插件比较好,各平台的IAB均有,但费用相对过高(几乎都是70刀左右,可怜穷小子)。在blogspot上找到仅有的一篇相关文章,特此转载在这以供大家相互交流学习。由于编程基础较差加上没学过java,按照该作者的方法暂时没有实现,希望看到此文章的程序猿们共同探讨解决方案。以下是文章原文:
 
底下是 Unity 介接 Google Play In-app Billing (IAB) 金流的步驟,使用的環境是 OSX,IAB 是 V3 版 :

1. 編譯 IInAppBillingService.java

a. 按照 GooglePlay 官方網站的指示 ( http://developer.android.com/training/in-app-billing/preparing-iab-app.html) 產生此檔案,其實這是一個自動產生的檔案,詳細方法可以參考連結網頁敘述。此外值得注意的是,程式碼也可以由 git 從此處下載 :

git clone https://code.google.com/p/marketbilling/

(附註 : 也許現在已經更新了,但文章撰寫的當下,若是從 SDK Manager 勾選 Google Play Billing Library 下載的版本有 Bug,所以才由 git 下載。 ref :
http://stackoverflow.com/questions/14397343/google-play-in-app-billing-version-3-crash-on-item-already-owned-and-missing )

b. 編譯 IInAppBillingService.java,指令如下 (此處假設  android sdk 之資料夾路徑在 /Users/macaronics/android-sdks/) :

javac ./IInAppBillingService.java -cp /Users/macaronics/android-sdks/platforms/android-10/android.jar -d .

其中 -cp 指示編譯時參考之 library (android-10, Android 2.3.3)。編譯完成後輸入 :

jar cvfM ./iiabs.jar com/

產生的 iiabs.jar 放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下( 此範例將檔案置於 /Users/macaronics/UnityIabProj/Assets/Plugins/Android/) 。
注意要刪除資料夾 com,以免後續執行 jar 時,打包到舊的 com package。 

2. 編譯 class IabHelper

a. 此類別由九個檔案組成,方便用來介接 Google IAB,九個 java 檔案應該位於前步驟下載之程式碼的 marketbilling/v3/src/com/example/android/trivialdrivesample/util 底下。

b. 逐一修改九個檔案裡的 "package com.example.android.trivialdrivesample.util" 改為你的 package name,例如  package com.macaronics.iab.util。

c. 建立欲編譯的檔案的 list,在 Terminal 底下的話直接輸入 cat > sources 然後依序輸入九個檔案檔名後按 control+d 儲存離開。

在 terminal 底下利用 cat 建立 sources file
 
d. 編譯 class IabHelper,輸入指令如下 :
 
javac @sources -cp /Users/macaronics/android-sdks/platforms/android-
10/android.jar:/Users/macaronics/UnityIabProj/Assets/Plugins/Android/iiabs.jar

其中 @sources 表示參考檔案 sources 裡的路徑。此外,引入的 library 除了 android-10/android.jar (Android 2.3.3) 外,還包含前一步驟所編譯的檔案 iiabs.jar。完成後接著輸入 :

jar cvfM ./iabhelper.jar com/

產生的 iabhelper.jar 放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下( 此範例將檔案置於 /Users/macaronics/UnityIabProj/Assets/Plugins/Android/) 。


3. 處理 onActivityResult 的結果訊息

a. 為了讓 IabHelper 能在 Activity 結束時處理 Activity 的結果,這裡我們需要自己撰寫繼承 UnityPlayerActivity 的類別來將結果 Relay 給 IabHelper。底下是範例的程式碼 :
?
package com.macaronics.iab;
 
import com.unity3d.player.UnityPlayerActivity;
 
import android.os.Bundle;
import android.util.Log;
import android.content.Intent;
 
public class overrideActivity extends UnityPlayerActivity {
     public interface cbEvent{
         public boolean cbEvent( int requestCode, int resultCode, Intent data);
     }
 
     protected cbEvent ie;
     static protected overrideActivity inst;
     protected void onCreate(Bundle savedInstanceState) {
         super .onCreate(savedInstanceState);
         inst = this ;
         
         // print debug message to logcat
         Log.d( "overrideActivity" , "onCreate called!" );
     }
 
     @Override
     public void onDestroy(){
         super .onDestroy();
         inst = null ;
         Log.d( "overrideActivity" , "onDestroy called!" );
     }
 
     @Override
     public void onActivityResult( int requestCode, int resultCode, Intent data){
         Log.d( "overrideActivity" , "onActivityResult called!" );
         
         boolean ret = false ;
         if (ie != null ){
             try {
                 ret =ie.cbEvent(requestCode, resultCode, data);
             }
             catch (Exception e){
                 ret = false ;
             }
         }
 
         if (ret == false ){
             super .onActivityResult(requestCode, resultCode, data);
         }
     }
     
     static public void registerOnActivityResultCBFunc( final cbEvent pcbfunc){
         if (inst != null )
             inst.ie =pcbfunc;
     }
}
其中函數 registerOnActivityResultCBFunc 只能記憶單一個 call back function,讀者可以把功能擴充成使用佇列來記憶更多的 call back function。

b. 同樣的編譯這個 class :

javac ./overrideActivity.java -cp /Users/macaronics/android-sdks/platforms/android-10/android.jar:/Applications/Unity/Unity.app/Contents/PlaybackEngines/AndroidPlayer/bin/classes.jar -d .

其中引入的 library 有 android-10/android.jar (Android 2.3.3),及 Unity 的 classes.jar。然後輸入 :

jar cvfM ./overrideActivity.jar com/

產生的 overrideActivity.jar 放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下( 此範例將檔案置於 /Users/macaronics/UnityIabProj/Assets/Plugins/Android/) 。


4. 撰寫 iabWrapper.java

a. 主要用於介接 IabHelper 及 Unity ,提供基本的下單功能。程式碼分別敘述如下 :
?
package com.macaronics.iab;
 
import com.unity3d.player.UnityPlayer;
 
import android.app.Activity;
import android.util.Log;
import android.os.Bundle;
import android.os.Looper;
import android.content.Intent;
 
import java.util.*;
 
import com.macaronics.iab.util.*;
import com.macaronics.iab.overrideActivity;
值得注意的是這裡引入 UnityPlayer,步驟2編譯的 IabHelper (com.macaronics.iab.util),及步驟3編譯的 overrideActivity (com.macaronics.iab.overrideActivity)。

底下是 class iabWrapper 的私有變數部分 :
?
public class iabWrapper{
     private Activity mActivity;
     private IabHelper mHelper;
     private String mEventHandler; 
     ...
其中 mEventHandler 是回呼 Unity 時,接收訊息的 GameObject 名稱。此 class 有一個 Constructor 函數,三個成員函數及兩個給 IabHelper 回呼的函數,分別敘述如下 :

Constructor 函數在呼叫時給予 GooglePlay API PublicKey (由 GooglePlay 提供),以及回呼 Unity 時,接收訊息的 GameObject 之名稱 : 
?
public iabWrapper(String base64EncodedPublicKey, String strEventHandler){
     mActivity =UnityPlayer.currentActivity;
     mEventHandler =strEventHandler;
 
     if (mHelper != null ){
         dispose();
     }
 
     mHelper = new IabHelper(mActivity, base64EncodedPublicKey);
     mHelper.enableDebugLogging( true );
 
     mHelper.startSetup( new IabHelper.OnIabSetupFinishedListener() {
         public void onIabSetupFinished(IabResult result){
             if (!result.isSuccess()){
                 //回呼 Unity GameObject 之函數 "msgReceiver", 並傳送字串訊息 (JSON 格式)
                 UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver" , "{\"code\":\"1\",\"ret\":\"false\",\"desc\":\"" +result.toString()+ "\"}" );
                 dispose();
                 return ;
             }
             
             //回呼 Unity GameObject 之函數 "msgReceiver", 並傳送字串訊息 (JSON 格式)
             UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver" , "{\"code\":\"1\",\"ret\":\"true\",\"desc\":\"" +result.toString()+ "\"}" );
 
             //register mHelper
             //向 overrideActivity 註冊 onActivityResult 回呼函數,並將資料 Relay 給 mHelper
             overrideActivity.registerOnActivityResultCBFunc(
                 new overrideActivity.cbEvent(){
                     public boolean cbEvent( int requestCode, int resultCode, Intent data)
                     {
                             
                         if (mHelper.handleActivityResult(requestCode, resultCode, data)){
                             return true ;
                         }
                         else {
                             return false ;
                         }
                     }
                 }
             );
         }
     });
}
其中的 UnitySendMessage 用來呼叫 Unity GameObject 之函數 "msgReceiver", 並傳送字串訊息 (JSON 格式)。此外值得注意的是此 Constructor 函數還向 overrideActivity 註冊用來接收 onActivityResult 訊息的回呼函數。底下的dispose 函數主要用於釋放 mHelper : 
?
public void dispose()
{
     if (mHelper != null )
     {
         mHelper.dispose();
     }
     mHelper = null ;
}
purchase 函數會啟動購買商品的介面,其中第一項參數是 Product SKU (在 GooglePlay 設定的產品 SKU),第二項是回呼 onActivityResult 函數時用來分辨購買動作的 reqCode,第三項是用來確認此筆講買是否合法的 payloadString (購買成功之後, GooglePlay 回覆的資訊裡會包含此字串。)。 
?
public void purchase(String strSKU, String reqCode, String payloadString)
{
     int intVal =Integer.parseInt(reqCode);
     if (mHelper != null )
         mHelper.launchPurchaseFlow(mActivity, strSKU, intVal, mPurchaseFinishedListener, payloadString);
}
底下是完成購買之後回呼的函數 (onIabPurchaseFinished),這裡把接收的資訊以 JSON 格式轉送給 Unity : 
?
IabHelper.OnIabPurchaseFinishedListener mPurchaseFinishedListener = new IabHelper.OnIabPurchaseFinishedListener() {
     public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
         if (result.isFailure()){
             UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver" , "{\"code\":\"2\",\"ret\":\"false\",\"desc\":\"\",\"sign\":\"\"}" );
             return ;
         }
   
         boolean ret = false ;
         String result_json = "" ;
         String result_sign = "" ;
         if (purchase != null ){
             ret = true ;
             result_json =purchase.getOriginalJson().replace( '\"' , '\'' );
             result_sign =purchase.getSignature();
         }
 
         UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver" , "{\"code\":\"2\",\"ret\":\"" +ret+ "\",\"desc\":\"" +result_json+ "\",\"sign\":\"" +result_sign+ "\"}" );
     }
};
產品在購買 (Purchase) 之後若再次購買則會失敗,在 Console 裡可以看到 "Item already owned" 訊息。若產品的類型屬於消耗性的產品,則需要執行 consume 指令才可再次購買 :
?
public void consume(String itemType, String jsonPurchaseInfo, String signature)
{
     String transedJSON =jsonPurchaseInfo.replace( '\'' , '\"' );
     if (mHelper == null )
         return ;
 
     Purchase pp = null ;
     try {
         pp = new Purchase(itemType, transedJSON, signature);
     }
     catch (Exception e){
         pp= null ;
     }
 
     if (pp != null ){
         final Purchase currpp =pp;
         mActivity.runOnUiThread( new Runnable(){
             public void run(){
                 mHelper.consumeAsync(currpp, mConsumeFinishedListener);
             }
         });
     }
}
其中參數 itemType 在此範例為 "inapp",jsonPurchaseInfo 及 signature 是在 purchase 完成後, IabHelper 回呼 onIabPurchaseFinished 所給的資料。consume 完成之後回呼的函數如下 :
?
IabHelper.OnConsumeFinishedListener mConsumeFinishedListener = new IabHelper.OnConsumeFinishedListener() {
     public void onConsumeFinished(Purchase purchase, IabResult result) {
         if (result.isSuccess()){
             Log.d( "iabWrapper" , "Consumption successful. Provisioning" );
             UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver" , "{\"code\":\"3\",\"ret\":\"true\",\"desc\":\"" +purchase.getOriginalJson().replace( '\"' , '\'' )+"\ ",\"sign\":\"" +purchase.getSignature()+ "\"}" );
         }
         else {
             UnityPlayer.UnitySendMessage(mEventHandler, "msgReceiver" , "{\"code\":\"3\",\"ret\":\"false\",\"desc\":\"\",\"sign\":\"\"}" );
         }
     }
};

b. 編譯 iabWrapper.java

javac ./iabWrapper.java -cp /Users/macaronics/android-sdks/platforms/android-10/android.jar:/Applications/Unity/Unity.app/Contents/PlaybackEngines/AndroidPlayer/bin/classes.jar:/Users/macaronics/UnityIabProj/Assets/Plugins/Android/iabhelper.jar:/Users/macaronics/UnityIabProj/Assets/Plugins/Android/overrideActivity.jar -d .

其中引入的 library 有 android-10/android.jar (Android 2.3.3), Unity 的 classes.jar,先前編譯的 iabhelper.jar 及 overrideActivity.jar。然後輸入 :

jar cvfM ./iabWrapper.jar com/

產生的 iabWrapper.jar 放在 Unity 專案的 Assets/Plugins/Android/ 資料夾底下( 此範例將檔案置於 /Users/macaronics/UnityIabProj/Assets/Plugins/Android/) 。


5. 設定 Permission ( AndroidManifest.xml )

若要執行 GooglePlay Billing 功能則要設定 AndroidManifest.xml,將底下的 AndroidManifest.xml 置於 /Users/macaronics/UnityIabProj/Assets/Plugins/Android 資料夾。
?
<? xml version = "1.0" encoding = "utf-8" ?>
< manifest android:versionCode = "1" android:versionName = "1.0"
           android:installLocation = "preferExternal"
           package = "com.macaronics.iab"
           xmlns:android = "http://schemas.android.com/apk/res/android" >
   < supports-screens android:anyDensity = "true" android:smallScreens = "true"
                     android:normalScreens = "true" android:largeScreens = "true"
                     android:xlargeScreens = "true" />
   < application android:label = "@string/app_name"
                android:icon = "@drawable/app_icon" android:debuggable = "false" >
     < activity android:label = "@string/app_name"
               android:name = "com.macaronics.iab.overrideActivity"
               android:screenOrientation = "portrait"
               android:configChanges =
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"             
               >
       < intent-filter >
         < action android:name = "android.intent.action.MAIN" />
         < category android:name = "android.intent.category.LAUNCHER" />
       </ intent-filter >
     </ activity >
     < activity android:label = "@string/app_name"
               android:name = "com.unity3d.player.UnityPlayerActivity"
               android:screenOrientation = "portrait"
               android:configChanges =
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
               />
     < activity android:label = "@string/app_name"
               android:name = "com.unity3d.player.UnityPlayerNativeActivity"
               android:screenOrientation = "portrait"
               android:configChanges =
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
               >
       < meta-data android:name = "android.app.lib_name" android:value = "unity" />
       < meta-data android:name = "unityplayer.ForwardNativeEventsToDalvik"
                  android:value = "false" />
     </ activity >
     < activity android:label = "@string/app_name"
               android:name = "com.unity3d.player.VideoPlayer"
               android:screenOrientation = "behind"
               android:configChanges =
"locale|mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|uiMode|screenSize|smallestScreenSize|fontScale"
               />
   </ application >
   < uses-feature android:glEsVersion = "0x20000" />
  
   < uses-permission android:name = "com.android.vending.BILLING" />
   < uses-permission android:name = "android.permission.INTERNET" />
   < uses-permission android:name = "android.permission.ACCESS_NETWORK_STATE" />
 
</ manifest >
注意其中的 package="com.macaronics.iab" 要改成你自己的 package 名稱,與 Unity 裡的 Bundle Identifier 相同。此外,這裡還設定了主要啟動的 Activity 為 overrideActivity,以及 uses-permission。 

到目前為止 /Users/macaronics/UnityIabProj/Assets/Plugins/Android 資料夾底下應該會有這些檔案 (iabhelper.jar, iabWrapper.jar, iiabs.jar, overrideActivity.jar, AndroidManifest.xml ) : 

到目前為止應該要有的檔案

6. Unity 部分的 iabWrapper.cs

利用 AndroidJNI 介接 Java 程式 (注意為了接收 java 回傳的字串訊息,此 script (component) 必須加入到名為 iabWrapper 的 GameObject 裡)。 :
?
using UnityEngine;
 
using System.Collections;
using System.Collections.Generic;
 
public class iabWrapper : MonoBehaviour
{
     public delegate void cbFunc( object [] retarr);
     cbFunc iabSetupCB = null ;
     cbFunc iabPurchaseCB = null ;
     cbFunc iabConsumeCB = null ;
 
     AndroidJavaObject mIABHelperObj = null ;
     static iabWrapper g_inst = null ;
 
     void Start(){
         g_inst = this ;
     }
 
     static public void init( string base64EncodedPublicKey, cbFunc tmpIabSetupCBFunc){
         if (g_inst == null )
             return ;
 
         g_inst.iabSetupCB =tmpIabSetupCBFunc;
 
         dispose();
         g_inst.mIABHelperObj = new AndroidJavaObject( "com.macaronics.iab.iabWrapper" , new object [2]{base64EncodedPublicKey, "iabWrapper" });
     }
 
     static public void dispose(){
         if (g_inst == null )
             return ;
 
         if (g_inst.mIABHelperObj != null ){
             g_inst.mIABHelperObj.Call( "dispose" );
             g_inst.mIABHelperObj.Dispose();
             g_inst.mIABHelperObj = null ;
         }
     }
 
         ...
其中 init 負責呼叫 iabWrapper.java 之建構函數並建立該物件,dispose 則是負責刪除釋放 iabWrapper.java 物件。底下是主要的兩個功能,purchase 和 consume : 
?
static public void purchase( string strSKU, int reqCode, string payload, cbFunc tmpIabPurchaseCBFunc){
     if (g_inst == null )
         return ;
 
     g_inst.iabPurchaseCB =tmpIabPurchaseCBFunc;
 
     if (g_inst.mIABHelperObj != null ){
         g_inst.mIABHelperObj.Call( "purchase" , new object [3]{strSKU, reqCode.ToString(), payload});
     }
}
 
static public void consume_inapp( string strPurchaseJsonInfo, string strSignature, cbFunc tmpIabConsumeCBFunc){
     if (g_inst == null )
         return ;
 
     g_inst.iabConsumeCB =tmpIabConsumeCBFunc;
 
     if (g_inst.mIABHelperObj != null ){
         g_inst.mIABHelperObj.Call( "consume" , new object [3]{ "inapp" , strPurchaseJsonInfo, strSignature});
     }
}
其中先將 callback 函數記錄起來,然後呼叫其對應的 java function。最後一部分是 msgReceiver,負責接收 java 回呼的結果並將資訊回呼對應的 callback function (先前呼叫 purchase 或是 consume 所給予的 callback function)。 
?
void msgReceiver( string msg){
     if (g_inst == null )
         return ;
  
     //parse json
     Dictionary< string , object > cache =(Dictionary< string , object >)MiniJSON.Json.Deserialize(msg);
 
     //dispatch msg
     if (cache.ContainsKey( "code" )== true ){
         int val =0;
         int .TryParse(( string )cache[ "code" ], out val);
         switch (val){
             case 0:{
                 //unknown
                 Debug.Log( "Unity-iabWrappe :cannot parse cache[code]" );
 
             }
             break ;
 
             case 1:{
                 //OnIabSetupFinishedListener
                 if (cache.ContainsKey( "ret" )== true ){
                     string retval =( string )cache[ "ret" ];
                     if (retval == "true" ){
                         //可使用
                         if (iabSetupCB != null ){
                             iabSetupCB( new object [1]{ true } );
                         }
 
                     }
                     else if (retval == "false" ){
                         //不可使用
                         if (iabSetupCB != null )
                         {
                             iabSetupCB( new object [1]{ false } );
                         }
                     } else {
                         Debug.Log( "Unity-iabWrapper :cannot parse cache[ret], code=1" );
                     }
                 }
             }
             break ;
 
             case 2:{
                 //onIabPurchaseFinished
                 if (cache.ContainsKey( "ret" )== true ){
                     string retval =( string )cache[ "ret" ];
                     if (retval == "true" ){
                         //可使用
                         if (iabPurchaseCB != null ){
                             iabPurchaseCB( new object [3]{ true , ( string )cache[ "desc" ], ( string )cache[ "sign" ]} );
                         }
 
                     }
                     else if (retval == "false" ){
                         //不可使用
                         if (iabPurchaseCB != null )
                         {
                             iabPurchaseCB( new object [3]{ false , "" , "" } );
                         }
 
                     }
                     else {
                         Debug.Log( "Unity-iabWrapper  :cannot parse cache[ret], code=2" );
                     }
                 }
             }
             break ;
 
             case 3:{
                 //OnConsumeFinishedListener
                 if (cache.ContainsKey( "ret" )== true ){
                     string retval =( string )cache[ "ret" ];
                     if (retval == "true" ){
                         //可使用
                         if (iabConsumeCB != null )
                         {
                             iabConsumeCB( new object [3]{ true , ( string )cache[ "desc" ], ( string )cache[ "sign" ]} );
                         }
 
                     } else if (retval == "false" ){
                         //不可使用
                         if (iabConsumeCB != null )
                         {
                             iabConsumeCB( new object [3]{ false , "" , "" } );
                         }
 
                     }
                     else {
                         Debug.Log( "Unity-iabWrapper :cannot parse cache[ret], code=3" );
 
                     }
                 }
             }
             break ;
         }
     }
}
程式碼先將 JSON 字串轉為 object 物件並依照物件種類做對應的處理,其中 purchase 的 callback (case 2),回傳的參數依序為 : 1. 是否成功,2. purchase 結果字串以及 3. purchase 結果之 signature (可作為驗証之用,參考 這篇)。 


7. 實作 IAB APP

建立一個 script 名為 main,並將此 script 加入至 Camera。這裡實作一個 purchase 功能,並在 purchase 完成之後立刻執行 consume。
?
using UnityEngine;
using UnityEngine;
using System.Collections;
  
public class main : MonoBehaviour {
  
     // Use this for initialization
     void Start () {
         iabWrapper.init(
             "PUBLIC_KEY" ,
             delegate ( object [] ret){
                 if ( true ==( bool )ret[0]){
                     Debug.Log( "iab successfully initialized" );
                 }
                 else {
                     Debug.Log( "failed to initialize iab" );
                 }
             });
     }
  
     void OnGUI(){
         if (GUI.Button( new Rect(0, 0, 100, 100), "purchase" )){
             iabWrapper.purchase( "PRODUCT_SKU" , 10001, "PRODUCT_SKU_AND_USER_ID_AND_DATE" ,
                 delegate ( object [] ret){
                     if ( false ==( bool )ret[0]){
                         Debug.Log( "purchase cancelled" );
                     }
                     else {
                         string purchaseinfo =( string )ret[1];
                         string signature =( string )ret[2];
                         iabWrapper.consume_inapp(purchaseinfo, signature,
                             delegate ( object [] ret2){
                                 if ( false ==( bool )ret2[0])
                                 {
                                     Debug.Log( "failed to consume product" );
                                 }
                             });
                     }
                 });
         }
     }
  
     void OnApplicationQuit(){
         iabWrapper.dispose();
     }
}
其中的 PUBLIC_KEY,更換成你的 Public Key,代入 purchase 函數的參數更改成你的參數。範例原始碼可以從這裡取得 : http://github.com/phardera/unity3d_googleplay_iab.git 
unity playforward
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值