IAP(程序內購買): 完全攻略

第一印象覺得In-App Purchase(簡稱IAP)非常簡單。Apple提供的大量文檔應該讓開發者很快熟悉地熟悉。那麼,爲什麼在你的應用中集成IAP特性就如此令人生厭呢?
 這是因爲在開發過程中不可避免會出現一些錯誤。而但這些錯誤發生的時候,你就抓瞎了。雖然Apple提供了有關IAP的大量文檔,但他們並未提及集成IAP的詳細步驟。而且對StoreKit集成過程中出現的問題也沒有一個核對清單。另外對於爲什麼諸如產品ID非法之類的問題也沒有提供NSError之類的對象來告訴你原因。

在試用了各種可能的解決方案後,你只能身心疲憊,彷徨無助。

爲了提高你的效率和減少你的痛苦,我覺定利用此文來介紹一下實現IAP的詳細步驟。本文很詳細,有點長。甚至可能太長了,但不像Apple的文檔,它提供了爲實現IAP的每一個步驟。

廢話少說,我們直入主題吧。

概況

IAP能正常工作的祕訣:分成兩個步驟:

  1. 創建及提取產品描述
  2. 購買產 品

第一個步驟是你可能遇到問題的部分。一旦你在代碼中成功地獲取了產品描述,編寫購買產品的代碼不過是小菜一碟。

我們先看看步驟1。

創建及提取產品描述

下面是有關創建產品及提取其描述的非常粗略的步驟:

  1. 創建唯一的App ID
  2. 生成及安裝新的provisioning profile文件
  3. 在Xcode中更新 bundle ID 及 code signing profile
  4. 如果還沒做的話,請在iTunes Connect中提交有關你程序的 metadata
  5. 如果還沒做的話,請在iTunes Connect中提交你程序的二進制碼
  6. 爲IAP添加新產品
  7. 編寫提取產品描述的代碼
  8. 等待幾小時

提取產品描述的代碼非常簡單,但其他步驟則很容易錯。

注意: 爲提取產品描述,你並需要在iTunes Connect中創建IAP測試用戶。

1. 創建唯一的App ID

爲支持IAP,你的App ID不能包括通配符(“*”)。爲確定你的App Id是否包括通配符,請登錄http://developer.apple.com/iphone,在 iPhone Developer Program Portal中選擇左邊菜單中的 “App IDs”檢查你的 App ID。

下面是一個唯一的App ID:

7DW89RZKLY.com.runmonster.runmonsterfree

下面是一個唯一的 App ID:

7DW89RZKLY.com.runmonster.* 

如果你還沒有一個唯一的App ID,按如下步驟創建一個:

  1. 在developer portal中的 App IDs 部分,選擇“New App ID”
  2. 填寫下列信息:
    • Display name(顯示名): 選取一個不同的App ID的名稱。你不能編輯或刪除舊的App ID,所以你必須爲你的App ID提供一個新名稱以避免混淆。
    • Prefix(前綴): 生成一個新的前綴,或者如果你的程序是通過Keychain Services API分享數據的系列程序中之一的話,則選用已存在的前綴。
    • Suffix(後綴): com.companyname.appname (這是通用格式 – 注意沒有使用通配符)。
  3. 按 “Save”
  4. 按 App ID旁的“Configure” 鏈接
  5. 選取 “Enable In App Purchase”選擇框
  6. 按“Done”

2. 創建一個新的Provisioning Profile文件

在創建了新的App ID後,你需要生成一個指向這個App ID的新provisioning profile。

下面就是令人痛苦的生成和安裝新provisioning profile的詳細步驟:

  1. 在 iPhone Developer Portal中, 選擇左邊的Provisioning部分
  2. 確保你處於Development 標籤下, 按下右上角的 “New Profile”
  3. 填入所需信息並指向你剛創建的唯一的App ID
  4. 如果你在Actions條目下看到 “Pending”,那麼請按下“Development”標籤標題進行刷新
  5. 點擊 “Download” 下載新的profile文件
  6. 將profile文件拖入到Dock中Xcode圖標上進行安裝
  7. 如果你想在硬盤上保存provisioning profile,那麼你可以按如下步驟手工安裝profile:
    1. 在Xcode中, 選擇 Window > Organizer
    2. 選擇左邊 “Provisioning Profiles” 分類
    3. Ctrl-按下profile > Reveal in Finder
    4. 將新profile拖入到 profile Finder 窗口

