Apple Pay编程指导

1.About Apple Pay

Apple Pay是一种移动支付技术,让使用者把它们对真实的物品和服务的支付信息以一种方便和安全的方式给你。

对于在app中给出的数字物品和服务,可查看In-App Purchase Programming Guide

Working with Apple Pay

使用Apple Pay功能的Apps需要在Xcode 中开启Apple Pay capabilities。也需要注册一个商家标识并设置密钥(用来加密发送支付数据给服务器)。

初始化支付时,app创建一个支付请求。该请求包含几乎全部的服务和商品购买,也包含任何额外的费用:税金、运费或者折扣。传送该请求给支付授权视图控制器,控制器展示该请求给用户并对任何需要的信息作出提示,例如运送地址或者账单地址。当用户与视图控制器进行交互时,调用代理用以更新请求。

一旦用户授权支付,Apple Pay加密支付信息用以保护未授权的第三方访问它。在设备上,Apple Pay发送支付请求给加密元件,该元件是一个用户设备上的专用芯片。该加密元件添加支付信息为指定的卡和商家生成一个加密的支付密钥。然后传递该密钥给Apple 服务器,在服务器上使用商家标识证书(Merchant Identifier certificate)解密。最后,服务器传递该密钥返回到app中继续处理。

支付密钥从不访问或者存取在Apple服务器上。该服务器仅仅可使用证书解密密钥。这种处理使得不用加密证书app安全的加密支付信息,且必须分配商家标识证书作为app的一部分。

更多关于Apple Pay安全的信息,可查看iOS Security Guide

大多数情况下,app传送加密密钥给第三发支付平台解密并处理支付。然而,如果自己的团队有已经存在的支付基础设施,可以在自己的服务器上解密并处理支付。

更多关于支持Apple Pay的支付平台的信息,可查看develop.apple.com/apple-pay/

2.Configuring Your Environment

商户ID用来判别用户是否对Apple Pay能够接受支付。与商户ID有关的公共的钥匙和证书被用来作为支付进程的一部分,用来加密支付信息。在app可以使用Apple Pay之前,需要注册一个merchant ID商户ID 并配置它的证书。

