Swift蘋(píng)果內(nèi)購(gòu)IAP(余額充值場(chǎng)景)

前言

老夫接到一個(gè)項(xiàng)目,需要蘋(píng)果內(nèi)購(gòu)做充值壤躲,經(jīng)過(guò)一周多的努力把核心代碼完成城菊,希望對(duì)大家有所幫助。

效果圖

廢話不多說(shuō)碉克,直接上圖

余額充值頁(yè)面

流程說(shuō)明

  • 正常充值流程
    1凌唬、【可選】App詢問(wèn)服務(wù)端是否可以請(qǐng)求充值A(chǔ)CG_PAY_50(50元)
    2、若允許漏麦,App向蘋(píng)果發(fā)起支付(50元)
    3客税、蘋(píng)果支付成功,返回憑證(payload)到App
    4.1撕贞、App使用payload請(qǐng)求本接口更耻,
    4.2、服務(wù)端使用payload到蘋(píng)果拿到下面Json數(shù)據(jù)
    4.3捏膨、校驗(yàn)Json數(shù)據(jù)中transaction_id是否已經(jīng)使用
    4.4秧均、若未使用食侮,則給用戶充值(50元),生成充值記錄
    4.5目胡、處理結(jié)束锯七,返回成功(code=success)
    5、App把收到憑證從本地清除誉己,完成支付眉尸,刷新余額

  • 異常情況:
    1、若充值過(guò)程(第3步)退出App巨双,可充值成功效五,余額未給用戶增加,再次打開(kāi)App完成后續(xù)步驟即可
    2炉峰、若校驗(yàn)過(guò)程(第4步)網(wǎng)絡(luò)斷開(kāi)畏妖,則可能校驗(yàn)成功,余額增加疼阔,App未收到校驗(yàn)信息戒劫,再次打開(kāi)App仍然會(huì)發(fā)布充值,服務(wù)端判斷transaction_id為重復(fù)婆廊,忽略即可迅细。
    3、校驗(yàn)返回碼每次都不成功時(shí)淘邻,App不會(huì)清除憑證茵典,會(huì)造成每次打開(kāi)App都校驗(yàn),該商品也無(wú)法再次購(gòu)買成功(系統(tǒng)提示:已經(jīng)支付宾舅,可以恢復(fù)購(gòu)買)统阿。

核心代碼,稍作修改即可復(fù)用

  • IAPHelper.swift
//
//  IAPHelper.swift
//  IAP
//
//  Created by lin bo on 2019/5/13.
//  Copyright ? 2019 appTech. All rights reserved.
//

import UIKit
import StoreKit

/// 商品列表
enum ACG_PAY_ID: String {
    
    case pay50 = "ACG_PAY_50"
    case pay98 = "ACG_PAY_98"
    case pay148 = "ACG_PAY_148"
    case pay198 = "ACG_PAY_198"
    case pay248 = "ACG_PAY_248"
    case pay298 = "ACG_PAY_298"

    func price() -> Int {
        
        switch self {
            
        case .pay50: return 50
        case .pay98: return 98
        case .pay148: return 148
        case .pay198: return 198
        case .pay248: return 248
        case .pay298: return 298
        }
    }
}

/// 回調(diào)狀態(tài)
enum IAPProgress: Int {
    
    /// 初始狀態(tài)
    case none
    /// 開(kāi)始
    case started
    /// 購(gòu)買中
    case purchasing
    
    /// 支付成功
    case purchased
    /// 失敗
    case payFailed
    /// 重復(fù)購(gòu)買
    case payRestored
    /// 狀態(tài)未確認(rèn)
    case payDeferred
    /// 其他
    
    case payOther
    /// 開(kāi)始后端校驗(yàn)
    case checking
    /// 后端校驗(yàn)成功
    case checkedSuccess
    /// 后端校驗(yàn)失敗
    case checkedFailed

}

enum IAPPayCheck {
    
    case busy /// 有支付正在進(jìn)行
    case notInit /// 未初始化
    case initFailed /// 初始化失敗
    case notFound /// 沒(méi)有找到該商品筹我,中斷
    case systemFailed /// 系統(tǒng)檢測(cè)失敗
    case ok /// 可以進(jìn)行

}

