PHP服务端处理 ios内购(IPA)

支付功能在平时开发肯定都是家常便饭,我们在网站或者app中接入最多的就是支付宝和微信支付。ios App 端苹果内购什么情况下必须接入呢。

假入你的付费内容属于虚拟商品就必须接入苹果内购、否则审核就不能通过。那么哪些内容属于虚拟商品呢。举个栗子。像视频课程、网站会员、网站内的金币这些不需要配送实物的商品就属于虚拟商品。苹果官方规定必须使用苹果IAP应用内支付,给苹果分成30%。还是比较坑的。

苹果内购流程是通过客户端接入iOS的IAP模块后,由客户端发起支付,然后再把充值数据(receipt)发给服务端,最后由服务端远程调用AppStore服务器验证。这个过程要处理好订单问题,不然可能会出现丢单的情况。

上代码,这里我是在uniapp 里接入苹果内购。以下接口可在这里查看 这里使用vue的mixins 先定义一个applePay.js 文件

export default {
  data() {
    return {
      productIds: [
        'com.xxx.xxx.xxx',//在苹果开发者平台填写的产品ID,每一个都是唯一的
      ],
      iapChannel: null,
      object_id: "", 
      productId: '', //商品的标识
      appusername: '', //购买用户名称
      quantity: 1, //商品数量
      payedProductList: [], //从苹果服务器获取的已购买商品订单
    }
  },
  onHide() {
    plus.nativeUI.closeWaiting();
  },
  methods: {
    get_channel() {
      return new Promise((resolve, reject) => {
        plus.payment.getChannels((channels) => {
          console.log("获取到channel" + JSON.stringify(channels))
          for (var i in channels) {
            var channel = channels[i];
            if (channel.id === 'appleiap') {
              this.iapChannel = channel;
              resolve(channel)
            }
          }
          if (!this.iapChannel) {
            reject('暂不支持苹果 iap 支付')
          }
        }, (e) => {
          reject('获取支付通道列表失败:' + e.message)
        });
      });
    },
    // 获取已购买商品(非消耗性商品和订阅商品)
    restoreComplatePay() {
      this.payedProductList = [];
      this.iapChannel.restoreComplateRequest({}, function(response) {
        console.log(response);
      });
    },
    //从苹果服务器请求支付商品列表
    requestOrder() {
      return new Promise((resolve, reject) => {
        plus.nativeUI.showWaiting('检测支付环境...');
        this.iapChannel.requestOrder(this.productIds, function(e) { //IAP支付在调用plus.payment.request方法支付前须先向服务器请求获取商品的详细信息,否则会支付失败
          plus.nativeUI.closeWaiting();
          resolve(e)
        }, function(e) {
          plus.nativeUI.closeWaiting();
          plus.nativeUI.confirm("请求失败", function(e) {
            if (e.index == 0) {
              requestOrder();
            }
          }, '重新请求支付', ['确定', '取消']);
        });
      });
    },
    //带上当前需要支付的product_id 支付
    applePay() {
      let vm = this
      return new Promise((resolve, reject) => {
        plus.nativeUI.showWaiting('', {
          style: "black",
          background: "rgba(0,0,0,0)"
        });
        plus.payment.request(vm.iapChannel, {
          "productid": vm.productId,
          "username": vm.appusername,
          "quantity": vm.quantity,
        }, function(response) {
          plus.nativeUI.closeWaiting();
          //去服务端验证是否支付成功
          vm.$request({
            url: 'xxx',//后台验证是否支付成功
            data: {
              callback_data: JSON.stringify(response),
              object_id: vm.object_id
            },
            method: 'post',
            success: function(res) {
              console.log(res);//支付成功后的逻辑
            },
            error: function(e) {
              reject("支付失败");
            }
          })
        }, function(e) {
          plus.nativeUI.closeWaiting();
          reject("支付失败");
        });
      });
    }
  }
}

服务端

简单分为以下几步

  • 接收ios客户端发过来的购买凭证。
  • 判断凭证是否已经存在或验证过,然后存储该凭证。
  • 将该凭证发送到苹果的服务器验证,并将验证结果返回给客户端。客户端处理后续相关业务

苹果的验证接口文档:传送门。简单来说就是将该购买凭证POST发送给苹果的验证服务器,苹果将验证结果以JSON形式返回!

ApplePay.php 单例类

<?php
/**
 * 苹果内购订单查询
 * Class ApplyPay
 */
class ApplePay
{
    static protected $instance;
    //沙箱环境请求url
    static protected $sandbox_url = "https://sandbox.itunes.apple.com/verifyReceipt";