注册商户ID(a merchant ID)

  • 1.在会员中心(Member Center),选择Certificates,Identifiers & Profiles(http://developer.apple.com/account)选项。
  • 2.在Identifiers选项下面,选择Merchant IDs选项。
  • 3.点击右上角的添加按钮(+)。
  • 4.输入一段描述和标识,点击Continue继续按钮。
  • 5.检查设置内容,然后点击Register注册按钮。
  • 6.点击Done 按钮,完成。

配置商户ID相应的证书

  • 1.在会员中心(Member Center)(https://developer.apple.com),选择Certificates,Identifiers & Profiles选项。
  • 2.在Identifiers选项下面,选择Merchant IDs选项。
  • 3.从列表中选择商户ID(merchant ID),点击Edit编辑按钮。
  • 4.点击Create Certificate按钮生成证书,跟随获得的说明或是生成一个CSR(certificate signing request),点击Continue按钮。
  • 5.点击选择文件,选中CSR文件,点击Generate按钮。
  • 6.点击Download按钮下载该证书,并点击Done按钮,完成。

如果在钥匙串使用中看见一个警告warning ,说明证书被未知的认证授权签名或者它是一个无效的发行者,确认有the WWDR intermediate certificate - G2证书并在钥匙串中安装了the Apple Root CA - G2。这些可以在apple.com/certificateauthority 下载得到。


注意:
当出现故障时,有时候这对手动开启Apple Pay功能有所帮助。跟随下面的步骤手动开启Apple Pay功能。

  • 1.在会员中心(Member Center),选择Certificates ,Identifiers & Profiles选项。
  • 2.在Identifiers选项下,选择App IDs选项。
  • 3.从列表中选中app ID,点击Edit按钮。
  • 4.选择Apple Pay选项,点击Edit按钮。
  • 5.选择准备使用的商户IDs(merchant IDs),点击Continue按钮。
  • 6.检查设置内容,然后点击Assign指派按钮。
  • 7.点击Done按钮,完成。

3.Creating Payment Requests

Payment Requests支付请求是PKPaymentRequest类的实例。支付请求包含一系列描述用户的支付对象(为哪些东西付款)的概要项:可用的运送方式的列表,用户需要提供的运送信息的描述,关于商家和支付处理的信息。

Decide Whether the User Can Make Payments 判决用户是否有能力支付

在生成一个支付请求之前,通过调用PKPaymentAuthorizationViewController类中的canMakePaymentsUsingNetworks:方法判断用户是否能够使用支持的系统完成支付。使用canMakePayments方法,检查设备的硬件和父类控制是否支持Apple Pay。

如果canMakePayments返回NO,表示设备不支持Apple Pay。因此也不显示Apple Pay按钮,转到其他的支付方式。

如果canMakePayments返回YES,但是canMakePaymentsUsingNetworks:返回NO,表示设备支持Apple Pay,但是用户没有添加任何符合要求的支付系统的卡片。可选择性地去显示一个支付设置按钮,提示用户设置卡片。一旦用户点击了该按钮,初始化设置一个新卡的进程(例如:调用openPaymentSetup方法)。

另外,一旦用户按下Apple Pay按钮,必须开始支付授权进程。在展示支付请求之前不能要求用户执行其他任何任务。例如,如果用户需要输入折扣码,必须在按下Apple Pay按钮之前请求该代码。

Bridging from Web-Based Interfaces 基于网络接口进行桥接

如果app使用网络接口购买物品和服务,必须在处理Apple Pay交易之前从网络接口移动该请求到本地iOS代码。列表3-1展示了需要处理web view的请求的步骤。

Listing 3-1Buying items from a web view
    // Called when the web view tries to load "myShoppingApp:buyItem"
    -(void)webView:(nonnull WKWebView *)webView
    decidePolicyForNavigationAction:(nonnull WKNavigationAction *)navigationAction
    decisionHandler:(nonnull void (^)(WKNavigationActionPolicy))decisionHandler {
        
        // Get the URL for the selected link.
        NSURL *URL = navigationAction.request.URL;
        
        // If the scheme and resource specifier match those defined by your app,
        // handle the payment in native iOS code.
        if ([URL.scheme isEqualToString:@"myShoppingApp"] &&
            [URL.resourceSpecifier isEqualToString:@"buyItem"]) {
            
            // Create and present the payment request here.
            
            // The web view ignores the link.
            decisionHandler(WKNavigationActionPolicyCancel);
        }
        
        // Otherwise the web view loads the link.
        decisionHandler(WKNavigationActionPolicyAllow);
    }

Payment Requests Include Currency and Region Information 包括货币和地区信息的支付请求

在支付请求中所有的概要数值均使用相同的货币(指定使用PKPaymentRequest类中的currencyCode属性),均使用三个字母的ISO货币代码,例如USD。

支付请求的国家码表明在该国家发生购买操作或者在该国家购买将要被处理。使用两个字母的ISO国家码,例如US。

在支付请求中设置的商户ID(merchant ID)必须与app中的entitlement中的merchant IDs相匹配.

    request.currencyCode = @"USD";
    request.countryCode = @"US";
    request.merchantIdentifier = @"merchant.com.example";

Payment Requests Have a List of Payment Summary Items支付请求有一个支付概要项的列表

展示在PKPaymentSummaryItem类中的支付概要项,描述给用户的支付请求的不同部分。使用一个小部分的概要项- 典型地包含总和、任何折扣、运费、税金和最终的总和。如果没有任何其他额外的费用(例如:运费或税金),仅仅包含购物的总和。在app中提供逐项的消费细节。

每个概要项有一个标记和数值,展示在Listing 3-2。标记是用户可读的概要项总结的描述。该数值与支付数值是一致的。在支付请求中的所有数值均使用在支付请求中指定的货币。折扣或者优惠劵的数值,则设置为负数。

当支付被授权的时候,如果不知道某个实际的费用(例如:打车费),生成一个使用PKPaymentSummaryItemTypePending类型的总和概要项并且值为0.0。该系统然后标记该费用为未决定的。

Listing 3-2Creating a payment summary item
    // 12.75 subtotal
    NSDecimalNumber *subtotalAmount = [NSDecimalNumber decimalNumberWithMantissa:1275 exponent:-2 isNegative:NO];
    self.subtotal = [PKPaymentSummaryItem summaryItemWithLabel:@"Subtotal" amount:subtotalAmount];
    
    // 2.00 discount
    NSDecimalNumber *discountAmount = [NSDecimalNumber decimalNumberWithMantissa:200 exponent:-2 isNegative:YES];
    self.discount = [PKPaymentSummaryItem summaryItemWithLabel:@"Discount" amount:discountAmount];

注意:
支付概要项使用NSDecimalNumber类存储数量为以10为基的数。该类的实例可以通过明确地指定尾数和指数(展示在代码列表中)或者提供一个string类型的数量并指定一个区域来创建。总是使用以10为基的数来进行金融计算,例如,确定5%折扣的数量。

即使IEEE的浮点类型数据例如floatDouble显示起来非常方便,但不适合于金融计算。这些数据类型使用以2为基的数字表示出来,这意味着一些小数数值不能被精确地表示出来。例如:0.42很可能接近于0.419999无限循环。这类近似值会造成金融计算得到错误的结果。

在列表中最后的一个支付概要项是最终的总和。通过添加所有其他的概要项数值来计算总和数值。最终总和的显示不同于其他的概要项:使用公司的名称作为它的label内容,使用所有其他的概要项的数值之和作为它的数值。使用paymentSummaryItems属性添加支付概要项到支付请求。

    // 10.75 grand total
    NSDecimalNumber *totalAmount = [NSDecimalNumber zero];
    //  计算总和
    totalAmount = [totalAmount decimalNumberByAdding:subtotalAmount];
    totalAmount = [totalAmount decimalNumberByAdding:discountAmount];

    self.total = [PKPaymentSummaryItem summaryItemWithLabel:@"My Company Name" amount:totalAmount];
    
    self.summaryItems = @[self.subtotal, self.discount, self.total];
    request.paymentSummaryItems = self.summaryItems;

A Shipping Method Is a Special Payment Summary Item 运送方式是一种特殊的支付概要项

为每个适用的运送方式创建一个PKShippingMethod的实例。正如其他的支付概要项一样,运送方式有一个用户可读label内容(例如“标准的运送”或者“次日运送”)和一个运费的数值。不像其他的概要项,运送方式也有detail属性-例如:“Arrives by July 29”或者“Ships in 24 hours”- 这说明了运送方式之间的不同。

为了在代理方法中区分运送方式,可使用identifier属性。该属性仅仅被用于在自己的app中-框架处理它作为一个不透明的值,也不显示在UI界面中。当创建每个运送方式时,为它指定一个独一无二的标识。为了使调试容易些,使用摘要或者简短的string值,例如“discount”,“standard”或者“next-day”。

一些运送方式不适用于所有的区域或者对不同的地址有不同的运费。当用户选择一个运送地址或者方式时可以更新这些信息,正如Your Delegate Updates Shipping Methods and Costs中描述的。

Indicating Your Support Payment Processing Mechanisms 指定支持的支付处理机制

通过用string常量的数组填充supportNetworks属性来指定支持何种支付系统。通过给merchantCapabilities属性设定一个值来指定支持何种支付处理协议。必须支持3DS,支持EMV是可选的。

商家支付能力是位掩码,联合展示如下:

    request.supportedNetworks = @[PKPaymentNetworkAmex, PKPaymentNetworkDiscover, PKPaymentNetworkMasterCard, PKPaymentNetworkVisa];
    
    // Supports 3DS only
    request.merchantCapabilities = PKMerchantCapability3DS;
    
    // Supports both 3DS and EMV
    request.merchantCapabilities = PKMerchantCapability3DS | PKMerchantCapabilityEMV;

Indicating What Shipping and billing Information Is Needed 指定哪些运送和账单信息是必需的

填充支付授权视图控制器的requiredBillingAddressFields和requiredShippingAddressFields属性来指定哪些账单和运送信息是必需的。当展示该视图控制器时,它提示用户提供要求的账单和运送信息。这些区域的常量按照下面的方式联合设定这些属性的值:

    request.requiredBillingAddressFields = PKAddressFieldEmail;
    request.requiredBillingAddressFields = PKAddressFieldEmail | PKAddressFieldPostalAddress;

注意:
仅仅请求需要用来处理支付的账单和运送信息和传送商品或者服务。请求不必要的信息会增加不需要的复杂性。每个额外的步骤会增加用户简单地取消了该交易的可能性。

如果你有最新的账单和运送联系方式信息,可以在支付请求时设置这些内容。Apple Pay会默认使用这些信息;然而,用户仍然可以选择其他的联系方式信息作为支付授权处理的一部分。

    PKContact *contact = [[PKContact alloc] init];
    
    //  联系人姓名
    NSPersonNameComponents *name = [[NSPersonNameComponents alloc] init];
    name.givenName = @"John";
    name.familyName = @"Appleseed";
    
    contact.name = name;
    
    // 联系人地址
    CNMutablePostalAddress *address = [[CNMutablePostalAddress alloc] init];
    address.street = @"1234 Laurel Street";
    address.city = @"Atlanta";
    address.state = @"GA";
    address.postalCode = @"30303";
    
    contact.postalAddress = address;
    
    request.shippingContact = contact;

注意:
地址信息可以来自在iOS中的广泛的输入源。在使用它之前总是验证这些信息。

Storing Additional Information 存储额外的信息

为了存储每个app特定的支付请求的信息,例如一个购物车标识,使用applicationData属性。该属性被对待作为一个系统提供的不透明的值。当用户授权支付请求之后一大把应用数据会显示在支付密钥中。

4.Authorizing Payments

支付授权处理是支付授权视图控制器和它的代理之间的协力合作的结果。支付授权视图控制器做两件事:让用户选择支付请求中所需要的账单和运送信息,让用户去授权支付。当用户和视图控制器交互时调用代理的方法以便app可以更新展示的信息-例如:当一个运送地址被选择时更新运送价格。在用户授权支付请求之后调用该代理。

注意:
当实现代理方法时,记得它们会被调用很多次并且它们被调用的规则取决于用户的动作顺序。

所有在授权处理期间调用的代理方法均传送一个完成的block作为它们的参数之一。在它调用其他代理方法之前,支付授权视图控制器等待代理完成响应一个方法(通过调用完成block)。paymentAuthorizationViewControllerDidFinish:方法是唯一的例外。它没有取到完成block,但是它在任何时候均可被调用。

完成 block取到一个论证,该论证可以指定基于可用信息的交易的当前授权状态。如果该交易没有任何问题,传送该值PKPaymentAuthorizationStatusSuccess;否则,传送一个值来标识错误。

为了创建一个PKPaymentAuthorizationViewController类的实例,传送支付请求给视图控制器的初始化。设置该视图控制器的代理,然后显示它。

    PKPaymentAuthorizationViewController *viewController = [[PKPaymentAuthorizationViewController alloc] initWithPaymentRequest:request];
    if (!viewController) { /* ... Handle error ... */ }
    viewController.delegate = self;
    [self presentViewController:viewController animated:YES completion:nil];

当用户与视图控制器交互时,视图控制器调用它的代理方法。

注意:
在Xcode 7.0或者以后,可以在模拟器上测试支付授权视图控制器。它提供了所有支持的支付系统的模拟卡片并以简单的文本形式返回虚拟的支付数据。在设备上,该数据使用商户标识加密并必须在自己的服务器上或者在支付处理时解密。
虽然模拟器提供一个快捷而且方便的方式测试自己的代码,仍然需要在真实的物理设备上完全地测试Apple Pay。
如果使用 Xcode的早期版本,仅仅可以在设备上测试Apple Pay。

Your Delegate Updates Shipping Methods 代理更新运送方式和费用

当用户提供运送信息时,授权视图控制器调用代理的paymentAuthorizationViewController:didSelectShippingContact:completion:方法和paymentAuthorizationViewController:didSelectShippingMethod:completion:方法。使用这些方法来更新基于这些新信息的支付请求。

    - (void) paymentAuthorizationViewController:    (PKPaymentAuthorizationViewController *)controller
                       didSelectShippingContact:(CNContact *)contact
                                     completion:(void (^)   (PKPaymentAuthorizationStatus, NSArray *, NSArray *))completion
    {
        self.selectedContact = contact;
        [self updateShippingCost];
        NSArray *shippingMethods = [self shippingMethodsForContact:contact];
        completion(PKPaymentAuthorizationStatusSuccess, shippingMethods, self.summaryItems);
    }
    - (void) paymentAuthorizationViewController:(PKPaymentAuthorizationViewController *)controller
                        didSelectShippingMethod:(PKShippingMethod *)shippingMethod
                                     completion:(void (^)(PKPaymentAuthorizationStatus, NSArray *))completion
    {
        self.selectedShippingMethod = shippingMethod;
        [self updateShippingCost];
        completion(PKPaymentAuthorizationStatusSuccess, self.summaryItems);
    }

注意:
为了维护隐私,paymentAuthorizationViewController:didSelectShippingContact:completion:方法中提及的运送信息是匿名的。返回的联系方式包含足够的信息用以计算运费,不展现用户的敏感信息。直到用户批准了该支付之后,才能获取到用户的全部运送信息,否则不能获取到。另外,联系方式中的数据可以根据不同的国家而改变,并可以一次又一次地改变。保证以合适的方式测试app。

A Payment Token Is Created When a Payment Is Authorized 当支付被授权时生成一个支付密钥

当用户授权支付请求时,框架通过Apple 服务器和安全元件联合生成一个支付密钥。可以通过paymentAuthorizationViewController:didAuthorizePayment:completion:代理方法传送该支付密钥给自己的服务器,与其它需要的信息一起处理购买。例如:运送地址和购物车标识。过程如下:

  • 1.框架发送支付请求给安全元件。只有安全元件可以使用tokenized标记化的特定设备的支付卡号。
  • 2.安全元件将含有特定卡和商户的支付数据 放在一起,加密它使得只有Apple 可以读取它,并发送它给框架。框架然后发送支付数据给Apple 服务器。
  • 3.Apple 服务器使用Merchant Identifier certificate商户标识证书解密支付数据。该密钥仅仅自己和那些已分享商户标识证书给他们的人可读。然后服务器写下该支付密钥,并返回它给设备。
  • 4.框架通过调用

paymentAuthorizationViewController:didAuthorizePayment:completion:方法传送该密钥给代理。代理传送该密钥给自己的服务器。

在自己服务器上的行为改变取决于是否处理自己的支付或者对一个支付平台有效。在两种情况下,服务器处理订单并传送一个状态返回给设备,代理可传送给它的完成处理者,具体描述在Processing a Payment

    - (void) paymentAuthorizationViewController:(PKPaymentAuthorizationViewController *)controller
                            didAuthorizePayment:(PKPayment *)payment
                                     completion:(void (^)(PKPaymentAuthorizationStatus))completion
    {
        NSError *error;
        ABMultiValueRef addressMultiValue = ABRecordCopyValue(payment.billingAddress, kABPersonAddressProperty);
        NSDictionary *addressDictionary = (__bridge_transfer NSDictionary *) ABMultiValueCopyValueAtIndex(addressMultiValue, 0);
        NSData *json = [NSJSONSerialization dataWithJSONObject:addressDictionary options:NSJSONWritingPrettyPrinted error: &error];
        
        // ... Send payment token, shipping and billing address, and order information to your server ...
        
        PKPaymentAuthorizationStatus status;  // From your server
        completion(status);
    }

Your Delegate Dismisses the Payment Authorization View Controller代理dismiss支付授权视图控制器

当框架显示交易的状态之后,授权视图控制器调用代理的paymentAuthorizationViewControllerDidFinish:方法。在实施阶段,dismiss授权视图控制器并且显示自己app特定的订单确认页面。

    - (void) paymentAuthorizationViewControllerDidFinish:(PKPaymentAuthorizationViewController *)controller
    {
        [controller dismissViewControllerAnimated:YES completion:nil];
    }

5.Processing Payments

处理支付包括这几步:

  • 1.发送支付信息给服务器,与需要的其它信息一起处理订单
  • 2.验证支付数据的哈希表和签名
  • 3.解密加密的支付数据
  • 4.提交支付数据给支付处理网络
  • 5.提交订单给订单追踪系统

两种选择处理支付:有一个支付平台的优势去处理支付,或者自己实施支付进程。一个支付处理平台一般处理大部分如上步骤。

读取、验证和处理支付信息需要对密码学的几种领域有所理解,例如,计算一个SHA-1 哈希,读取和验证一个PKCS #7类型的签名,和执行elliptic curve Diffie-Hellman key椭圆曲线密钥交换。如果没有密码学的背景,考虑使用一个支付平台来处理这些操作。更多支持Apple Pay的支付平台的信息,可查看developer.apple.com/apple-pay/

用来处理支付的信息有一个嵌套的数据组织,正如展示在Figure 5-1中支付密钥是PKPaymentToken类的实例。它的paymentData属性的值是一个JSON字典,它有 个头部header包含用于确认和加密支付数据的信息。加密数据包含像数量、持卡人姓名和其它用于特定的支付处理协议的的信息。

Figure 5-1Payment data structure


关于支付数据结构格式的详情,可查看Payment Token Format Reference

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容