iOS内购-防越狱刷单

2020年8月12日更新

关于文中,苹果用户退款了也不知道是谁退的那块表述,现在来看 是有误的。实际上从今年WWDC后,苹果就增加了一个Server To Server的回调通知,当有用户退款时,会触发该通知。非续期订阅,消耗型,非消耗型均会收到退款通知。自动续期类订阅,苹果之前就会有通知。

详情可参考以下官方文档:
苹果退款回调

2019年5月8日更新

最近统计丢单率的时候,反查我们公司的订单有时候会出现后台的某个商品销量居然比iTunes后台的该商品销量还高的现象。排除时差因素和丢单自动补的流程因素以外,发现是后台校验订单重复性的逻辑出现了问题。

问题原因:

我们后台之前的校验逻辑是对receipt_data 进行MD5映射,然后每次服务器收到客户端上报receipt_data的时候,先MD5,然后在数据库进行排重对比。以上的这个校验逻辑是建立在相同订单的receipt_data一定相同。但实际过程中,我发现苹果并不是这样的,存在以下现象。
对于同一笔订单,苹果在极个别情况下会回调不一样的receipt_data。所以用以上的排重校验逻辑,就存在有给客户多发内购商品的现象。

解决方案:

对苹果返回的transaction_id进行MD5映射或者直接保存在数据库里,排重用这个字段。这里有一点要注意的是如果直接保存到数据库的话,数据库类型不要用整型,因为有同行反馈说该字段可能会出现字符串。

博客上开头讲内购大家通常的逻辑第⑥点时候是说的receipt_data用MD5映射做排重。但是后面的正文讲服务器的校验逻辑那里说的是要用transaction_id做排重。这个应该是当时没注意。现在为了避免引起误解,我将排重方式都统一改成用transaction_id做排重。另外如果你们的后台也是用receipt_data做排重,那就有问题了,需要尽快改成用transaction_id做排重。


---------------------------以下为正文---------------------------

iOS内购开发大家一定不陌生,网上类似的文章能搜出千八百篇。大部分都是围绕着如何实现?如何防止漏单丢单说明的。很少有提及到越狱的,即使偶尔有一两篇说越狱,也是简单的三言两语说 为了安全,我们直接屏蔽了越狱手机的内购功能。巴拉巴拉... 以前我也是这么想的,直到上个周末发现我们的内购被xx了...才有了这篇文章。本篇文章就是来讲述越狱下的内购如何防止被xx。

首先我们先简单理一下整个内购的核心流程:
①客户端发起支付订单
②客户端监听购买结果
③苹果回调订单购买成功时,客户端把苹果给的receipt_data和一些订单信息上报给服务器
④后台服务器拿receipt_data向苹果服务器校验
⑤苹果服务器向返回status结果,含义如下,其中为0时表示成功。
21000 App Store无法读取你提供的JSON数据
21002 收据数据不符合格式
21003 收据无法被验证
21004 你提供的共享密钥和账户的共享密钥不一致
21005 收据服务器当前不可用
21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中
21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证
21008 收据信息是产品环境中使用,但却被发送到测试环境中验证
⑥服务器发现订单校验成功后,会把这笔订单存起来,transaction_id用MD5值映射下,保存到数据库,防止同一笔订单,多次发放内购商品。
以上应该是主流的校验流程。当然客户端其中会插一些丢单漏单的逻辑校验,因为那些跟本篇文章无关,所以不在此展开。


从上面的流程可以看出,整个内购的核心其实就是receipt_data。苹果回调给客户端,客户端上报给服务器,服务器拿到后去向苹果服务器校验,苹果服务器再返回给我们服务器订单结果。其实严格来说,整个流程是没问题的。整个的漏洞是在最后一步上,【苹果服务器再返回给我们服务器订单结果】。receipt_data在越狱环境下是可以被插件伪造的,后台向苹果验证时,居然还能验证通过。是的,你没看错,苹果这里有个贼鸡儿坑的地方。这是最坑最坑的地方,伪造的receipt_data苹果校验也返回支付成功

如何解决?我们先来看下越狱订单和正常订单对比

