写在最前
与支付宝支付和微信支付异步回调不同的是,苹果内购并不存在回调一说,交易是由商户服务器主动向 苹果内购服务发起验证请求。
具体流程
1.iOS客户端向苹果内购服务发起交易请求并拉起交易操作,此时,客户端会等待用户进行支付确认和支付验证(如指纹识别,面部识别等);
2.客户端支付成功后,会收到苹果内购服务返回的票据
信息(这个信息将用于客户端异常处理、与商户服务器的交互处理等);
3.客户端收到票据
信息后,将票据信息发送至商户服务器;
4.商户服务器收到票据
信息后,将票据信息发送至苹果内购服务器进行验证;
5.商户服务器接收到验证结果后进行后续逻辑处理(步骤4、5是同步操作);
6.商户服务器处理后,将处理结果下发给iOS客户端进行最后确认。
示例代码
/**
* @desc 苹果内购
* 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环境的验证服务
* @return json
*/
public function postApplePayValidateCallBack()
{
//小票信息
$post_data = ["receipt-data" => $_REQUEST["receipt-data"]];
$post_data_encode = json_encode($post_data);
//正式购买地址 沙盒购买地址
$url_buy = "https://buy.itunes.apple.com/verifyReceipt";
$url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt";
//根据配置信息决定支付环境是否为沙盒环境
$url = env("APPLE_PAY_SANDBOX", true) == true ? $url_sandbox : $url_buy;
//简单的curl
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data_encode);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$apple_response = curl_exec($ch);
curl_close($ch);
$apple_response_decode = json_decode($apple_response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
exit(json_encode([
"status" => 1,
"msg" => "[APPLE 001] Invalid request data",
"error_code" => "DL_APPLE_PAY_RESPONSE_ERROR",
"content" => []]));
}
//判断是否购买成功
if(intval($apple_response_decode['status']) === 0){
//写入充值信息
/**
**********************************************
*
* 由于客户端存在重新回调机制,因此"in_app"数组中可能存在多组历史数据,
* 这里进行遍历操作
* 此时操作类似于支付宝类的回调成功 处理逻辑
*
**********************************************
*/
if (
count($apple_response_decode["in_app"]) !== 0 &&
strtolower($apple_response_decode["environment"]) != "sandbox"
) {
foreach ($apple_response_decode["in_app"] as $deposit_array) {
//商品编号
$product_id = $deposit_array["product_id"];
//订单编号
$order_id = $deposit_array["transaction_id"];
//成功支付时间戳
$deposit_success_time = intval($deposit_array["purchase_date_ms"] / 1000);
//todo something
}
}
exit(json_encode([
"status" => 0,
"msg" => "deposit success",
"error_code" => "",
"content" => []]));
}
exit(json_encode([
"status" => 1,
"msg" => "[APPLE 001] Invalid request data: (" . $apple_response_decode["status"] . ")",
"error_code" => "DL_APPLE_PAY_INVALID_FAILED",
"content" => []]));
}
疑问
1.关于失败重发?
支付宝、微信支付等传统支付流程都会有支付异步回调通知,并且有失败重发机制,苹果内购怎么处理失败重发的问题?
首先,我们来观察一组苹果内购服务返回的验证数据(步骤4接收到的结果)
{
"receipt": {
"receipt_type": "ProductionSandbox",
"adam_id": 0,
"app_item_id": 0,
"bundle_id": "com.DD.livePlay",
"application_version": "3",
"download_id": 0,
"version_external_identifier": 0,
"receipt_creation_date": "2018-10-15 14:30:42 Etc/GMT",
"receipt_creation_date_ms": "1539613842000",
"receipt_creation_date_pst": "2018-10-15 07:30:42 America/Los_Angeles",
"request_date": "2018-10-15 15:02:48 Etc/GMT",
"request_date_ms": "1539615768080",
"request_date_pst": "2018-10-15 08:02:48 America/Los_Angeles",
"original_purchase_date": "2013-08-01 07:00:00 Etc/GMT",
"original_purchase_date_ms": "1375340400000",
"original_purchase_date_pst": "2013-08-01 00:00:00 America/Los_Angeles",
"original_application_version": "1.0",
"in_app": [{
"quantity": "1",
"product_id": "com.DD.livePlay.coin.6",
"transaction_id": "1000000457122701",
"original_transaction_id": "1000000457122701",
"purchase_date": "2018-10-14 04:02:51 Etc/GMT",
"purchase_date_ms": "1539489771000",
"purchase_date_pst": "2018-10-13 21:02:51 America/Los_Angeles",
"original_purchase_date": "2018-10-14 04:02:51 Etc/GMT",
"original_purchase_date_ms": "1539489771000",
"original_purchase_date_pst": "2018-10-13 21:02:51 America/Los_Angeles",
"is_trial_period": "false"
},
{
"quantity": "1",
"product_id": "com.DD.livePlay.coin.6",
"transaction_id": "1000000457173576",
"original_transaction_id": "1000000457173576",
"purchase_date": "2018-10-14 14:55:18 Etc/GMT",
"purchase_date_ms": "1539528918000",
"purchase_date_pst": "2018-10-14 07:55:18 America/Los_Angeles",
"original_purchase_date": "2018-10-14 14:55:18 Etc/GMT",
"original_purchase_date_ms": "1539528918000",
"original_purchase_date_pst": "2018-10-14 07:55:18 America/Los_Angeles",
"is_trial_period": "false"
},
{
"quantity": "1",
"product_id": "com.DD.livePlay.coin.12",
"transaction_id": "1000000457175666",
"original_transaction_id": "1000000457175666",
"purchase_date": "2018-10-14 15:10:48 Etc/GMT",
"purchase_date_ms": "1539529848000",
"purchase_date_pst": "2018-10-14 08:10:48 America/Los_Angeles",
"original_purchase_date": "2018-10-14 15:10:48 Etc/GMT",
"original_purchase_date_ms": "1539529848000",
"original_purchase_date_pst": "2018-10-14 08:10:48 America/Los_Angeles",
"is_trial_period": "false"
},
{
"quantity": "1",
"product_id": "com.DD.livePlay.coin.12",
"transaction_id": "1000000457640163",
"original_transaction_id": "1000000457640163",
"purchase_date": "2018-10-15 14:10:57 Etc/GMT",
"purchase_date_ms": "1539612657000",
"purchase_date_pst": "2018-10-15 07:10:57 America/Los_Angeles",
"original_purchase_date": "2018-10-15 14:10:57 Etc/GMT",
"original_purchase_date_ms": "1539612657000",
"original_purchase_date_pst": "2018-10-15 07:10:57 America/Los_Angeles",
"is_trial_period": "false"
},
{
"quantity": "1",
"product_id": "com.DD.livePlay.coin.12",
"transaction_id": "1000000457640514",
"original_transaction_id": "1000000457640514",
"purchase_date": "2018-10-15 14:12:36 Etc/GMT",
"purchase_date_ms": "1539612756000",
"purchase_date_pst": "2018-10-15 07:12:36 America/Los_Angeles",
"original_purchase_date": "2018-10-15 14:12:36 Etc/GMT",
"original_purchase_date_ms": "1539612756000",
"original_purchase_date_pst": "2018-10-15 07:12:36 America/Los_Angeles",
"is_trial_period": "false"
},
{
"quantity": "1",
"product_id": "com.DD.livePlay.coin.12",
"transaction_id": "1000000457649073",
"original_transaction_id": "1000000457649073",
"purchase_date": "2018-10-15 14:30:42 Etc/GMT",
"purchase_date_ms": "1539613842000",
"purchase_date_pst": "2018-10-15 07:30:42 America/Los_Angeles",
"original_purchase_date": "2018-10-15 14:30:42 Etc/GMT",
"original_purchase_date_ms": "1539613842000",
"original_purchase_date_pst": "2018-10-15 07:30:42 America/Los_Angeles",
"is_trial_period": "false"
}
]
},
"status": 0,
"environment": "Sandbox"
}
以上"in_app"数组中的值便是支付成功信息,支付成功一条,便会有一组信息。
啥?那为啥有多组数据?
在上述的流程步骤6中,iOS客户端收到商户服务器下发的消息后,才进行最后的交易确认处理,此时iOS客户端会与苹果内购服务进行交互,当成功处理后,内购服务器才认为这笔交易的逻辑彻底完成。
那么,在上述的流程步骤没有完整走通的情况下,内购服务会认为交易没有彻底完成,因此,每次向内购服务进行验证的时候(步骤4、5),都会收到未正常处理的历史记录("in_app"数组)。
此时,商户服务器应当处理"in_app"数组中的历史数据(到这里有没有似曾相识的感觉:没错,这就是失败重发)。
2.纵观验证返回数据,为什么没有看到商品价格信息?
我们能看到,在"in_app"数组中,每一组数据里面有一个键是"product_id",这键就是记录的商品编号。这个商品编号是商户服务器与iOS客户端约定的商品编号与价格的映射,比如此处的"com.DD.livePlay.coin.6"就是6元档位,"com.DD.livePlay.coin.12"就是12元档位。
咦?为啥是6、12...而不是整数?这就要说到人民币与美元的汇率问题...
3.读到这里,不知道你有没有发现,票据
是来源于iOS客户端,商户服务器如何确认数据来源的可信(这是否是我自己的iOS客户端)?
我们可以看到在"receipt"中,有一个键是"bundle_id",这个值便是iOS客户端的包名称,从这一点可以鉴别是否是自家商户可信任的客户端(因为步骤1、2操作会根据一定签名与内购服务器交互,数据安全性可以得到保证)。
感谢 墨墨的小孩 小伙伴的支持。