class IAPHelper: ATBaseHelper {
    
    static let shared = IAPHelper()
    
    /// 檢測(cè)初始化回調(diào)
    fileprivate var checkBlock: ((_ b: IAPPayCheck) -> ())?
    /// 支付過(guò)程回調(diào)
    var resultBlock: ((_ type: IAPProgress, _ pID: ACG_PAY_ID?) -> ())?
    
    /// 是否正在支付
    fileprivate var isBusy: Bool {
        get {
            switch progress {
            case .none:
                return false
            default:
                return true
            }
        }
    }
   
    /// 購(gòu)買的狀態(tài)
    fileprivate var progress: IAPProgress = .none {
        didSet {
            /// 狀態(tài)改變回調(diào)
            if let block = resultBlock {
                block(progress, currentPID)
            }
        }
    }
    
    /// 當(dāng)前付費(fèi)的ID
    fileprivate var currentPID: ACG_PAY_ID?
    /// 商品列表
    fileprivate var productList: [SKProduct]?

    /// 初始化配置扶平,請(qǐng)求商品
    func config() {
        
        SKPaymentQueue.default().add(self)
        requestAllProduct()
    }
    
    /// 初始化,請(qǐng)求商品列表
    func initPayments(_ block: @escaping ((_ b: IAPPayCheck) -> ())) {
        
        let c = checkPayments()
        
        if c == .notInit {
            
            requestAllProduct()
            checkBlock = block

        }else {
            
            block(c)
        }
    }
    
    /// 檢測(cè)支付環(huán)境蔬蕊,非.ok不允許充值
    func checkPayments() -> IAPPayCheck {
        
        guard isBusy == false else {
            return .busy
        }
        
        guard let plist = productList, !plist.isEmpty else {
            return .notInit
        }
        
        guard SKPaymentQueue.canMakePayments() else {
            return .systemFailed
        }
        
        return .ok
    }
    
    /// 請(qǐng)求商品列表
    private func requestAllProduct() {
        
        let set: Set<String> = [ACG_PAY_ID.pay50.rawValue,
                        ACG_PAY_ID.pay98.rawValue,
                        ACG_PAY_ID.pay148.rawValue,
                        ACG_PAY_ID.pay198.rawValue,
                        ACG_PAY_ID.pay248.rawValue,
                        ACG_PAY_ID.pay298.rawValue]
        
        let request = SKProductsRequest(productIdentifiers: set)
        request.delegate = self
        request.start()
    }
    
    /// 支付商品
    @discardableResult
    func pay(pID: ACG_PAY_ID) -> IAPPayCheck {
        
        let c = checkPayments()
        
        if c == .ok {
            
            guard let plist = productList, !plist.isEmpty else {
                return .notInit
            }

            let pdts = plist.filter {
                return $0.productIdentifier == pID.rawValue
            }
            
            guard let product = pdts.first else {
                return .notFound
            }
            
            currentPID = pID
            requestProduct(pdt: product)
        }
        
        return c
    }
    
    /// 請(qǐng)求充值
    fileprivate func requestProduct(pdt: SKProduct) {
        
        progress = .started

        let pay: SKMutablePayment = SKMutablePayment(product: pdt)
        SKPaymentQueue.default().add(pay)
    }
    
    /// 重置
    fileprivate func payFinish() {
        
        currentPID = nil
        progress = .none
    }
    
    /// 充值完成后給后臺(tái)校驗(yàn)
    func completeTransaction(_ checkList: [SKPaymentTransaction]) {
        
        if resultBlock == nil {
            showAlert("充值校驗(yàn)中...")
        }
        ALog("充值校驗(yàn)中...")
        progress = .checking
        
        guard let rURL = Bundle.main.appStoreReceiptURL, let data = try? Data(contentsOf: rURL) else {
            ALog("appStoreReceiptURL error")
            
            progress = .checkedFailed
            payFinish()
            return
        }
        
        let str = data.base64EncodedString()
        print(str)
        
        OrderServer.shared.requestCheckIAP(str) { [weak self] (code, msg, result) in
            
            guard let helper = self else {
                return
            }
            
            if result { // 成功則刪除
                checkList.forEach({ (transaction) in
                    SKPaymentQueue.default().finishTransaction(transaction)
                })
            }
            
            if helper.resultBlock == nil {
                showAlert(result ? "校驗(yàn)成功" : "校驗(yàn)失敗")
            }
            ALog(result ? "充值成功" : "充值失敗")
            
            helper.progress = result ? .checkedSuccess : .checkedFailed
            helper.payFinish()
        }
    }
}