越狱订单receipt_data向苹果服务器校验后如下:
{
    "status": 0, 
    "environment": "Production", 
    "receipt": {
        "receipt_type": "Production", 
        "adam_id": 1377028992, 
        "app_item_id": 1377028992, 
        "bundle_id": "*******【敏感信息不给看】*******", 
        "application_version": "3", 
        "download_id": 80042231041057, 
        "version_external_identifier": 827853261, 
        "receipt_creation_date": "2018-07-23 07:30:45 Etc/GMT", 
        "receipt_creation_date_ms": "1532331045000", 
        "receipt_creation_date_pst": "2018-07-23 00:30:45 America/Los_Angeles", 
        "request_date": "2018-07-23 07:33:54 Etc/GMT", 
        "request_date_ms": "1532331234485", 
        "request_date_pst": "2018-07-23 00:33:54 America/Los_Angeles", 
        "original_purchase_date": "2018-07-01 12:16:21 Etc/GMT", 
        "original_purchase_date_ms": "1530447381000", 
        "original_purchase_date_pst": "2018-07-01 05:16:21 America/Los_Angeles", 
        "original_application_version": "3", 
        "in_app": [ ]
    }
}
正常订单receipt_data向苹果服务器校验后如下:
{
   {
    "status": 0, 
    "environment": "Production", 
    "receipt": {
        "receipt_type": "Production", 
        "adam_id": 1377028992, 
        "app_item_id": 1377028992, 
        "bundle_id": "*******【敏感信息不给看】*******", 
        "application_version": "3", 
        "download_id": 36042096097927, 
        "version_external_identifier": 827703432, 
        "receipt_creation_date": "2018-07-10 13:54:27 Etc/GMT", 
        "receipt_creation_date_ms": "1531230867000", 
        "receipt_creation_date_pst": "2018-07-10 06:54:27 America/Los_Angeles", 
        "request_date": "2018-07-23 08:03:27 Etc/GMT", 
        "request_date_ms": "1532333007664", 
        "request_date_pst": "2018-07-23 01:03:27 America/Los_Angeles", 
        "original_purchase_date": "2018-06-13 06:52:13 Etc/GMT", 
        "original_purchase_date_ms": "1528872733000", 
        "original_purchase_date_pst": "2018-06-12 23:52:13 America/Los_Angeles", 
        "original_application_version": "5", 
        "in_app": [
            {
                "quantity": "1", 
                "product_id": "*******【敏感信息不给看】*******", 
                "transaction_id": "160000477610856", 
                "original_transaction_id": "160000477610856", 
                "purchase_date": "2018-07-10 13:54:27 Etc/GMT", 
                "purchase_date_ms": "1531230867000", 
                "purchase_date_pst": "2018-07-10 06:54:27 America/Los_Angeles", 
                "original_purchase_date": "2018-07-10 13:54:27 Etc/GMT", 
                "original_purchase_date_ms": "1531230867000", 
                "original_purchase_date_pst": "2018-07-10 06:54:27 America/Los_Angeles", 
                "is_trial_period": "false"
            }
        ]
    }
}

看完两笔订单的对比我相信大家可以清楚的知道,越狱订单虽然状态返回是成功的,但是in_app这个参数是空的。大概查了一下。iOS7以下是没有这个in_app参数的,iOS7以上是有的。因为现在App基本支持的起步都是iOS8 iOS9了,iOS7可以不用管了。但这里还有一个问题,就是in_app这个字段并不总是只返回一个,有可能会返回多个,比如下面的这种订单。

正常订单receipt_data校验后  in_app多个元素时:
{
    "status":0,
    "environment":"Sandbox",
    "receipt":{
        "receipt_type":"ProductionSandbox",
        "adam_id":0,
        "app_item_id":0,
        "bundle_id":"*******【敏感信息不给看】*******",
        "application_version":"1",
        "download_id":0,
        "version_external_identifier":0,
        "receipt_creation_date":"2018-07-24 04:28:24 Etc/GMT",
        "receipt_creation_date_ms":"1532406504000",
        "receipt_creation_date_pst":"2018-07-23 21:28:24 America/Los_Angeles",
        "request_date":"2018-07-24 04:30:06 Etc/GMT",
        "request_date_ms":"1532406606695",
        "request_date_pst":"2018-07-23 21:30:06 America/Los_Angeles",
        "original_purchase_date":"2013-08-01 07:00:00 Etc/GMT",
        "original_purchase_date_ms":"1375340400000",
        "original_purchase_date_pst":"2013-08-01 00:00:00 America/Los_Angeles",
        "original_application_version":"1.0",
        "in_app":[
            {
                "quantity":"1",
                "product_id":"*******【敏感信息不给看】*******",
                "transaction_id":"1000000398911598",
                "original_transaction_id":"1000000398911598",
                "purchase_date":"2018-05-16 03:26:12 Etc/GMT",
                "purchase_date_ms":"1526441172000",
                "purchase_date_pst":"2018-05-15 20:26:12 America/Los_Angeles",
                "original_purchase_date":"2018-05-16 03:26:12 Etc/GMT",
                "original_purchase_date_ms":"1526441172000",
                "original_purchase_date_pst":"2018-05-15 20:26:12 America/Los_Angeles",
                "is_trial_period":"false"
            },
            {
                "quantity":"1",
                "product_id":"*******【敏感信息不给看】*******",
                "transaction_id":"1000000398911640",
                "original_transaction_id":"1000000398911640",
                "purchase_date":"2018-05-16 03:26:37 Etc/GMT",
                "purchase_date_ms":"1526441197000",
                "purchase_date_pst":"2018-05-15 20:26:37 America/Los_Angeles",
                "original_purchase_date":"2018-05-16 03:26:37 Etc/GMT",
                "original_purchase_date_ms":"1526441197000",
                "original_purchase_date_pst":"2018-05-15 20:26:37 America/Los_Angeles",
                "is_trial_period":"false"
            },
            {
                "quantity":"1",
                "product_id":"*******【敏感信息不给看】*******",
                "transaction_id":"1000000398911784",
                "original_transaction_id":"1000000398911784",
                "purchase_date":"2018-05-16 03:26:50 Etc/GMT",
                "purchase_date_ms":"1526441210000",
                "purchase_date_pst":"2018-05-15 20:26:50 America/Los_Angeles",
                "original_purchase_date":"2018-05-16 03:26:50 Etc/GMT",
                "original_purchase_date_ms":"1526441210000",
                "original_purchase_date_pst":"2018-05-15 20:26:50 America/Los_Angeles",
                "is_trial_period":"false"
            },
            {
                "quantity":"1",
                "product_id":"*******【敏感信息不给看】*******",
                "transaction_id":"1000000398911801",
                "original_transaction_id":"1000000398911801",
                "purchase_date":"2018-05-16 03:27:22 Etc/GMT",
                "purchase_date_ms":"1526441242000",
                "purchase_date_pst":"2018-05-15 20:27:22 America/Los_Angeles",
                "original_purchase_date":"2018-05-16 03:27:22 Etc/GMT",
                "original_purchase_date_ms":"1526441242000",
                "original_purchase_date_pst":"2018-05-15 20:27:22 America/Los_Angeles",
                "is_trial_period":"false"
            },
            {
                "quantity":"1",
                "product_id":"*******【敏感信息不给看】*******",
                "transaction_id":"1000000399060767",
                "original_transaction_id":"1000000399060767",
                "purchase_date":"2018-05-16 11:10:45 Etc/GMT",
                "purchase_date_ms":"1526469045000",
                "purchase_date_pst":"2018-05-16 04:10:45 America/Los_Angeles",
                "original_purchase_date":"2018-05-16 11:10:45 Etc/GMT",
                "original_purchase_date_ms":"1526469045000",
                "original_purchase_date_pst":"2018-05-16 04:10:45 America/Los_Angeles",
                "is_trial_period":"false"
            },
            {
                "quantity":"1",
                "product_id":"*******【敏感信息不给看】*******",
                "transaction_id":"1000000399061778",
                "original_transaction_id":"1000000399061778",
                "purchase_date":"2018-05-16 11:14:52 Etc/GMT",
                "purchase_date_ms":"1526469292000",
                "purchase_date_pst":"2018-05-16 04:14:52 America/Los_Angeles",
                "original_purchase_date":"2018-05-16 11:14:52 Etc/GMT",
                "original_purchase_date_ms":"1526469292000",
                "original_purchase_date_pst":"2018-05-16 04:14:52 America/Los_Angeles",
                "is_trial_period":"false"
            },
            ...
        ]
    }
}

综上,整个服务器那边校验逻辑应该是这样的。
首先客户端必须要给服务器传的三个参数:receipt_data, product_id ,transaction_id

//该方法为监听内购交易结果的回调
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray<SKPaymentTransaction *> *)transactions
transactions 为一个数组 遍历就可以得到 SKPaymentTransaction 对象的元素transaction。然后从transaction里可以取到以下这两个个参数,product_id,transaction_id。另外从沙盒里取到票据信息receipt_data 
我们先看怎么取到以上的三个参数
//获取receipt_data
NSData *data = [NSData dataWithContentsOfFile:[[[NSBundle mainBundle] appStoreReceiptURL] path]];
NSString * receipt_data = [data base64EncodedStringWithOptions:0];
//获取product_id
NSString *product_id = transaction.payment.productIdentifier;
//获取transaction_id
NSString * transaction_id = transaction.transactionIdentifier;
这是我们必须要传给服务器的三个字段。以上三个字段需要做好空值校验,避免崩溃。
下面我们来解释一下,为什么要给服务器传这三个参数。
receipt_data:这个不解释了 大家都懂 不传的话 服务器根本没法校验
product_id:这个也不用解释 内购产品编号 你不传的话 服务器不知道你买的哪个订单
transaction_id:这个是交易编号,是必须要传的。因为你要是防止越狱下内购被xx就必须要校验in_app这个参数。而这个参数的数组元素有可能为多个,你必须得找到一个唯一标示,才可以区分订单到底是那一笔。

