iOS 内购收据验证的基础实现

以下是代码优化、异常处理、关键逻辑解读最佳实践

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. 常见问题排查
  1. 类找不到 :检查 use 命名空间和 autoload.php 路径是否正确;
  2. 21004 错误:共享密钥(sharedSecret)错误,需在 App Store Connect > 你的 App > 订阅 > 共享密钥 中确认;
  3. 收据为空:客户端传递的收据需是 base64 编码后的字符串,且未被篡改;
  4. 沙盒环境验证失败:确保测试账号是沙盒测试账号,且 App 已配置内购商品。

总结

该代码是 iOS 内购验证的核心逻辑,优化后增加了异常处理、自动环境切换、结果解析、日志记录等生产级特性,可直接集成到业务系统中。核心注意点:

  • 区分沙盒 / 生产环境;
  • 校验 transaction_id 防止重复发货;
  • 解析状态码并处理异常;
  • 订阅类商品需传递共享密钥。
相关推荐
坏小虎1 小时前
Expo 快速创建 Android/iOS 应用开发指南
android·ios·rn·expo
光影少年3 小时前
Android和iOS原生开发的基础知识对RN开发的重要性,RN打包发布时原生端需要做哪些配置?
android·前端·react native·react.js·ios
北京自在科技3 小时前
Find My 修复定位 BUG,AirTag 安全再升级
ios·findmy·airtag
Digitally3 小时前
如何不用 USB 线将 iPhone 照片传到电脑?
ios·电脑·iphone
Sim148016 小时前
iPhone将内置本地大模型,手机端AI实现0 token成本时代来临?
人工智能·ios·智能手机·iphone
Digitally18 小时前
如何将 iPad 上的照片传输到 U 盘(4 种解决方案)
ios·ipad
报错小能手20 小时前
ios开发方向——swift并发进阶核心 @MainActor 与 DispatchQueue.main 解析
开发语言·ios·swift
LcGero20 小时前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb
ii_best20 小时前
lua语言开发脚本基础、mql命令库开发、安卓/ios基础开发教程,按键精灵新手工具
android·ios·自动化·编辑器
用户223586218202 天前
WebKit WebPage API 的引入尝试与自研实现
ios