20200504-IAP订阅功能梳理-applepay


title: IAP订阅功能梳理
date: 2020-05-04 08:10:08
tags: [Applepay, iap, 业务功能, 交易系统]


会员功能需要接入 IAP 订阅功能, SKU -> 连续包年(首月免费);连续包月(前3天免费);连续包季(首周免费)

前提: 系统已经有交易系统, 下面的描述只针对 连续订阅产品

需求

  • 用户订阅后,获得会员资格, 且每次扣费会有对应的订单(包含免费试用阶段的订单)

苹果支付背景

  • 苹果支付的核心是 收据(receipt)

    • 连续订阅产品的话: 第一次产生收据是一直可用的 (can verify) 每次续订/恢复购买的记录都「保存」在收据里, 业务逻辑核心就是理解 receipt 里的字段
  • 涉及到的人员

    • 设计: 设计购买页面
    • 产品: 产品交互/辅助开发 在 app_store 创建产品
    • IOS 开发: 初始化购买
    • 服务器开发: 验证客户端上传收据,并管理用户续费订单

开发流程简述

  • 产品将 产品列表创建好
App product_id 服务器后台 sku 价格 优惠说明 标题
product_year_xxx xxxx 100 首月免费 连续包年
product_quarter_xxx xxx 50 首周免费 连续包季
product_month_xxx xxxx 30 3天免费 连续包月
  • IOS 客户端 可以从苹果拿到 product_id 列表开始测试

    • 目的: 产生 receipt 交给服务器设计表结构
    • 走具体流程
image-20200504170848061.png
  • 服务器具体要做的事
    • 产品列表接口: 提供可购买的 product_id, 以及当前用户的订阅状态; 如果已订阅那么禁止用户继续订阅
    • 验证收据接口: 服务器收到 receipt 先在本地通过单元测试解析并结合文档(https://developer.apple.com/documentation/appstorereceipts)梳理
    • 定时任务: 检测 payment_applepay 数据 按过期了一个月/将要过期的 订单每隔6h 轮询一次 业务逻辑同 (验证收据接口)

服务器表结构设计

  • 验证动作表
    • 记录每次验证 receipt 的 request_data 以及 response_data 方便测试和复盘
/******************************************/
/*   DatabaseName = trade   */
/*   TableName = trade_payment_applepay_verify   */
/******************************************/
CREATE TABLE `trade_payment_applepay_verify` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `created` datetime(6) NOT NULL,
  `modified` datetime(6) NOT NULL,
  `is_removed` tinyint(1) NOT NULL,
  `user_id` int(11) NOT NULL,
  `action_type` varchar(32) NOT NULL DEFAULT 'client' COMMENT '验证类型,客户端验证(client);服务器轮询验证(server_crontab)',
  `order_no` varchar(64) NOT NULL,
  `receipt_data` longtext NOT NULL COMMENT '待验证的收据',
  `status` int(11) NOT NULL COMMENT '验证状态 成功/失败',
  `environment` varchar(16) NOT NULL COMMENT 'sandbox/production',
  `is_retryable` tinyint(1) NOT NULL,
  `latest_receipt` longtext NOT NULL COMMENT 'response: latest_receipt',
  `latest_receipt_info` longtext NOT NULL COMMENT ' response: latest_receiptjson对象',
  `pending_renewal_info` longtext NOT NULL COMMENT 'response: pending_renewal_info',
  `receipt` longtext NOT NULL COMMENT 'response: receipt',
  PRIMARY KEY (`id`),
  KEY `trade_payment_applepay_verify_user_id_4c0bc485_idx` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
;
  • 对应 receipt 的 in-app 列表 就是对应的每个 transcation_id
    • 记录每次购买 、 续费、恢复购买数据
/******************************************/
/*   DatabaseName = trade   */
/*   TableName = trade_payment_applepay   */
/******************************************/
CREATE TABLE `trade_payment_applepay` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `created` datetime(6) NOT NULL,
  `modified` datetime(6) NOT NULL,
  `is_removed` tinyint(1) NOT NULL,
  `user_id` int(11) NOT NULL,
  `action_type` varchar(32) NOT NULL DEFAULT 'client',
  `order_no` varchar(64) NOT NULL  DEFAULT '' COMMENT '对应订单 订单创建成功后赋值',
  `paid_time` bigint(20) DEFAULT NULL,
  `status` varchar(100) NOT NULL,
  `status_changed` datetime(6) NOT NULL,
  `bundle_id` varchar(32) NOT NULL,
  `application_version` varchar(16) NOT NULL,
  `original_application_version` varchar(16) NOT NULL,
  `version_external_identifier` varchar(16) NOT NULL,
  `app_item_id` varchar(16) NOT NULL,
  `product_id` varchar(64) NOT NULL DEFAULT '',
  `quantity` int(10) unsigned NOT NULL,
  `environment` varchar(16) NOT NULL,
  `transaction_id` varchar(32) NOT NULL COMMENT '每次交易 id 注意恢复购买时不需要',
  `original_transaction_id` varchar(32) NOT NULL COMMENT '原交易 id',
  `expires_date` datetime(6) DEFAULT NULL COMMENT '过期时间',
  `expires_date_ms` bigint(20) NOT NULL COMMENT '过期时间 ms',
  `purchase_date_ms` bigint(20) NOT NULL COMMENT '购买时间 ms', 
  `purchase_date` varchar(64) NOT NULL  COMMENT '购买时间',
  `auto_renew_status` tinyint(1) NOT NULL COMMENT '续费状态', 
  `is_in_intro_offer_period` tinyint(1) NOT NULL COMMENT 'An indicator of whether an auto-renewable subscription is in the introductory price period',
  `is_trial_period` tinyint(1) NOT NULL COMMENT '是否是使用期间',
  `latest_receipt` longtext NOT NULL COMMENT '最后一次的收据',
  `web_order_line_item_id` varchar(64) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `trade_payment_applepay_transaction_id_product_i_7f940417_uniq` (`original_transaction_id`,`product_id`,`expires_date_ms`) USING BTREE,
  KEY `trade_payment_applepay_user_id_9e9b627e_idx` (`user_id`),
  KEY `trade_payment_applepay_transaction_id_f8abbb58_idx` (`transaction_id`),
  KEY `trade_payment_applepay_order_no_0ad1fa70_idx` (`order_no`),
  KEY `trade_payment_applepay_original_transaction_id_7746fd50_idx` (`original_transaction_id`)
) ENGINE=InnoDB AUTO_INCREMENT=337 DEFAULT CHARSET=utf8
;