3. 更新Xcode 設置

在Xcode中安裝了 profile 文件後,你需要對使用此provisiong profile的項目進行一些編輯工作:

  1. 編輯項目 .plist 文件使其 Bundle ID 與 App ID 匹配。忽略ID開始部分的字母數字序列。例如,在Developer Portal中你的App ID爲“7DW89RZKLY.com.runmonster.runmonsterfree”,那麼在Bundle ID中你只需輸入“com.runmonster.runmonsterfree” 。
  2. 編輯項目的 target 信息以使用新的provisioning profile:
    1. 選取 Project > Edit Active Target
    2. 選取頂部“Build” 標籤
    3. 選取需要的 configuration (通常爲 Debug)
    4. Code Signing Identity中選擇新的provisioning profile
    5. Code Signing Identity之下的行中(可能名爲 Any iPhone OS Device)選擇新的provisioning profile

4. 添加你的應用程序

如果你的程序已經發表到App Store了,那麼可以略過此步驟。

在你將產品添加到 iTunes Connect之前,你必須添加此產品所需的程序。如果你的程序還沒有100%完成也無需擔心,你可以先提交具有部分數據的程序,最後再提交真實的程序。

注意: 只有 SKU 和 version(版本)部分是以後不可修改的

  1. 登錄到 http://developer.apple.com/iphone
  2. 點擊右邊鏈接進入 iTunes Connect
    • 注意:你必須先登錄到developer.apple.com,否則會有不測發生(譯者注:具體是什麼不測我也不太清楚,膽大的請自己試一下)
  3. 在 iTunes Connect主頁點擊 “Manage Your Applications”
  4. 在右上角點擊“Create New Application”
  5. 填寫程序所需的一切信息。當要求程序二進制碼時,請選擇稍後上傳選項。

5. 提交程序二進制碼

Apple的文檔中沒有任何地方提及詳情,但它卻是必須的步驟。要成功測IAP功能,你必須提交程序的二進制碼。即使你的程序還沒有100%完成,你仍然需要提交二進制碼。然而,你也可以立即擯棄你的二進制碼,使其不會進入審覈階段。

下面這些步驟非常關鍵,我可是因爲少做了某些步驟而度過了一段非常痛苦的時間:

  1. 生成App Store發佈版程序
    • 如果你不知怎麼做,請在 iPhone Developer Portal 中點擊左方的 Distribution標籤,並選擇 “Prepare App” 標籤。然後,根據藍色鏈接的指示:
      • 獲取iPhone發行許可證
      • 創建並下載在App Store發行所需的iPhone Distribution Provisioning Profile
      • 在Xcode中生成程序的發行版
  2. 在iTunes Connect中進入程序頁
  3. 選擇 “Upload Binary”
  4. 上傳.zip壓縮程序
  5. 如果你的程序還沒有100%完成以進行審覈,那麼請點擊iTunes Connect中你程序首頁中的 “Reject Binary”鏈接。程序的狀態應該更新爲 “Developer Rejected”.

不用擔心,由於程序的狀態是“Developer Rejected”,Apple是不會對其進行審覈的。你可以在任何時候提交程序的新版本並使其狀態爲“Developer Rejected”,這不會對以後程序正式提交的等待時間有任何影響。

6. 添加產品