所以服务器那边逻辑就很清晰了。

①先判重,避免重复分发内购商品。收到客户端上报的transaction_id后,直接MD5后去数据库查,能查到说明是重复订单,返回相应错误码给客户端,如果查不到,去苹果那边校验。

沙箱校验地址 = "https://sandbox.itunes.apple.com/verifyReceipt";
正式校验地址 = "https://buy.itunes.apple.com/verifyReceipt";

②服务器拿到苹果的校验结果后,首先判断订单状态是不是成功。

③如果订单状态成功在判断in_app这个字段有没有,没有直接就返回失败了。如果存在的话,遍历整个数组,通过客户端给的transaction_id 来比较,取到相同的订单时,对比一下bundle_id ,product_id 是不是正确的。

如果以上校验都正确就把这笔订单充值进去,给用户分发内购商品。

注意:一定要告诉后台,不论校验是否成功,只要客户端给服务器传了receipt_data等参数就一定要保存到数据库里。【下面会解释为什么】

以上的校验步骤,可以有效的防止内购xx,下面内容是我看苹果官方能文档的关于in_app这个参数说明和解释下为啥服务器必须要保存每一个不同的receipt_data。

苹果IAP官方文档

苹果文档上介绍in_app参数内容截图.png

In the JSON file, the value of this key is an array containing all in-app purchase receipts based on the in-app purchase transactions present in the input base-64 receipt-data. For receipts containing auto-renewable subscriptions, check the value of the latest_receipt_info key to get the status of the most recent renewal.

