以下是代码优化、异常处理、关键逻辑解读 及最佳实践:
1. 完整可运行代码(含异常处理)
<?php
/**
* iOS 内购收据验证
* 优化点:异常捕获、参数校验、环境区分、结果解析
*/
// 自动加载(确保vendor路径正确)
require_once __DIR__ . "/../vendor/autoload.php";
// 引入验证类(注意命名空间/路径正确性)
use sn01615\iap\ios\Verify;
try {
// 1. 基础参数配置
$receipt = "你的iOS内购收据字符串"; // 客户端传递的base64编码收据
$sharedSecret = "123"; // 自动续订订阅的共享密钥(非订阅可不传)
$isSandbox = true; // 沙盒环境(true) / 生产环境(false)
// 2. 校验必要参数
if (empty($receipt)) {
throw new InvalidArgumentException("内购收据不能为空");
}
// 3. 初始化验证类
$verify = new Verify();
// 4. 环境配置(沙盒/生产)
$verify->endpoint($isSandbox);
// 5. 可选配置(按需设置)
if (!empty($sharedSecret)) {
$verify->setPassword($sharedSecret); // 仅自动续订订阅需要
}
// 仅自动续订订阅:只返回最新续订交易(排除历史交易)
$verify->setExcludeOldTransactions(true);
// 6. 执行收据验证
$result = $verify->query($receipt);
// 7. 解析验证结果
$verifyResult = [
'isSuccess' => false,
'status' => $result->status ?? -1,
'environment' => $result->environment ?? 'unknown',
'receiptInfo' => [],
'errorMsg' => ''
];
// 8. 状态码判断(status=0 表示验证成功)
if ($result->status === 0) {
$verifyResult['isSuccess'] = true;
$verifyResult['receiptInfo'] = [
'bundleId' => $result->receipt->bundle_id ?? '', // App唯一标识
'appVersion' => $result->receipt->application_version ?? '', // App版本
'transactionList' => $result->receipt->in_app ?? [], // 内购交易列表
'originalPurchaseDate' => $result->receipt->original_purchase_date ?? '' // 首次购买时间
];
// 解析单笔交易(常用字段)
if (!empty($verifyResult['receiptInfo']['transactionList'])) {
$firstTransaction = $verifyResult['receiptInfo']['transactionList'][0];
$verifyResult['transactionDetail'] = [
'productId' => $firstTransaction->product_id ?? '', // 商品ID
'transactionId' => $firstTransaction->transaction_id ?? '', // 交易ID
'originalTransactionId' => $firstTransaction->original_transaction_id ?? '', // 原始交易ID
'purchaseTime' => $firstTransaction->purchase_date_ms ?? '', // 购买时间(时间戳)
'isTrial' => $firstTransaction->is_trial_period === 'true', // 是否试用
'quantity' => $firstTransaction->quantity ?? 1 // 购买数量
];
}
} else {
// 状态码非0:根据苹果官方状态码解析错误
$errorMap = [
21000 => 'App Store无法读取你提供的JSON数据',
21002 => '收据数据不符合格式',
21003 => '收据无法被验证',
21004 => '你提供的共享密钥和账户的共享密钥不匹配',
21005 => '收据服务器当前不可用',
21006 => '收据有效,但订阅已过期',
21007 => '收据是沙盒收据,却发送到生产环境验证',
21008 => '收据是生产收据,却发送到沙盒环境验证',
21010 => '收据无效'
];
$verifyResult['errorMsg'] = $errorMap[$result->status] ?? "未知错误(状态码:{$result->status})";
}
// 9. 输出结果(可根据业务需求返回JSON/存入数据库)
echo "验证结果:\n";
var_dump($verifyResult);
} catch (InvalidArgumentException $e) {
// 参数错误
echo "参数异常:{$e->getMessage()}\n";
exit(1);
} catch (Exception $e) {
// 其他异常(网络错误、类不存在等)
echo "验证失败:{$e->getMessage()} 行号:{$e->getLine()}\n";
exit(1);
}
2. 关键知识点解读
(1)核心类方法说明
| 方法 | 作用 | 注意事项 |
|---|---|---|
endpoint(bool $isSandbox) |
切换验证环境 | 沙盒环境:测试用;生产环境:正式上线用 |
setPassword(string $secret) |
设置共享密钥 | 仅自动续订订阅需要(在 App Store Connect 配置) |
setExcludeOldTransactions(bool $bool) |
排除历史交易 | 仅订阅有效,返回最新续订记录 |
query(string $receipt) |
执行验证 | 入参是客户端传递的 base64 编码收据 |
(2)苹果返回状态码(关键)
| 状态码 | 含义 | 处理方案 |
|---|---|---|
| 0 | 验证成功 | 解析交易信息,完成业务逻辑(如发货、更新订单状态) |
| 21007 | 沙盒收据发到生产环境 | 自动切换到沙盒环境重新验证 |
| 21008 | 生产收据发到沙盒环境 | 自动切换到生产环境重新验证 |
| 21006 | 订阅已过期 | 提示用户订阅过期,不执行发货 |
| 其他 | 验证失败 | 记录日志,返回客户端错误提示 |
(3)核心字段解析(交易信息)
| 字段 | 含义 | 业务用途 |
|---|---|---|
product_id |
商品 ID | 匹配后台配置的内购商品,确认购买的商品 |
transaction_id |
交易 ID | 唯一标识,防止重复发货(需入库去重) |
original_transaction_id |
原始交易 ID | 订阅类商品的唯一标识(续订时不变) |
purchase_date_ms |
购买时间戳 | 转换为本地时间,记录购买时间 |
is_trial_period |
是否试用 | 试用期间是否发货 / 计费 |
bundle_id |
App 包名 | 校验是否为自家 App 的内购(防止伪造) |
3. 最佳实践
(1)环境自动适配
苹果建议:先调用生产环境验证,若返回 21007(沙盒收据),再调用沙盒环境验证,示例:
// 优化:自动切换环境
function verifyReceiptAutoEnv($receipt, $sharedSecret) {
$verify = new Verify();
// 先试生产环境
$verify->endpoint(false);
$verify->setPassword($sharedSecret);
$result = $verify->query($receipt);
// 生产环境返回21007,切换沙盒重试
if ($result->status === 21007) {
$verify->endpoint(true);
$result = $verify->query($receipt);
}
return $result;
}
(2)防止重复发货
- 将
transaction_id存入数据库,每次验证前检查是否已存在; - 苹果可能多次回调同一笔交易,需幂等处理。
(3)日志记录
记录验证结果(成功 / 失败)、状态码、收据(脱敏)、交易 ID 等,便于排查问题:
// 示例:记录日志
$logData = [
'receipt' => substr($receipt, 0, 50) . '...', // 脱敏
'status' => $result->status,
'transaction_id' => $firstTransaction->transaction_id ?? '',
'create_time' => date('Y-m-d H:i:s')
];
file_put_contents(__DIR__ . '/ios_verify.log', json_encode($logData) . PHP_EOL, FILE_APPEND);
(4)时区转换
苹果返回的时间是 GMT/PST 时区,需转换为本地时区(如北京时间):
// 转换purchase_date_ms为北京时间
$purchaseTime = $firstTransaction->purchase_date_ms ?? 0;
$beijingTime = date('Y-m-d H:i:s', $purchaseTime / 1000 + 8 * 3600); // +8小时
4. 常见问题排查
- 类找不到 :检查
use命名空间和autoload.php路径是否正确; - 21004 错误:共享密钥(sharedSecret)错误,需在 App Store Connect > 你的 App > 订阅 > 共享密钥 中确认;
- 收据为空:客户端传递的收据需是 base64 编码后的字符串,且未被篡改;
- 沙盒环境验证失败:确保测试账号是沙盒测试账号,且 App 已配置内购商品。
总结
该代码是 iOS 内购验证的核心逻辑,优化后增加了异常处理、自动环境切换、结果解析、日志记录等生产级特性,可直接集成到业务系统中。核心注意点:
- 区分沙盒 / 生产环境;
- 校验
transaction_id防止重复发货; - 解析状态码并处理异常;
- 订阅类商品需传递共享密钥。