// MARK: - SKProductsRequestDelegate
extension IAPHelper: SKProductsRequestDelegate {
    
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        ALog("---IAP---")

        if currentPID == nil {
            // 列表賦值
            productList = response.products
        }
    }
    
    func requestDidFinish(_ request: SKRequest) {
        ALog("---IAP---")

        if currentPID == nil {
            
            if let block = checkBlock {
                
                if let pList = productList, !pList.isEmpty {
                    block(.ok)
                }else {
                    block(.initFailed)
                }
                checkBlock = nil
            }
        }
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        ALog("---IAP---")

        if currentPID == nil {
            
            if let block = checkBlock {
                block(.initFailed)
                checkBlock = nil
            }
        }
    }
}

// MARK: - SKPaymentTransactionObserver
extension IAPHelper: SKPaymentTransactionObserver {
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedDownloads downloads: [SKDownload]) {
        ALog("---IAP---")
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, removedTransactions transactions: [SKPaymentTransaction]) {
        ALog("---IAP---")
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        ALog("---IAP---")

        var checkList: [SKPaymentTransaction] = []
        var type: IAPProgress = progress

        for transaction in transactions {
            
            ALog("支付結(jié)果: \(transaction.description)")

            let pid = transaction.payment.productIdentifier
            switch transaction.transactionState {
                
            case .purchasing:
                
                ALog("支付中:\(pid)")
                type = .purchasing

            case .purchased:
                
                checkList.append(transaction)
                ALog("支付成功:\(pid)")
                type = .purchased

            case .failed:
                
                ALog("支付失敗:\(pid)")
                type = .payFailed
                SKPaymentQueue.default().finishTransaction(transaction)

            case .restored:
                
                checkList.append(transaction)
                ALog("支付已購(gòu)買過(guò):\(pid)")
                type = .payRestored

            case .deferred:
                
                ALog("支付不確認(rèn):\(pid)")
                type = .payDeferred
                SKPaymentQueue.default().finishTransaction(transaction)

            @unknown default:
                
                ALog("支付未知狀態(tài):\(pid)")
                type = .payOther
                SKPaymentQueue.default().finishTransaction(transaction)
            }
        }
        
        progress = type
        
        if !checkList.isEmpty {
            // 有內(nèi)購(gòu)已經(jīng)完成
            completeTransaction(checkList)
            
        }else if type == .purchasing {
            // 正常情況:內(nèi)購(gòu)正在支付
            // 特殊情況:若該商品已購(gòu)買结澄,未執(zhí)行finishTransaction,系統(tǒng)會(huì)提示(免費(fèi)恢復(fù)項(xiàng)目)岸夯,回調(diào)中斷
            // 解決方法:在應(yīng)用開(kāi)啟的時(shí)候捕捉到restored狀態(tài)的商品麻献,提交后臺(tái)校驗(yàn)后執(zhí)行finishTransaction

        }else { // 其他狀態(tài)
            
            payFinish()
        }
    }

    func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
        ALog("---IAP---")
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, restoreCompletedTransactionsFailedWithError error: Error) {
        ALog("---IAP---")
    }
    
    func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool {
        ALog("---IAP---")
        return true
    }
}

  • ViewController調(diào)用

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // 回調(diào)支持過(guò)程,處理HUD顯隱和用戶提示
        IAPHelper.shared.resultBlock = { [weak self] (result, pID) in
            
            guard let vc = self else {
                return
            }
            
            switch result {

            case .none:
                break
                
            case .started:
                vc.updateHUD(true, text: "支付中")
                
            case .purchasing:
                break
                
            case .purchased:
                break
                
            case .payFailed:
                vc.updateHUD(false, text: "支付取消")

            case .payRestored:
                vc.updateHUD(false)

            case .payDeferred:
                vc.updateHUD(false)

            case .payOther:
                vc.updateHUD(false)

            case .checking:
                vc.updateHUD(true, text: "充值中")
                
            case .checkedSuccess:
                vc.updateHUD(false, text: "充值成功")
                vc.updateData()
                
            case .checkedFailed:
                vc.updateHUD(false, text: "充值失敗猜扮,請(qǐng)檢測(cè)網(wǎng)絡(luò)")
                vc.updateData()
            }
        }
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        
        IAPHelper.shared.resultBlock = nil
    }

    @IBAction func payAction(_ sender: Any) {
        
        for bt in sumBtns {
            if bt.isSelected == true {
 
                switch bt.tag {
                case 1:
                    pay(id: .pay50)
                case 2:
                    pay(id: .pay98)
                case 3:
                    pay(id: .pay148)
                case 4:
                    pay(id: .pay198)
                case 5:
                    pay(id: .pay248)
                case 6:
                    pay(id: .pay298)
                default:
                    break
                }
                break
            }
        }
    }

    func pay(id: ACG_PAY_ID) {
        
        // 請(qǐng)求支付
        let p = IAPHelper.shared.pay(pID: id)
        
        switch p {
            
        case .ok:
            break
            
        case .notInit:
            IAPHelper.shared.initPayments { (c) in
                
                if c == .ok {
                    
                    if IAPHelper.shared.pay(pID: id) != .ok {
                        showAlert("暫時(shí)無(wú)法支付勉吻,請(qǐng)稍后再試")
                    }
                    
                }else {
                    showAlert("暫時(shí)無(wú)法支付,請(qǐng)稍后再試")
                }
            }
            break

        default:
            showAlert("暫時(shí)無(wú)法支付破镰,請(qǐng)稍后再試")
            break
        }
    }

  • AppDelegate需要初始化一下
