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 交给服务器设计表结构
- 走具体流程
- 服务器具体要做的事
- 产品列表接口: 提供可购买的 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里的数据,
- 如果此时是插入数据 且 expires_date_ms > now 就表明用户续费了或者刚购买 需要创建订单
- 如果此时payment 数据已存在, 且 expires_date_ms > now 且 order_no 没赋值, 那么说明漏单了, 创建订单
- 如果 is_trial_period or is_in_intro_offer_period 就要注意创建试用订单 此时用户还没真正付钱所以权益之只分配是用权限 比如 首月免费 那就创建首月免费订单 , 等苹果扣费时, 验证苹果 receipt 时 会新增一个 transaction_id 表明付款了
测试流程
-
了解 in-app-purchase sandbox 背景 如下图
补充
- 苹果还有订阅 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
;