ios内购续订服务端

本文最后更新于:1 个月前

ios内购续订服务端


参考

业务逻辑

业务逻辑

  • 同一Apple 账号生成续订订单的原始交易ID(original_transaction_id)一致
  • 服务端处理交易过程 :要确定一个交易ID(transaction_id)只能完成一笔订单,处理完该交易的订单之后,该交易ID记录标识为处理完成状态
  • 服务端可以通过用户购买凭证(receipt_data)查询用户所有交易记录 查询到的数据有in_app、latest_receipt_info、pending_renewal_info
  • 连续订阅主要用到数据是latest_receipt_info,里面有所有的续订记录。
  • 如果里面的交易有cancellation_date字段,说明该交易已经被退款。
  • pending_renewal_info里面的auto_renew_status字段用于标识用户是否开通自动订阅;0:已关闭;1:已开通。

在接收 App Store的续订、取消、退款通知时,因为选择的版本2(version 2 notification)的通知,版本二通知是jwt编码实现。
所以需要解码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168

// 支付回调
public function validatePay()
{
//苹果内购的验证收据
$param = $this->param;
$receipt_data = $param['receipt_data'];
Log::info('内购receipt_data_old:' . $receipt_data);
$receipt_data = str_replace(" ", "+", $receipt_data);
Log::info('内购receipt_data:' . $receipt_data);
Log::info('内购receipt_data_json:' . json_encode($receipt_data));
$orderSn = $param['order_sn'] ?? 0;
$sandbox = Env::get('APP_ENV') != 'prod';
// 验证支付状态
$result = validate_apple_pay($receipt_data, $sandbox);
Log::info('内购回调res:' . json_encode($result));
if ($result['status']) {
// 验证通过 此处可以是修改数据库订单状态等操作
$payId = $result['data']['transactionId'];
$time = $result['data']['purchaseTime'] / 1000;
$order = $this->orderService->getOrderBySn($orderSn);
if (is_null($order)) {
throw new BusinessException('无此订单');
}
$isHasPaid = $this->orderService->isHasPaid($orderSn);
if ($isHasPaid) {
return $this->dataToJson(0, $isHasPaid);
}
$res = $this->orderService->payOrder($order, $payId, $time);
return $this->dataToJson(0, $res);

} else {
// 验证不通过
return $this->dataToJson(1, $result['message']);

}

}


