从踩坑到上线,一个PHP后端的苹果支付接入实录
📌 写在前面
公司项目需要接入苹果App内购(IAP)的订阅支付功能。刚开始我心想:微信支付都接了八百遍了,苹果还能玩出什么花?
结果一做才发现——坑比想象的多。
主要问题是:网上的资料太少了,大部分都是零零散散的片段,没有一个完整的、能直接“抄作业”的教程。
于是自己踩完坑、做完之后,决定把整个过程整理出来,希望能帮到下一个被苹果支付折磨的兄弟。
🎯 本文核心内容
| 模块 | 说明 |
|---|---|
| 业务逻辑 | 服务端需要做什么 |
| 主动购买 | 用户点击购买,客户端拿票据来验证 |
| 订阅续费 | 苹果服务器自动回调,处理续费逻辑 |
| 完整代码 | 可直接复用的PHP代码片段 |
| 踩坑记录 | 常见问题及解决方案 |
一、整体业务流程
先上一张图,看懂苹果支付的核心流程:
核心理解:苹果支付和微信支付最大的区别在于——苹果不会直接告诉你“支付成功了” ,而是给客户端一个票据(receipt),客户端拿到后再传给服务端,由服务端去苹果服务器验证。
二、两种支付场景
苹果IAP支付分为两种:
| 类型 | 说明 | 特点 |
|---|---|---|
| 主动购买 | 用户点击购买,一次性付费 | 类似买断制,立即生效 |
| 订阅购买 | 用户订阅会员,到期自动续费 | 需要处理苹果服务端回调 |
下面分别讲解。
三、场景一:主动购买(一次性付费)
3.1 业务流程
-
App端创建订单,传给服务端保存
-
App调用苹果SDK发起支付
-
苹果返回支付票据(receipt)
-
App将票据 + 订单号传给服务端
-
服务端去苹果服务器验证票据
-
验证通过后,处理发货逻辑
💡 票据是什么?
票据就是一串很长的字符串,服务端拿着它去苹果服务器验证,苹果会返回一堆明文数据,里面包含购买信息、产品ID、交易ID等。
3.2 完整代码
① 支付验证接口
/**
* @title 验证支付票据 完成订单接口
*/
public function ios_pay()
{
$receipt_data = $_REQUEST['ios_billon'] ?? '';
$order_id = $_REQUEST['order_id'] ?? '';
$result = array('status' => false, 'message' => '非法参数');
if (empty($receipt_data) || empty($order_id)) {
return_json($result);
}
// 请求苹果服务器验证票据
$appkey = "你的苹果共享秘钥"; // 在苹果开发者后台获取
$html = $this->acurl($receipt_data, $appkey);
$data = json_decode($html, true);
// 状态码21007表示沙盒环境,切换到沙盒模式重新验证
if ($data['status'] == '21007') {
$html = $this->acurl($receipt_data, $appkey, 1);
$data = json_decode($html, true);
$data['sandbox'] = '1';
}
// 记录日志(方便排查问题)
$logStr = "************** 票证信息 " . date("Y-m-d H:i:s") . " **************\n\r";
file_put_contents("apple_pay_log.txt", $logStr . json_encode($data) . "\n\r", FILE_APPEND);
// 验证是否购买成功(status=0表示成功)
if (intval($data['status']) === 0) {
// 校验Bundle ID和票据有效性
if ($data['receipt']['bundle_id'] != IOS_BUNDLE_ID || empty($data['receipt']['in_app'])) {
return_json($result);
}
// 提取票据信息
$ks['status'] = $data['status'];
$ks['receipt'] = $data['receipt'];
unset($ks['receipt']['in_app']);
$data['receipt']['in_app'][0] = $data['receipt']['latest_receipt_info'][0] ?? $data['receipt']['in_app'][0];
// 合并数据
$k = array_merge($ks, $data['receipt']['in_app'][0]);
$pay_date = date('Y-m-d H:i:s', $k['purchase_date_ms'] / 1000); // 购买时间
$transaction_id = $k['transaction_id']; // 交易ID
$original_transaction_id = $k['original_transaction_id']; // 原始交易ID
// ========== 处理你的业务逻辑 ==========
// 判断是否为免费试用
if ($k['is_trial_period'] == 'true') {
$update_data['money'] = 0;
$update_data['ios_status'] = 1; // 1=免费试用中
$is_try = 1;
}
// 更新订单状态,开通会员权益...
// ====================================
$result = array('status' => true, 'message' => '支付成功');
} else {
$result = array('status' => false, 'message' => $this->err_msg[$data['status']] ?? $data['status']);
}
return_json($result);
}
② CURL请求封装(去苹果服务器验证)
/**
* 去苹果服务器验证票据
* @param string $receipt_data 票据字符串
* @param string $appkey 共享秘钥
* @param int $sandbox 是否沙盒环境(1=沙盒,0=正式)
* @return string
*/
public function acurl($receipt_data, $appkey = "", $sandbox = 0)
{
// 构造请求参数
$POSTFIELDS = array("receipt-data" => $receipt_data);
if (!empty($appkey)) {
$POSTFIELDS = array("receipt-data" => $receipt_data, 'password' => $appkey);
}
$POSTFIELDS = json_encode($POSTFIELDS);
// 苹果验证地址
$url_buy = "https://buy.itunes.apple.com/verifyReceipt"; // 正式环境
$url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt"; // 沙盒环境
$url = $sandbox ? $url_sandbox : $url_buy;
// 发起请求
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $POSTFIELDS);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
3.3 验证前后的数据对比
验证前的票据示例:
MjdwZjYxNWEtN2QwZi00Y2VmLWFiYmUtY...
就是一串很长的字符串,看不出任何信息)
验证后的数据(status=0代表成功):
{
"status": 0,
"receipt": {
"bundle_id": "com.your.app",
"in_app": [...],
"latest_receipt_info": [...]
}
}
⚠️ 注意:original_transaction_id 是苹果订阅的“身份证”,建议存下来,后续续费回调时会用到。
四、场景二:订阅购买(自动续费)
4.1 什么是订阅购买?
苹果订阅的特点是:
- 用户购买后,会在Apple ID的订阅中心生成一条记录
- 到期后自动扣款续费(除非用户手动取消)
- 每次续费,苹果会通过服务端回调通知你
4.2 业务流程
1. 用户首次订阅购买(走主动购买的流程)
2. 苹果服务器记录订阅关系
3. 到期前:苹果自动扣款
4. 苹果服务器回调你的接口(通知续费成功)
5. 你根据回调信息,更新会员有效期
4.3 配置回调地址
这个接口需要在 苹果开发者后台 配置:
路径:App Store Connect → App → 服务端通知 → 回调URL 填入:https://你的域名/api/ios_continu_pay
4.4 完整代码
/**
* 苹果订阅续费回调接口
* 需要在苹果开发者后台配置
*/
public function ios_continu_pay()
{
// 获取苹果回传的原始数据
$input_data = trim(file_get_contents("php://input"));
// 记录日志(重要!方便排查问题)
$logStr = "************** 苹果订阅回调 " . date("Y-m-d H:i:s") . " **************\n\r";
file_put_contents("apple_subscribe_log.txt", $logStr . $input_data . "\n\r", FILE_APPEND);
$resp_str = json_decode($input_data, true);
if (!empty($resp_str)) {
$data = $resp_str['unified_receipt'];
$notification_type = $resp_str['notification_type']; // 通知类型
$password = $resp_str['password']; // 共享秘钥
// 验证秘钥是否正确
if ($password == "你的苹果共享秘钥") {
// 获取最新的票据信息
$receipt = $data['latest_receipt_info'] ?? $data['latest_expired_receipt_info'];
$receipt = self::arraySort($receipt, 'purchase_date', 'desc');
$original_transaction_id = $receipt['original_transaction_id']; // 原始交易ID
$transaction_id = $receipt['transaction_id']; // 本次交易ID
$purchaseDate = str_replace(' America/Los_Angeles', '', $receipt['purchase_date_pst']);
// ========== 根据通知类型处理业务 ==========
// INITIAL_BUY:初次购买
// RENEWAL:自动续费成功
// DID_RENEW:续费成功
// INTERACTIVE_RENEWAL:用户在App Store内手动续费
// CANCEL:用户取消订阅
switch ($notification_type) {
case 'RENEWAL':
case 'DID_RENEW':
case 'INTERACTIVE_RENEWAL':
// 续费成功,延长会员有效期
// 根据 original_transaction_id 找到用户,更新会员到期时间
break;
case 'CANCEL':
// 用户取消订阅,记录订阅状态
break;
default:
// 其他通知类型,仅记录日志,不处理业务
break;
}
// ========================================
} else {
// 秘钥不正确,记录错误日志
// NewLog::log('订阅回调密码不正确', 'ios_pay_error');
}
}
}
4.5 辅助函数:数组排序(取最新一条)
/**
* 二维数组排序,取最新的一条数据
*/
public static function arraySort($arr, $key, $type = 'asc')
{
$keyArr = [];
foreach ($arr as $k => $v) {
$keyArr[$k] = $v[$key];
}
if ($type == 'asc') {
asort($keyArr);
} else {
arsort($keyArr);
}
foreach ($keyArr as $k => $v) {
$newArray[$k] = $arr[$k];
}
$newArray = array_merge($newArray);
return $newArray[0];
}
4.6 常见的 notification_type
| 类型 | 含义 | 是否需要处理 |
|---|---|---|
INITIAL_BUY | 初次购买订阅 | ✅ 需要(首次开通会员) |
RENEWAL | 自动续费成功 | ✅ 需要(延长会员) |
DID_RENEW | 续费成功(同RENEWAL) | ✅ 需要 |
INTERACTIVE_RENEWAL | 用户手动续费 | ✅ 需要 |
CANCEL | 用户取消订阅 | ⚠️ 记录状态即可 |
DID_CHANGE_RENEWAL_PREF | 用户更改订阅计划 | ⚠️ 可选处理 |
EXPIRED | 订阅过期 | ⚠️ 记录状态即可 |
五、踩坑记录 & 避坑指南
坑1:沙盒环境和正式环境容易搞混
| 环境 | 验证地址 | 用户类型 |
|---|---|---|
| 沙盒 | sandbox.itunes.apple.com | 测试账号 |
| 正式 | buy.itunes.apple.com | 真实用户 |
解决方案:优先用正式环境验证,返回21007再切到沙盒。
坑2:订阅回调通知类型太多,不知道处理哪些
苹果会发很多类型的通知,不是全部都要处理。核心只需要处理续费成功的类型:RENEWAL、DID_RENEW、INTERACTIVE_RENEWAL。
其他类型记录日志即可。
坑3:original_transaction_id 是唯一标识
苹果订阅中,original_transaction_id 是订阅记录的“身份证”,同一个订阅关系从首次购买到最后一次续费,这个ID都不变。
建议:把这个ID存到用户表,续费回调时用它来定位用户。
坑4:票据验证的时机
不要在客户端收到票据后立即验证。苹果的票据生成有延迟,建议:
- 立即验证一次
- 如果失败,延迟3-5秒再重试一次
六、最后
以上就是苹果IAP订阅支付的服务端完整实现。
核心总结:
| 要点 | 说明 |
|---|---|
| 主动购买 | 客户端拿票据 → 服务端去苹果验证 → 发货 |
| 订阅续费 | 配置苹果回调地址 → 处理续费通知 → 延长会员 |
| 核心字段 | original_transaction_id 是订阅唯一标识 |
| 常见坑点 | 沙盒/正式环境切换、通知类型筛选 |
网上的资料确实不多,希望这篇文章能帮到正在对接苹果支付的你。
如果有什么问题,欢迎留言交流。
👍 如果对你有帮助,欢迎点赞、收藏、转发!