大概意思是说:
在这个JSON文件中,这个键的值是一个数组,该数组包含基于base-64后的所有内购收据。如果你的内购类型是自动更新订阅,那么请通过检查latest_receipt_info键的值,来确定最近更新的状态。

很有意思的是,苹果还特别标明了这么一句话:

Note: An empty array is a valid receipt.

也就是说这个in_app参数可能为空,如果为空的话,也需要把这笔交易认为是有效的交易。这是苹果建议的操作。当然我们肯定不能这么干,这个参数是必须必须要校验的,不然越狱环境下,分分钟就把你内购xx了。我去校验了很多正常用户的内购订单,没发现一个in_app参数是为空的。但为了保险,还是让后台把所有前端传的receipt_data等参数不管成功失败都保存下来,万一哪个用户因此投诉充值不到账,我们有据可查。

下面两段话

The in-app purchase receipt for a consumable product is added to the receipt when the purchase is made. It is kept in the receipt until your app finishes that transaction. After that point, it is removed from the receipt the next time the receipt is updated - for example, when the user makes another purchase or if your app explicitly refreshes the receipt.

The in-app purchase receipt for a non-consumable product, auto-renewable subscription, non-renewing subscription, or free subscription remains in the receipt indefinitely.

大概意思是说:
每当有一笔交易发起的时候,in_app里就会添加收据的一些信息。这些信息会一直保存直到你结束这笔交易。在此之后,下次更新收据时会将其从收据中删除 - 例如,当用户再次购买时,或者您的应用明确刷新收据时。

