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 防止重复发货;
  • 解析状态码并处理异常;
  • 订阅类商品需传递共享密钥。
相关推荐
TheNextByte12 小时前
如何在没有 Wi-Fi 的情况下备份 iPhone
ios·iphone
2501_915106322 小时前
iOS开发中CPU功耗监控的实现与工具使用
android·macos·ios·小程序·uni-app·cocoa·iphone
chinesegf2 小时前
如何在沙盒环境中进行内购测试
笔记·ios
TheNextByte13 小时前
如何使用数据线或无线方式将照片从Mac传输到 iPhone?
macos·ios·iphone
denggun1234513 小时前
ios开发逆向安全防抓包
安全·ios
Digitally1 天前
如何在Windows 10 PC上获取 iPhone短信
ios·iphone
脾气有点小暴1 天前
uv-drop-down-popup 在 iOS 真机中随屏幕滚动偏移
ios·uniapp·uv
2501_924064111 天前
2025年移动应用渗透测试流程方案及iOS与Android框架对比
android·ios
tangweiguo030519871 天前
Objective-C 核心语法深度解析:基本类型、集合类与代码块实战指南
开发语言·ios·objective-c