    //正式环境请求url
    static protected $production_url = "https://buy.itunes.apple.com/verifyReceipt";

    public static function instance($options = [])
    {
        if (!self::$instance instanceof self) {
            self::$instance = new self($options);
        }
        return self::$instance;
    }

    /**
     * 验证AppStore 内付
     * @param $receipt_data ios客户端生成的验签token 
     * @return bool|mixed|string
     */
    public function validate_apple_pay($receipt_data, $is_debug = false)
    {
		//        21000    App Store 不能读取你提供的JSON对象
		//        21002    receipt-data 域的数据有问题
		//        21003    receipt 无法通过验证
		//        21004    提供的 shared secret 不匹配你账号中的 shared secret
		//        21005    receipt 服务器当前不可用
		//        21006    receipt 合法, 但是订阅已过期. 服务器接收到这个状态码时, receipt 数据仍然会解码并一起发送
		//        21007    receipt 是 Sandbox receipt, 但却发送至生产系统的验证服务
		//        21008    receipt 是生产 receipt, 但却发送至 Sandbox 环境的验证服务

        $request_data = '{"receipt-data":"' . $receipt_data . '"}';

        // 沙箱环境
        if ($is_debug) $response = $this->httpRequest(self::$sandbox_url, $request_data);

        // 正式环境
        else $response = $this->httpRequest(self::$production_url, $request_data);

        if (!$response || !is_array($response) || !isset($response['status'])) return false;

        if ($response['status'] != 0) return false;

        return $response;
    }

    /**
     * curl 请求
     * @param $url
     * @param array $postData
     * @param bool $json
     * @return bool|mixed|string
     */
    protected function httpRequest($url, $postData = array(), $json = true)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        if ($postData) {
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
        }
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
        $info = curl_getinfo($ch);
        $data = curl_exec($ch);
        curl_close($ch);
        if ($json) {
            return json_decode($data, true);
        } else {
            return $data;
        }
    }
}

AppStore 服务器有两个,对应测试环境(沙盒测试)和正式环境:

测试环境: https://sandbox.itunes.apple.com/verifyReceipt

正式环境: https://buy.itunes.apple.com/verifyReceipt

我们开发的时候使用沙盒测试,在测试机上登陆沙箱账号即可

订单查询结果示例

Array
      (
      [receipt] => Array
     (
     [original_purchase_date_pst] => 2020-05-27 02:17:25 America/Los_Angeles //    原始购买日期(pst)
     [purchase_date_ms] => 1590573569426 //原始购买日期(ms)
     [unique_identifier] => fdced6f10c130c0dd430f9433ab3fcc29ac57fd5
     [original_transaction_id] => 1000000670630742 // 原始交易号
     [bvrs] => 100
     [transaction_id] => 1000000670666202 //    交易号
     [quantity] => 1 //购买数量
     [unique_vendor_identifier] => 3E2D4E5E-0875-45EC-82A5-F3BF37FD59FB
     [item_id] => 1515440278
     [version_external_identifier] => 0
     [bid] => com.alhelp.wordApp
     [is_in_intro_offer_period] => false
     [product_id] => collection_video //    商品标识符
     [purchase_date] => 2020-05-27 09:59:29 Etc/GMT //购买日期
     [is_trial_period] => false
     [purchase_date_pst] => 2020-05-27 02:59:29 America/Los_Angeles //购买日期(pst)
     [original_purchase_date] => 2020-05-27 09:17:25 Etc/GMT //    原始购买日期
     [original_purchase_date_ms] => 1590571045000
)
      [status] => 0
)

验证订单是否成功,主要看这几个数据:

1、status为 0 表示成功;其他均为失败

2、根据 receipt.in_app 字段判断iOS版本,验证方法也不同

iOS7及以上有in_app字段,验证 receipt.bundle_id 是否为你 App 的 bundle id,根据 in_app 处理充值的每一笔订单, 根据 product_id 判断用户充值了哪个档位,同时取出 transaction_id

iOS7以下没有in_app字段,验证 receipt.bid 是否为你 App 的 bundle id,根据 product_id 判断用户充值了哪个档位,同时取出 transaction_id

3、根据 transaction_id 对比数据库历史订单判断是否已处理过,没有则认为本次充值是有效的。

上面说到可能会出现掉单,比如网络异常,如果验证失败了,应该进行重试。

蔡关荣博客
请先登录后发表评论
  • latest comments
  • 总共1条评论
蔡关荣博客

沉默纪年灬 蔡关荣博客

2020-07-31 01:23:33 回复