非消耗型项目,自动续期订阅,非续期订阅或免费订阅的应用内购买收据将无限期保留在收据中。

这一点也解释了说,为什么in_app这个数组有时候会有多个元素。


下面在举几个大家做内购经常遇到的一些问题,和一些容易混淆的点。

Q1:内购和Apple Pay的区别?

A1:内购是内购,Apple Pay是Apple Pay。我不知道有多少人第一次接触时,会把这俩概念混淆掉,这里你可以简单这么理解,虚拟的物品就是用内购,实际的物品就是用Apple Pay。Apple Pay是一种支付方式,你可以类比为支付宝,微信那种。但人家只支持实际物品,如果你东西是虚拟的话,你却集成Apple Pay上架是要被拒绝的哦~当然反过来,实际物品你却集成内购上架,也是一样被拒。对于大部分的国内开发者而言,你很少会遇到需要集成Apple Pay的App的。能用支付宝/微信的场景还要求支持Apple Pay的产品毕竟是少数。

Q2:内购项目的类型区别?

A2:首先内购项目分为以下4种,消耗型项目,非消耗型项目,自动续期订阅,非续期订阅。我们来一个个介绍。

消耗型项目:只可使用一次的产品,使用之后即失效,必须再次购买。就是大家最广为所知的虚拟币,比如直播平台斗鱼的鱼翅,熊猫的竹子,哔哩哔哩的B币等,这个概念大家应该很好理解,不过多解释了。

非消耗型项目:只需购买一次,不会过期或随着使用而减少的产品。这个一般是游戏那里用的多,一般是付费解锁关卡的场景,用户买过一次,卸载重装或者同一个Apple id但换App账号时,也要能保证用户重新获得该内购商品。所以App内部需要额外去实现恢复购买的逻辑。

自动续期订阅:允许用户在固定时间段内购买动态内容的产品。除非用户选择取消,否则此类订阅会自动续期。iTunces上给的示例是:每月订阅提供流媒体服务的 App。对比我们熟悉的,网易云音乐的内购商品-连续包月黑胶VIP,就是此类型。一般来说,没啥必要不要选这一种,如果是VIP的那种场景推荐下面非续期订阅类型去做。自动续期订阅的坑非常多,比另外几种内购类型都要复制。

非续期订阅:一般来说VIP可以用这种方法来做订阅,我们公司项目的VIP购买就是这种方式。他的实现方式你可以完全照搬消耗性项目,不用做什么额外处理,也不用去管返回的订阅日期什么的东西,就是以服务器那边为准。服务器的日期开始,服务器的日期结束。既简单又保险,不需要额外的做什么处理。

Q3:VIP一定要用内购做吗?

A3:其实判断你们公司的App到底需不需要用内购,很简单,就是看跟实际物品有没有关系。如果你的VIP功能是类似饿了么这种,点外卖可以打折/多领红包 那么就不需要用内购,上架的时候说清楚就行了。如果你的VIP功能是虚拟的,比如头像更炫酷,尊贵的VIP身份标示,独特的入场动画等等虚拟相关的,比如QQ会员,就必须要用内购去做。需要说明的是,那种是VIP才能和某某用户聊天的场景,是VIP才能得到App里某某用户的服务【语音,视频】时,这一类的场景苹果一样认为是虚拟的,一样要用内购去做。

Q4:VIP内购一定是非续期/自动续期订阅吗?我可不可以用虚拟币购买VIP呢?

A4:这个问题我自己经历过。我的答案是你也可以用虚拟币购买VIP的这种方式,但如果被拒绝,你只能老老实实的按前种方式去做。如果你们的App既有虚拟币又有VIP,产品希望你VIP是直接用虚拟币去购买,这样整个流程都很方便。那么你一定要记住。千万不要在1.0版本这么做,这是血泪教训。1.0版本会抓的很严很严,同时虚拟币+VIP功能,百分百苹果会要求你VIP要用续期订阅去实现。最保险的做法呢,1.0版本不要做任何内购,迭代几个小版本后,加入虚拟币内购,在迭代几个小版本,加入VIP直接用虚拟币购买的功能,这是最最保险的做法。记住:1.0的审核力度是真的很严,能先不做内购就不要做内购,老板或许不懂,1.0版本什么都想要,但往往因为内购,会让你们的产品反复被拒。这一块如果大家感兴趣,可以看看,我的1.0版本就是加了内购,反复被拒5次。血泪教训