字段设计以及 Why

  • original_transaction_id,product_id,expires_date_ms 组合 unique_key 唯一索引

    对于连续订阅产品 orginal_transaction_id 只有一个, 区分每次订阅/续期的就是 expires_date_ms(每次续费时间会不一样) 和 product_id(更换产品比如从 连续包年->连续包月 )

  • payment_applepay 为什么需要 order_no

    关联 order 才知道这单处理过 防止丢单

  • 怎么判断需不需要创建订单?

    解析in-app里的数据,

    1. 如果此时是插入数据 且 expires_date_ms > now 就表明用户续费了或者刚购买 需要创建订单
    2. 如果此时payment 数据已存在, 且 expires_date_ms > now 且 order_no 没赋值, 那么说明漏单了, 创建订单
    3. 如果 is_trial_period or is_in_intro_offer_period 就要注意创建试用订单 此时用户还没真正付钱所以权益之只分配是用权限 比如 首月免费 那就创建首月免费订单 , 等苹果扣费时, 验证苹果 receipt 时 会新增一个 transaction_id 表明付款了

测试流程

  • 了解 in-app-purchase sandbox 背景 如下图


    image-20200504163524179.png

补充

  • 苹果还有订阅 subscribe 接口, 订阅文档 当订阅发生改动时会向你的服务器发送通知. 这个为了订阅时效性可以选择接入。 我们这边只是写了接口 把订阅的数据存到database 没有做具体逻辑; 数据结构如下
/******************************************/
/*   DatabaseName = trade   */
/*   TableName = trade_payment_applepay_subscribe   */
/******************************************/
CREATE TABLE `trade_payment_applepay_subscribe` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `created` datetime(6) NOT NULL,
  `modified` datetime(6) NOT NULL,
  `is_removed` tinyint(1) NOT NULL,
  `user_id` int(11) NOT NULL DEFAULT 0,
  `order_no` varchar(64) NOT NULL DEFAULT '',
  `auto_renew_adam_id` varchar(64) NOT NULL,
  `auto_renew_product_id` varchar(64) NOT NULL,
  `auto_renew_status` tinyint(1) NOT NULL,
  `auto_renew_status_change_date` datetime(6) NOT NULL,
  `auto_renew_status_change_date_ms` bigint(20) NOT NULL,
  `environment` varchar(16) NOT NULL,
  `expiration_intent` smallint(6) NOT NULL,
  `latest_expired_receipt` longtext NOT NULL,
  `latest_expired_receipt_info` longtext NOT NULL,
  `latest_receipt` longtext NOT NULL,
  `latest_receipt_info` longtext NOT NULL,
  `notification_type` varchar(64) NOT NULL,
  `unified_receipt` longtext NOT NULL,
  `password` varchar(128) NOT NULL,
  `transaction_id` varchar(32) NOT NULL,
  `original_transaction_id` varchar(32) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `trade_payment_applepay_subscribe_user_id_0a63f52c_idx` (`user_id`),
  KEY `trade_payment_applepay_subscribe_OTI_0a63f52c_idx` (`original_transaction_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=190 DEFAULT CHARSET=utf8
;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,576评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,515评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,017评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,626评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,625评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,255评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,825评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,729评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,271评论 1 320
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,363评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,498评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,183评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,867评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,338评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,458评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,906评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,507评论 2 359