完成了以上所有步驟後,我們最終可以向iTunes Connect中添加產品了。

  1. 確保登錄到 http://developer.apple.com/iphone
  2. 進入 iTunes Connect 主頁
  3. 點擊 “Manage Your in App Purchases” 鏈接
  4. 點擊 “Create New”
  5. 選擇你的程序
  6. 填寫下列產品信息:
    • Reference Name(參考名稱): 產品的通用名稱。比如,我使用的是 “Pro Upgrade”。此名稱是不允許進行編輯的,它不會顯示於App Store中。
    • Product ID(產品ID): 你產品的唯一id。通常格式是 com.company.appname.product,但它可以說任何形式。它並不要求以程序的App ID作爲前綴。
    • Type(類型): 有三種選擇
      • Non-consumable(非消耗品): 僅需付費一次 (例如你希望將出現從免費版升級爲專業版)
      • Consumable(消耗品): 每次下載都需要付費
      • Subscription(預訂): 循環反覆
    • Price Tier(價格等級): 產品價格。參見不同等級的價格列表。
    • Cleared for Sale(等待銷售): 一定要選取此項,否則的話,測試時會發生非法產品ID的錯誤。
    • Language to Add(增加的語言): 選一項。下列兩項將出現:
      • Displayed Name(顯示名稱): 用戶看到的產品名稱。比如我選擇 “Upgrade to Pro”。
      • Description(描述): 對產品進行描述。此處輸入的文本將與Displayed Name 及 Price 一起在你代碼中提取 SKProduct時出現。
    • Screenshot(截屏): 展示你產品的截屏。儘管屏幕上會顯示“提交截屏會觸發產品審覈過程”之類的文字(個人拙見,這是非常糟糕的設計),你還是可以安全地提交截屏而不會使產品進入審覈過程。存儲後,選擇“Submit with app binary” (隨程序二進制碼一起提交)選項。是產品與程序二進制綁定在一起,所以在你最後正式提交100%完成的程序二進制碼時,產品也會隨之提交。
  7. 點擊 “Save” 

7. 編寫代碼

下面我們開始編寫代碼對剛加入到iTunes Connect中的產品信息進行提取。我訪問產品數據,我們需要使用 StoreKit framework。