Q5:我们老板心疼那百分之30的手续费,我能不能不用内购啊,有没办法绕过内购?

A5:办法是有的。但是有风险。我16年做过绕开内购的方法。思路很简单,就是App里集成支付宝/微信/内购这种功能,后台做控制开关,审核时,开关打开,给审核人员看内购功能,审核通过后,开关关闭,给正常用户是用支付宝/微信功能。这个方法,我17年的时候听到很多群友说不行了,你在上架审核时,苹果会扫描你的包,检测到第三方支付sdk时,会拒绝掉。后来又有群友说可以用H5的方式实现支付功能。另外可能会有别的绕开苹果审核的实现方式,如果有哪位朋友知道,不妨留言告知。但不管是哪种方式,都是有风险的,苹果对内购一直抓的很严,如果让它知道你们在钱的方面上欺骗过他,后果还是很严重的。iOS上的用户付费率还是很不错的,付费意愿基本上可以是安卓用户的十几倍。所以如果你们的产品真的有前景,并且想长久做下去,还是奉劝不要做欺骗苹果的事情了。

Q6:网上有好多讲丢单的博客,看的是一脸懵逼,有的看懂后,在看下一篇又不懂了,感觉都好复杂。

A6:引起内购丢单的主要操作其实是当用户点击内购商品时,苹果服务器太慢了,支付页面一直不出来。结果用户退出或者杀死App,这时候在Home页面,支付框又弹出来了,然后用户点击支付,成功后在打开App发现丢单。
一般这种只要你在Appdelegate的didFinishLaunchingWithOptions方法就开始对苹果内购回调做监听,然后把所有相关内购的东西抽出来做一个单例即可解决丢单。另外还有一些丢单可能是用applicationUsername做透传引起的,这种解决办法一般就是NSUserDefaults或者keychain或者在极端点NSUserDefaults+ keychain来做本地信息的记录。关于丢单这一块感兴趣的可以自己搜贝聊解决丢单的那个博客【虽然他讲的比较乱,但思路可以借鉴下】。另外评论区下面有个 广东深圳 的

我在刚接触内购的时候也是这样,我觉得有些博客讲的真有点过了,它为了考虑一些用户的极端操作,多出来很多逻辑处理,导致博客异常的复杂,我记得有博客讲必须要把receipt_data等信息存到keychain里,因为用户有可能卸载App,如果你只存到NSUserDefaults里,那样就丢单了。 ......那么有没有这种情况呢?我觉得是肯定有的。但我们来算算几率,首先他内购成功,在向服务器调接口的时候,他手机突然没电了/断网了/程序崩溃了/网络差等的久他自己杀死进程了 巴拉巴拉。 然后他在下一次手机恢复正常的时候,果断卸载掉App,重新去App Store上下载安装,进入App后,发现内购没到账。
867088104DC1554FE5CDF4E962061E43.jpg

网上博客还爱用那种切换账号的场景举例,A内购成功了,但用户各种骚操作后,自己换到B账号,然后服务器那边把商品发到B账号上了,等等。
这些情况都是存在的,因为苹果的内购机制问题,你是不能百分百保证不丢单的,不要把丢单情况看的那么严重,逻辑写的那么复杂。你看看所有大厂的App上都会写充值遇到问题,点我联系客服 巴拉巴拉。关于丢单,我的做法是这样的,在苹果内购成功的回调里,NSUserDefaults存每一笔支付成功的订单,如果服务器校验成功,就把本地存的这笔订单删除。如果没收到服务器的响应,就一直保留。然后每次App启动就会去把本地存的丢单信息扔向服务器校验,校验成功删除,校验失败不管。这里还是看开发时间,当时我写内购功能的时候,预算时间就两天不到,所以写的飞快,就简单的用这个办法去防止丢单,目前来看,没有发现过一笔真正用户充钱但商品没到账的例子。如果大家开发时间充足,可以慢慢去弥补极端操作漏洞。

Q7:内购为什么会有这么多坑啊?看网上好多博客都在说,我自己做微信/支付宝的时候,没感觉有这么多坑啊

