支付功能在平时开发肯定都是家常便饭,我们在网站或者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 对比数据库历史订单判断是否已处理过,没有则认为本次充值是有效的。
上面说到可能会出现掉单,比如网络异常,如果验证失败了,应该进行重试。
评论区