注意: StoreKit 無法在模擬器上工作。你必須在真機上進行測試。

  1. 1.添加 StoreKit framework 到你的項目中。
  2. 2.添加SKProduct引用到你的 .h 文件中:
  1. // InAppPurchaseManager.h  
  2.  
  3. #import <StoreKit/StoreKit.h>  
  4.  
  5. #define kInAppPurchaseManagerProductsFetchedNotification @"kInAppPurchaseManagerProductsFetchedNotification"  
  6.  
  7. @interface InAppPurchaseManager : NSObject <SKProductsRequestDelegate>  
  8. {  
  9.     SKProduct *proUpgradeProduct;  
  10.     SKProductsRequest *productsRequest;  

注意: InAppPurchaseManager 是一個單例類,它處理程序中所有IAP任務。它是本文中的示例程序。

  1. 3.產品請求,並在相應.m文件中實現代理協議:
  1. // InAppPurchaseManager.m  
  2.  
  3. - (void)requestProUpgradeProductData  
  4. {  
  5.     NSSet *productIdentifiers = [NSSet setWithObject:@"com.runmonster.runmonsterfree.upgradetopro" ];  
  6.     productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];  
  7.     productsRequest.delegate = self;  
  8.     [productsRequest start];  
  9.       
  10.     // we will release the request object in the delegate callback  
  11. }  
  12.  
  13. #pragma mark -  
  14. #pragma mark SKProductsRequestDelegate methods  
  15.  
  16. - (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response  
  17. {  
  18.     NSArray *products = response.products;  
  19.     proUpgradeProduct = [products count] == 1 ? [[products firstObject] retain] : nil;  
  20.     if (proUpgradeProduct)  
  21.     {  
  22.         NSLog(@"Product title: %@" , proUpgradeProduct.localizedTitle);  
  23.         NSLog(@"Product description: %@" , proUpgradeProduct.localizedDescription);  
  24.         NSLog(@"Product price: %@" , proUpgradeProduct.price);  
  25.         NSLog(@"Product id: %@" , proUpgradeProduct.productIdentifier);  
  26.     }  
  27.       
  28.     for (NSString *invalidProductId in response.invalidProductIdentifiers)  
  29.     {  
  30.         NSLog(@"Invalid product id: %@" , invalidProductId);  
  31.     }  
  32.       
  33.     // finally release the reqest we alloc/init’ed in requestProUpgradeProductData  
  34.     [productsRequest release];  
  35.       
  36.     [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerProductsFetchedNotification object:self userInfo:nil];  

上面代碼有幾點需要注意:

  • 指定產品id時,你必須使用完整產品id。例如,上例中使用 “com.runmonster.runmonsterfree.upgradetopro”。僅使用 “upgradetopro” 將不會正常工作。
  • 如果在productsRequest:didReceiveResponse:中response.products 爲 nil,而你的產品id出現於 response.invalidProductIdentifers 數組中時,那麼請做好心理準備開始一場徒勞的搜索戰吧。 StoreKit API沒有提供任何幫助,也沒有任何指示關於爲什麼你的id是無效的。很可愛,不是嗎?
  • SKProduct類提供了有關程序標題和描述的本地化版本,但是價格則沒有本地化版本。下面是針對此疏忽提供的代碼:
  1. // SKProduct+LocalizedPrice.h  
  2.  
  3. #import <Foundation/Foundation.h>  
  4. #import <StoreKit/StoreKit.h>  
  5.  
  6. @interface SKProduct (LocalizedPrice)  
  7.  
  8. @property (nonatomic, readonly) NSString *localizedPrice;  
  9.  
  10. @end 
  1. // SKProduct+LocalizedPrice.m  
  2.  
  3. #import "SKProduct+LocalizedPrice.h"  
  4.  
  5. @implementation SKProduct (LocalizedPrice)  
  6.  
  7. - (NSString *)localizedPrice  
  8. {  
  9.     NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];  
  10.     [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];  
  11.     [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];  
  12.     [numberFormatter setLocale:self.priceLocale];  
  13.     NSString *formattedString = [numberFormatter stringFromNumber:self.price];  
  14.     [numberFormatter release];  
  15.     return formattedString;  
  16. }  
  17.  
  18. @end 

加入上述代碼,測試一下。你應該在控制檯窗口中看見產品信息了。然而更大的可能是,你得到了一個無效的產品id。我下一篇文章將介紹怎樣對這個問題進行調試。但是,下面的步驟8有可能是阻礙你前進的障礙。

8. 等待幾小時

遵循了上述所有步驟,但是你的產品仍然是無效的?你是否兩次,三次,四次不懈努力地確認你是否遵循了上面提到的每個步驟?你是否已經對網上IAP信息少得可憐而感到絕望?

那麼,你應該等待。

你的產品要進入iTunes Connect使得Apple準備好沙箱環境需要一些時間。對於我而言,我是經過了無數次產品無效錯誤的絕望。而在24小時後,我沒有修改任何一行代碼,但產品id變爲有效。我認爲要使產品發佈到Apple的網絡系統需要幾個小時的時間,但如果你有時間的話,你可以像我一樣等上24個小時。

購買產品

至此你應該已經成功地獲取了 SKProduct 描述。比較而言,支持購買產品相對簡單些。僅需下面三個步驟:

  1. 編寫代碼支持事務(transaction)
  2. 在iTunes Connect中添加程序測試用戶
  3. 在設備中登錄你的 iTunes Store 帳號
  4. 購買測試

我們從編寫支持事務所需代碼開始。

1. 編寫代碼支持事務

首先注意:你將負責開發產品購買的用戶界面。StoreKit 未提供任何與用戶界面相關的元素。如果你希望你的購買用戶界面與App Store一樣,那麼你要自己完成。

下面所有代碼都是有關事務處理的後臺部分。這是一個單獨的類只有一條簡單的API以供外部類(比如view controller)調用進行購買。如果你找到將其集成到你程序的購買部分的方法,那麼我推薦你使用類似方案。

首先,需要遵循 SKPaymentTransactionObserver 協議:

  1. // InAppPurchaseManager.h  
  2.  
  3. // add a couple notifications sent out when the transaction completes  
  4. #define kInAppPurchaseManagerTransactionFailedNotification @"kInAppPurchaseManagerTransactionFailedNotification"  
  5. #define kInAppPurchaseManagerTransactionSucceededNotification @"kInAppPurchaseManagerTransactionSucceededNotification"  
  6.  
  7. …  
  8.  
  9. @interface InAppPurchaseManager : NSObject <SKProductsRequestDelegate, SKPaymentTransactionObserver>  
  10. {  
  11.     …  
  12. }  
  13.  
  14. // public methods  
  15. - (void)loadStore;  
  16. - (BOOL)canMakePurchases;  
  17. - (void)purchaseProUpgrade;  
  18.  
  19. @end 

上面我們定義了兩個新的notification,它們將作爲購買事務的結果被髮送。在上例中我們仍然使用與獲取產品描述同一個InAppPurchaseManager類。

  1. // InAppPurchaseManager.m  
  2.  
  3. #define kInAppPurchaseProUpgradeProductId @"com.runmonster.runmonsterfree.upgradetopro"  
  4.  
  5. …  
  6.  
  7. #pragma -  
  8. #pragma Public methods  
  9.  
  10. //  
  11. // call this method once on startup  
  12. //  
  13. - (void)loadStore  
  14. {  
  15.     // restarts any purchases if they were interrupted last time the app was open  
  16.     [[SKPaymentQueue defaultQueue] addTransactionObserver:self];  
  17.       
  18.     // get the product description (defined in early sections)  
  19.     [self requestProUpgradeProductData];  
  20. }  
  21.  
  22. //  
  23. // call this before making a purchase  
  24. //  
  25. - (BOOL)canMakePurchases  
  26. {  
  27.     return [SKPaymentQueue canMakePayments];  
  28. }  
  29.  
  30. //  
  31. // kick off the upgrade transaction  
  32. //  
  33. - (void)purchaseProUpgrade  
  34. {  
  35.     SKPayment *payment = [SKPayment paymentWithProductIdentifier:kInAppPurchaseProUpgradeProductId];  
  36.     [[SKPaymentQueue defaultQueue] addPayment:payment];  
  37. }  
  38.  
  39. #pragma -  
  40. #pragma Purchase helpers  
  41.  
  42. //  
  43. // saves a record of the transaction by storing the receipt to disk  
  44. //  
  45. - (void)recordTransaction:(SKPaymentTransaction *)transaction  
  46. {  
  47.     if ([transaction.payment.productIdentifier isEqualToString:kInAppPurchaseProUpgradeProductId])  
  48.     {  
  49.         // save the transaction receipt to disk  
  50.         [[NSUserDefaults standardUserDefaults] setValue:transaction.transactionReceipt forKey:@"proUpgradeTransactionReceipt" ];  
  51.         [[NSUserDefaults standardUserDefaults] synchronize];  
  52.     }  
  53. }  
  54.  
  55. //  
  56. // enable pro features  
  57. //  
  58. - (void)provideContent:(NSString *)productId  
  59. {  
  60.     if ([productId isEqualToString:kInAppPurchaseProUpgradeProductId])  
  61.     {  
  62.         // enable the pro features  
  63.         [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"isProUpgradePurchased" ];  
  64.         [[NSUserDefaults standardUserDefaults] synchronize];  
  65.     }  
  66. }  
  67.  
  68. //  
  69. // removes the transaction from the queue and posts a notification with the transaction result  
  70. //  
  71. - (void)finishTransaction:(SKPaymentTransaction *)transaction wasSuccessful:(BOOL)wasSuccessful  
  72. {  
  73.     // remove the transaction from the payment queue.  
  74.     [[SKPaymentQueue defaultQueue] finishTransaction:transaction];  
  75.       
  76.     NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:transaction, @"transaction" , nil];  
  77.     if (wasSuccessful)  
  78.     {  
  79.         // send out a notification that we’ve finished the transaction  
  80.         [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerTransactionSucceededNotification object:self userInfo:userInfo];  
  81.     }  
  82.     else 
  83.     {  
  84.         // send out a notification for the failed transaction  
  85.         [[NSNotificationCenter defaultCenter] postNotificationName:kInAppPurchaseManagerTransactionFailedNotification object:self userInfo:userInfo];  
  86.     }  
  87. }  
  88.  
  89. //  
  90. // called when the transaction was successful  
  91. //  
  92. - (void)completeTransaction:(SKPaymentTransaction *)transaction  
  93. {  
  94.     [self recordTransaction:transaction];  
  95.     [self provideContent:transaction.payment.productIdentifier];  
  96.     [self finishTransaction:transaction wasSuccessful:YES];  
  97. }  
  98.  
  99. //  
  100. // called when a transaction has been restored and and successfully completed  
  101. //  
  102. - (void)restoreTransaction:(SKPaymentTransaction *)transaction  
  103. {  
  104.     [self recordTransaction:transaction.originalTransaction];  
  105.     [self provideContent:transaction.originalTransaction.payment.productIdentifier];  
  106.     [self finishTransaction:transaction wasSuccessful:YES];  
  107. }  
  108.  
  109. //  
  110. // called when a transaction has failed  
  111. //  
  112. - (void)failedTransaction:(SKPaymentTransaction *)transaction  
  113. {  
  114.     if (transaction.error.code != SKErrorPaymentCancelled)  
  115.     {  
  116.         // error!  
  117.         [self finishTransaction:transaction wasSuccessful:NO];  
  118.     }  
  119.     else 
  120.     {  
  121.         // this is fine, the user just cancelled, so don’t notify  
  122.         [[SKPaymentQueue defaultQueue] finishTransaction:transaction];  
  123.     }  
  124. }  
  125.  
  126. #pragma mark -  
  127. #pragma mark SKPaymentTransactionObserver methods  
  128.  
  129. //  
  130. // called when the transaction status is updated  
  131. //  
  132. - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions  
  133. {  
  134.     for (SKPaymentTransaction *transaction in transactions)  
  135.     {  
  136.         switch (transaction.transactionState)  
  137.         {  
  138.             case SKPaymentTransactionStatePurchased:  
  139.                 [self completeTransaction:transaction];  
  140.                 break;  
  141.             case SKPaymentTransactionStateFailed:  
  142.                 [self failedTransaction:transaction];  
  143.                 break;  
  144.             case SKPaymentTransactionStateRestored:  
  145.                 [self restoreTransaction:transaction];  
  146.                 break;  
  147.             default:  
  148.                 break;  
  149.         }  
  150.     }  

 要測試上面的新代碼,你還需要編寫調用 loadStore, canMakePurchases 以及 purchaseProUpgrade 方法的代碼。

有關上述代碼的詳細解釋,請參考官方 In App Purchase Programming Guide (IAP編程指南)

上述代碼有幾個部分是針對我的程序的。例如,在 provideContent:中,NSUserDefaults 中的@”isProUpgradePurchased” BOOL 字段被設定爲 YES。程序的其他部分將檢查此BOOL值以確定是否需要啓動專業版功能。如果你正好也要實現免費升級專業版的功能,那麼你可以使用同樣的方法。

2. 添加測試用戶

爲測試上述代碼,你需要在 iTunes Connect 中創建測試用戶以對IAP功能進行測試。你可以使用測試帳號購買產品而不被Apple收取費用。

按以下步驟創建測試用戶:

  1. 登錄到 http://developer.apple.com/iphone
  2. 進入 iTunes Connect
  3. 選擇iTunes Connect首頁中的 “Manage Users”
  4. 選擇 “In App Purchase Test User”
  5. 選擇 “Add New User”
  6. 填入用戶信息. 所有信息都不必是合法的。建議使用虛假簡短的email地址及簡短的密碼。
  7. 選擇 “Save”

測試時你需要輸入這些email地址和密碼。

3. 在你的設備中退出登錄

在進行程序購買功能測試前,你必須在你的設備中退出iTunes Store。遵循以下步驟:

  1. 打開Settings App
  2. 點擊 “Store” 行
  3. 點擊 “Sign Out” 

4. 購買測試

現在,終於可以開始進行IAP功能的測試了。測試很簡單:

  1. 運行你設備中的程序
  2. 進行購買
  3. 當程序提示輸入用戶名和密碼時,輸入參數用戶的信息

如果你使用同一賬戶進行購買時,系統將提示你已經購買了此產品。按“Yes”就可以再次下載此產品。

總結

實現IAP功能比想象的要複雜許多。我可是經過無數痛苦的經歷才完成我的程序。希望能夠幫助其他開發者減輕他們的痛苦。

原文見:In App Purchases: A Full Walkthrough

本文轉自:IAP(程序內購買): 完全攻略

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章