A7:苹果的内购坑主要有以下几点

  • applicationUsername该字段可能为nil 导致客户端没办法用这个参数给服务器透传订单编号,来形成一个交易订单号的绑定。

  • 校验订单流程是必须服务器主动去询问苹果服务器,而支付宝/微信 却是他们的服务器会在用户支付成功时主动给我们服务器回调。正是这个原因,让iOS开发者饱受折磨,大部分的丢单漏单都是苹果的这个设计造成的。苹果不会主动回调给我们服务器,也就意味着我们服务器需要主动去苹果那里询问这笔订单,到底成没成功。但服务器询问的时机,又是客户端告诉服务器的。这就鸡儿坑了,一些情况下,用户在付费成功后,突然断网了/崩溃了/出现意外了等等,客户端没办法告诉服务器,这就出现了,用户钱成功了,内购商品却没到账。所以网上才会有这么多篇讲防止丢单的博客。

  • 越狱下,插件也能xx掉苹果内购,然后校验状态status还返回成功。也就是本篇博客开头讲的那种情况。这一点真的是无力吐槽,亏你特么回调给我的receipt_data那么一大长串,有卵用?

  • 苹果的订单机制。苹果为了保护用户隐私,你是看不到一条条流水明细的。你看到的只有👇这种。
    27FED3A0-C9F3-47C1-BA67-4EFAA7B2FCBA.png

    每一种内购类型的总收入,或者总销量。导致对账查询的时候加了不少麻烦。

  • 苹果的退款机制。这个比上面一点更坑,iOS用户,内购了某商品,你可以在完全用完了后,联系苹果客服,说我误操作了巴拉巴拉或者说感觉这个商品不值那么多被开发者欺骗了巴拉巴拉,快给我退款,客服就会温柔的告诉你,不要急,她会帮你处理,1-2个工作日把,你就会发现你的钱就退回来了。没记错的话,一段时间内,一个Apple Id可以申请1-2次。但不能多,多了的话就会被苹果拒绝。而这一切,开发者这边是完全不知情的。你不知道哪个用户退款了,你知道的只是一个图,类似下面的这种。
    0A762A89-3A96-4503-995D-028A96518958.png

    用户消费了你的内购商品,公司却收不到钱,很多公司的内购服务都是要成本的。如果这种用户一旦多起来,坏账率会飙升,公司就会被活活的拖垮。一个好的项目也就凉凉掉。淘宝上关于iOS内购退款专门有一个超级庞大的黑色产业链。从弄账号到专门联系苹果客服再到道具销赃变现,各司其职,一环套一环,每个环节人都赚的盆满钵满。苦的都是公司,因为苹果没有任何损失,他也不会补偿你公司1毛钱,一切损失都是公司自己承担。没记错的话,15-16年,很多很多游戏公司都是因为这个被活活拖垮的。幸运的是,这种恶意退款一般都是针对游戏公司,因为游戏道具可以快速变现。像正常的App甚少碰到,因为他退款了也没毛用,没法及时变现。毕竟他们可不稀罕跟你们的女用户1v1视频聊天。

Q8:对于开发者来讲,用通过内购充值,那开发者到时候怎么得到这笔钱?

A8:做内购的时候,会填写银行税务等等这些信息。苹果会按期把钱打入到你们当时填写的银行卡账户中。这里要注意,如果你当月内购收入很低,比如只有几十美金,那苹果是不会给你打款的。具体的额度好像是以150美金分界限,当你内购收入超过150美金的时候,苹果会下月给你打款。如果你不够150美金,那苹果会累积到下个月打款,如果下个月还不够,那就会继续累积。
这里注意两点:

  1. 150美金是个大概的数值,我自己没有确实求证过,我实际经历来说遇到的当月收入都远超过这个数字,所以很准确的最低打款金额,我也不好评估。
  2. 苹果的打款日期,并没有严格的规律。上旬,中旬,下旬打款我都遇到过,另外有时候,即便你当月金额有很多,苹果也可能下个月不给你打款,而是给你累积到下下个月。但至多不会超过两个月以上,如果你遇到这种情况,需要及时和苹果客户沟通。
    综上:
    只要你银行卡,税务等相关财务信息填写正确,账户里收入超过150美金,大多数情况下,下个月上旬就能收到苹果的打款。

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

推荐阅读更多精彩内容