/*
* 自动续费订阅回调
* Doc: https://developer.apple.com/documentation/appstoreservernotifications/notification_type
*/
public function renew()
{
$resp_str = $this->request->post();
Log::info('resp_str' . json_encode($resp_str));
$data = $resp_str['signedPayload'];
$data = self::verifyToken($data);
$data['signedTransactionInfo'] = self::verifyToken($data['data']['signedTransactionInfo']);
$data['signedRenewalInfo'] = self::verifyToken($data['data']['signedRenewalInfo']);
if ($data) {
/*通知类型
https://developer.apple.com/documentation/appstoreservernotifications/notificationtype
CONSUMPTION_REQUEST 表示客户针对消耗品内购发起退款申请
DID_CHANGE_RENEWAL_PREF 对其订阅计划进行了更改 如果subtype是UPGRADE,则用户升级了他们的订阅;如果subtype是DOWNGRADE,则用户将其订阅降级或交叉分级
DID_CHANGE_RENEWAL_STATUS 通知类型及其subtype指示用户对订阅续订状态进行了更改
DID_FAIL_TO_RENEW 一种通知类型及其subtype指示订阅由于计费问题而未能续订
DID_RENEW 一种通知类型,连同其subtype指示订阅成功续订
EXPIRED 一种通知类型及其subtype指示订阅已过期
GRACE_PERIOD_EXPIRED 表示计费宽限期已结束,无需续订,因此您可以关闭对服务或内容的访问
OFFER_REDEEMED 一种通知类型,连同其subtype指示用户兑换了促销优惠或优惠代码。 subtype DID_RENEW
PRICE_INCREASE 一种通知类型,连同其subtype指示系统已通知用户订阅价格上涨
REFUND 表示 App Store 成功为消耗性应用内购买、非消耗性应用内购买、自动续订订阅或非续订订阅的交易退款
REFUND_DECLINED 表示 App Store 拒绝了应用开发者发起的退款请求
RENEWAL_EXTENDED 表示 App Store 延长了开发者要求的订阅续订日期
REVOKE表示 用户有权通过家庭共享获得的应用内购买不再通过共享获得
SUBSCRIBED 一种通知类型,连同其subtype指示用户订阅了产品
1. 用户主动取消订阅notificationType:DID_CHANGE_RENEWAL_STATUS
2. 用户取消订阅,又重新开通连续订阅notificationType: SUBSCRIBED subtype: RESUBSCRIBE
3. 用户首次开通订阅notificationType: SUBSCRIBED subtype: INITIAL_BUY
*/
$notification_type = $data['notificationType'];
$transactionData = $data['signedTransactionInfo'];
$product_id = $transactionData['productId'];
$sub_type = isset($data['subtype']) ? $data['subtype'] : '';
$original_transaction_id = $transactionData['originalTransactionId']; // //原始交易ID
$transaction_id = $transactionData['transactionId']; // //交易的标识
$expires_date = date('Y-m-d H:i:s', $transactionData['expiresDate'] / 1000);
//todo 记录通知log
$orderinfo = $this->orderModel->where('pay_id', $original_transaction_id)->find();

//查询原始交易绑定的用户ID
if (in_array($notification_type, ['DID_RENEW', 'SUBSCRIBED'])) {
//开通成功以及续订成功处理交易
$time = time();
$this->orderService->payOrder($orderinfo, $transaction_id, $time);
Log::info('开通成功以及续订成功处理交易, 续订成功userid--' . $orderinfo['user_id']);
}

//用户退款处理交易
if (in_array($notification_type, ['REFUND'])) {
Log::info('用户退款, userid--' . $orderinfo['user_id']);

}

//用户取消订阅或者订阅过期
if (in_array($notification_type, ['EXPIRED', 'DID_FAIL_TO_RENEW']) || ($notification_type == 'DID_CHANGE_RENEWAL_STATUS')) {
$is_renew = 0;
if (($notification_type == 'DID_CHANGE_RENEWAL_STATUS') && $sub_type == 'AUTO_RENEW_ENABLED') {
$time = time();
$this->orderService->payOrder($orderinfo, $transaction_id, $time);
Log::info('开通订阅成功, 续订成功userid--' . $orderinfo['user_id']);
//开通订阅成功
} elseif (($notification_type == 'DID_CHANGE_RENEWAL_STATUS') && $sub_type == 'AUTO_RENEW_DISABLED') {
//取消订阅成功
Log::info('取消订阅成功,userid--' . $orderinfo['user_id']);

} elseif (($notification_type == 'EXPIRED') && $sub_type == 'VOLUNTARY') {
//订阅在用户禁用订阅续订后过期
Log::info('订阅在用户禁用订阅续订后过期,userid--' . $orderinfo['user_id']);

}
//更新用户订阅状态
}

}
}

/**
* 验证token是否有效,默认验证exp,nbf,iat时间
* @param string $Token 需要验证的token
* @return bool|string
*/
public static function verifyToken($Token)
{
$tokens = explode('.', $Token);
if (count($tokens) != 3)
return false;

list($base64header, $base64payload) = $tokens;

//获取jwt算法
$base64decodeheader = json_decode(self::base64UrlDecode($base64header), true, 512, JSON_OBJECT_AS_ARRAY);
if (empty($base64decodeheader['alg']) || $base64decodeheader['alg'] != 'ES256')
return false;

$payload = json_decode(self::base64UrlDecode($base64payload), true, 512, JSON_OBJECT_AS_ARRAY);

return $payload;
}

/**
* base64UrlEncode https://jwt.io/ 中base64UrlEncode编码实现
* @param string $input 需要编码的字符串
* @return string
*/
private static function base64UrlEncode($input)
{
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
}

/**
* base64UrlEncode https://jwt.io/ 中base64UrlEncode解码实现
* @param string $input 需要解码的字符串
* @return bool|string
*/
private static function base64UrlDecode($input)
{
$remainder = strlen($input) % 4;
if ($remainder) {
$addlen = 4 - $remainder;
$input .= str_repeat('=', $addlen);
}
return base64_decode(strtr($input, '-_', '+/'));
}
}

ios内购续订服务端
https://calmchen.com/posts/23b26f62.html
作者
Calm
发布于
2022年8月12日
更新于
2022年8月12日
许可协议