IAPHelper.shared.config()

附件:

  • 支付成功后的交易憑證數(shù)據(jù)很大餐曼,后端接收這個(gè)數(shù)據(jù),跟蘋(píng)果校驗(yàn)后鲜漩,判斷用戶即可給該用戶充值源譬。


        guard let rURL = Bundle.main.appStoreReceiptURL, let data = try? Data(contentsOf: rURL) else {
            ALog("appStoreReceiptURL error")
        }
        
        let str = data.base64EncodedString()
        print(str)


  • 打印出來(lái)是這樣的(很長(zhǎng))

MIIVKgYJKoZIhvcNAQcCoIIVGzCCFRcCAQExCzAJBgUrDgMCGgUAMIIEywYJKoZIhvcNAQcBoIIEvASCBLgxggS0MAoCAQgCAQEEAhYAMAoCARQCAQEEAgwAMAsCAQECAQEEAwIBADALAgEDAgEBBAMMATEwCwIBCwIBAQQDAgEAMAsCAQ8CAQEEAwIBADALAgEQAgEBBAMCAQAwCwIBGQIBAQQDAgEDMAwCAQoCAQEEBBYCNCswDAIBDgIBAQQEAgIAiTANAgENAgEBBAUCAwHViDANAgETAgEBBAUMAzEuMDAOAgEJAgEBBAYCBFAyNTIwGAIBBAIBAgQQEXxl0gRk5NBqMO8/VFkmNzAZA... ...

  • 蘋(píng)果返回Json

1、收到這個(gè)數(shù)據(jù)表示孕似,里面的項(xiàng)目肯定付費(fèi)成功
2踩娘、通過(guò)transaction_id判斷是否重復(fù)校驗(yàn)
3、通過(guò)product_id * quantity 判斷支付金額
4喉祭、通過(guò)environment判斷所在環(huán)境

注意:用戶信息通過(guò)判斷App登錄用戶养渴,透?jìng)鰽pp用戶信息和App訂單信息都是不可靠的。


{
  "environment": "Sandbox",
  "receipt": {
    "in_app": [{
        "transaction_id": "1000000529594470",
        "original_purchase_date": "2019-05-21 08:25:55 Etc/GMT",
        "quantity": "1",
        "original_transaction_id": "1000000529594470",
        "purchase_date_pst": "2019-05-21 01:25:55 America/Los_Angeles",
        "original_purchase_date_ms": "1558427155000",
        "purchase_date_ms": "1558427155000",
        "product_id": "ACG_PAY_50",
        "original_purchase_date_pst": "2019-05-21 01:25:55 America/Los_Angeles",
        "is_trial_period": "false",
        "purchase_date": "2019-05-21 08:25:55 Etc/GMT"
      },
      {
        "transaction_id": "1000000529074541",
        "original_purchase_date": "2019-05-20 02:29:04 Etc/GMT",
        "quantity": "1",
        "original_transaction_id": "1000000529074541",
        "purchase_date_pst": "2019-05-19 19:29:04 America/Los_Angeles",
        "original_purchase_date_ms": "1558319344000",
        "purchase_date_ms": "1558319344000",
        "product_id": "ACG_PAY_98",
        "original_purchase_date_pst": "2019-05-19 19:29:04 America/Los_Angeles",
        "is_trial_period": "false",
        "purchase_date": "2019-05-20 02:29:04 Etc/GMT"
      }
    ],
    "adam_id": 0,
    "receipt_creation_date": "2019-05-21 08:51:54 Etc/GMT",
    "original_application_version": "1.0",
    "app_item_id": 0,
    "original_purchase_date_ms": "1375340400000",
    "request_date_ms": "1558430410539",
    "original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
    "original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
    "receipt_creation_date_pst": "2019-05-21 01:51:54 America/Los_Angeles",
    "receipt_type": "ProductionSandbox",
    "bundle_id": "com.xxx.acg",
    "receipt_creation_date_ms": "1558428714000",
    "request_date": "2019-05-21 09:20:10 Etc/GMT",
    "version_external_identifier": 0,
    "request_date_pst": "2019-05-21 02:20:10 America/Los_Angeles",
    "download_id": 0,
    "application_version": "1"
  },
  "status": 0
}

后端實(shí)現(xiàn)參考文章

1泛烙、流程寫(xiě)得很詳細(xì)理卑,php源碼也有:
http://www.cnblogs.com/wangboy91/p/7162335.html
2、Java端的支持:
https://blog.csdn.net/jianzhonghao/article/details/79343887

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蔽氨,一起剝皮案震驚了整個(gè)濱河市藐唠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鹉究,老刑警劉巖宇立,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異自赔,居然都是意外死亡妈嘹,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門绍妨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)润脸,“玉大人,你說(shuō)我怎么就攤上這事他去〗蚝” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵孤页,是天一觀的道長(zhǎng)尔苦。 經(jīng)常有香客問(wèn)我,道長(zhǎng)行施,這世上最難降的妖魔是什么允坚? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮蛾号,結(jié)果婚禮上稠项,老公的妹妹穿的比我還像新娘。我一直安慰自己鲜结,他們只是感情好展运,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布活逆。 她就那樣靜靜地躺著,像睡著了一般拗胜。 火紅的嫁衣襯著肌膚如雪蔗候。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,692評(píng)論 1 305
  • 那天埂软,我揣著相機(jī)與錄音锈遥,去河邊找鬼。 笑死勘畔,一個(gè)胖子當(dāng)著我的面吹牛所灸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播炫七,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼爬立,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了万哪?” 一聲冷哼從身側(cè)響起懦尝,我...
    開(kāi)封第一講書(shū)人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎壤圃,沒(méi)想到半個(gè)月后陵霉,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡伍绳,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年踊挠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片冲杀。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡效床,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出权谁,到底是詐尸還是另有隱情剩檀,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布旺芽,位于F島的核電站沪猴,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏采章。R本人自食惡果不足惜运嗜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望悯舟。 院中可真熱鬧担租,春花似錦、人聲如沸抵怎。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至尝艘,卻和暖如春演侯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背利耍。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盔粹,地道東北人隘梨。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像舷嗡,于是被迫代替她去往敵國(guó)和親轴猎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355