文章目录
欢迎进入Unity内购系列
你好! 这将是一个系列的文章
第一篇 介绍客户端里支付的调起以及购买。
第二篇 介绍后台对购买结果的验证以及发货(IOS)。
第三篇 介绍后台对购买结果的验证以及发货(Android)。
第四篇 介绍后台对内购退单问题的处理(IOS欺诈检测以及欺诈信息反馈)。
第四篇:后台对内购退单问题的处理(IOS欺诈检测以及欺诈信息反馈)
本篇介绍PHP后台对玩家退单问题处理,简称防欺诈。
这是纯后端内容,不限于Unity项目,其他项目同样可用。
重要提示:
1、本人不会PHP,这是同事提供的代码,是项目实战代码,代码里有些遗漏的还请自行添加,万一有无法解决的可以留言,我再请教同事。
2、代码中涉及到sql和db相关的代码为数据库操作,需要根据自己的项目实现
一、应用场景
玩家在游戏内充值成功之后,服务器会给玩家发送相应的道具,玩家将道具消耗,或者卖给其他人,然后在AppStore申请退款,此时如果我们不实现防欺诈接口,那么退款结果完全由苹果说了算,如果我们实现了防欺诈接口,那么玩家申请退单的时候,苹果会发送一条退款的通知到我们的服务器,我的服务器根据通知内容,查找对应的玩家,将玩家在游戏内的各种行为指标上报给苹果,然后苹果根据玩家行为决定是否退款。
二、基本流程
1、玩家申请退款
2、苹果发送退款通知给服务器
3、服务器解析通知数据
4、服务器根据解析出的数据,查询游戏中对应玩家的数据
5、服务器将玩家相关的数据通过接口上报给苹果(包括:玩家年龄,玩家消费频率,是否是新玩家,玩家是否获得物品,玩家的游戏账号状态等等)
三、AppStore后台设置
1、添加接受消息的Url
打开App Store connect,在 应用 > App信息 > 综合信息 > App Store 服务器通知网址 (URL)
中填入自己后台提供的地址,用于接受苹果推送的退款通知(例如:https://www.aaa.com/notify/AppleOrderRefund,其中必须要支持https)
2、获取相关的密钥以及证书,提供给PHP使用
(1)、private_key
1、选择 “用户和访问”,然后选择 “密钥” 子标签页 >
2、在 “密钥类型” 下选择 “App内购买项目” >
3、单击 “生成API内购买项目密钥”(如果之前创建过,则点击 “添加(+)” 按钮新增。) >
4、输入密钥的名称。名字可以随便取,名字不作为密钥的一部分 >
5、单击 “生成”
6、列表右边有 下载 App 内购买项目密钥 按钮,将密钥下载下来,这个就是private_key
(2)、pub_key
1、 公钥可以官网下载,这个公钥是所有人都一样的
苹果官网AppleRootCA-G3.cer下载
2、 将这个cer格式的公钥转为pem格式,转换方法自行百度
(3)、kid
1、 第一步生成内购买项目密钥时,密钥列表中间有个密钥 ID,把它拷贝出来,这个就是kid
(4)、bundle_id
1、 包名,不用多说
(5)、iss
1、选择 “用户和访问”,然后选择 “密钥” 子标签页 >
2、在 “密钥类型” 下选择 “App Store Connect API” >
3、单击 “生成API密钥” >
4、输入密钥的名称。名字可以随便取,名字不作为密钥的一部分 >
5、单击 “生成”
6、上方有个issuer ID,将这个id拷贝出来,这个就是iss
四、PHP代码实现
重要提示:PHP需要安装JWT环境,安装命令如下
// 安装JWT
composer require firebase/php-jwt
校验签名并解码返回数据:
// 校验签名并解码返回数据
use \Firebase\JWT\JWT;
use \Firebase\JWT\Key;
public function decodePayNotifyV2($jwt)
{
list($header, $payload, $sign) = explode('.', $jwt);
$header_decode = base64_decode($header);
$header_json = json_decode($header_decode, true);
if (!isset($header_json['x5c'])) {
// 解析失败
return false;
}
//这一步是将公钥转成对应的格式
$pubkey = "-----BEGIN CERTIFICATE-----\n" . $header_json['x5c'][0] . "\n-----END CERTIFICATE-----";
try {
$decoded = Firebase\JWT\JWT::decode($jwt, new Firebase\JWT\Key($pubkey, $header_json['alg']));
// 判断返回的证书链是否等于苹果公钥证书
$pem = $this->pub_key;
$str = str_replace("\r\n", "", $pem);
$pubPem = "-----BEGIN CERTIFICATE-----" . $header_json['x5c'][2] . "-----END CERTIFICATE-----";
if ($str != $pubPem) {
// 返回的公钥证书链错误
}
} catch (\Exception $e) {
// 签名校验失败
return false;
}
return json_decode(json_encode($decoded), true);
}
生成 JWT token:
//获取Authorization: Bearer
private $private_key = null;
private $pub_key = null;
private $header = null;
private $algorithm = "ES256";
private $payload = null;
private $kid = null;
private $bundle_id=null;
private $iss=null;
private function authorbeaer()
{
$this->payload = [
'iss' => $this->iss, //iss值
'iat' => intval(time()),
'exp' => intval(time() + 3600),
'aud' => 'appstoreconnect-v1', //固定值 https://appleid.apple.com
'bid' => $this->bundle_id, //应用bundle_id
];
//print_r($this->payload);
$this->header = [
"alg" => "ES256",
"kid" => $this->kid,
"typ" => "JWT"
];
//print_r($this->header);
$token = Firebase\JWT\JWT::encode($this->payload, $this->private_key, $this->algorithm, $this->kid, $this->header);
return $token;
}
提交欺诈信息:
private $private_key = null;
private $pub_key = null;
private $header = null;
private $algorithm = "ES256";
private $payload = null;
private $kid = null;
private $bundle_id=null;
private $iss=null;
public function AppleOrderRefund()
{
$data = array_merge($_POST,$_GET);
$content = file_get_contents("php://input");
$content = json_decode($content,true);
$jwt = $content['signedPayload'];
//需要从AppStore准备的5个数据
$this->pub_key = file_get_contents('AppleRootCA-G3.pem');
$this->private_key = file_get_contents('SubscriptionKey_9DY6HVVYLM.p8'); //密钥
$this->kid = '9DY6HVVYLM'; //kid
$this->bundle_id = 'com.abc.def'; //APP包名
$this->iss = 'c2072b74-65b7-4b12-9320-8699803627fa';
$payload_json = $this->decodePayNotifyV2($jwt);
if (empty($payload_json) || !isset($payload_json['data']))
{
return;
}
$t_payload_json = $this->decodePayNotifyV2($payload_json['data']['signedTransactionInfo']);
if (empty($t_payload_json) || empty($t_payload_json['originalTransactionId']))
{
return;
}
/*
* lifetimeDollarsPurchased 与 lifetimeDollarsRefunded
* 0. 终身购买金额未申报。
1. 终身购买金额为0美元。
2. 终身购买金额在0.01至49.99美元之间。
3. 终身购买金额在50-99.99美元之间。
4. 终身购买金额在100-499.99美元之间。
5. 终身购买金额在500–999.99美元之间。
6. 终身购买金额在1000–1999.99美元之间。
7. 终身购买金额超过2000美元。
* */
$originalTransactionId = $t_payload_json['originalTransactionId'];
$url = 'https://api.storekit.itunes.apple.com/inApps/v1/transactions/consumption/'.$originalTransactionId;
$player_order_DB = new IModel('player_order');
$player_order_info = $player_order_DB->getObj("o_no=".$originalTransactionId);
if(empty($player_order_info))
{
return;
}
if($payload_json['notificationType'] != 'CONSUMPTION_REQUEST'){
$AppleOrderRefund_log_DB = new IModel('AppleOrderRefund_log');
$time = time();
$sql = "update g_player_order set final_result='{
$payload_json['notificationType']}',update_time=$time where o_no='{
$originalTransactionId}' limit 1";
$AppleOrderRefund_log_DB->dbquery($sql);
$sql = "update g_AppleOrderRefund_log set final_result='{
$payload_json['notificationType']}',update_time=$time where originalTransactionId='{
$originalTransactionId}' limit 1";
$AppleOrderRefund_log_DB->dbquery($sql);
return;
}
$o_pid = $player_order_info['o_pid'];
$sql = "select sum(o_money) as o_money_total from g_player_order where o_pid={
$o_pid} and o_pay_status=1";
$money_total_res = $player_order_DB->dbquery($sql);
$o_money_total = isset($money_total_res[0]) ? $money_total_res[0]['o_money_total'] : 0;
$o_money_total_USD = $o_money_total/300;//换算成美元
if($o_money_total_USD==0){
$lifetimeDollarsPurchased = 1;
}elseif($o_money_total_USD >= 0.01 && $o_money_total_USD < 49.99){
$lifetimeDollarsPurchased = 2;
}elseif($o_money_total_USD >= 50 && $o_money_total_USD < 99.99){
$lifetimeDollarsPurchased = 3;
}elseif($o_money_total_USD >= 100 && $o_money_total_USD < 499.99){
$lifetimeDollarsPurchased = 4;
}elseif($o_money_total_USD >= 500 && $o_money_total_USD < 999.99){
$lifetimeDollarsPurchased = 5;
}elseif($o_money_total_USD >= 1000 && $o_money_total_USD < 1999.99){
$lifetimeDollarsPurchased = 6;
}elseif($o_money_total_USD >= 2000){
$lifetimeDollarsPurchased = 7;
}
$request_data = [
'accountTenure' => '1',//用户账户的年龄
'appAccountToken' => $payload_json['notificationUUID'],
'consumptionStatus' => '3',//消耗状态 0:不确定;1:没消耗;2:部分消耗;3:完全消耗
'customerConsented' => true,//用户是否同意提供消费数据 true:同意
'deliveryStatus' => 0,//应用是否成功交付了正常运行的应用内购买
'lifetimeDollarsPurchased' => $lifetimeDollarsPurchased,//计算用户所有充值的总金额
'lifetimeDollarsRefunded' => '1',//计算用户所有退款的总金额
'platform' => 1,
'playTime' => 1,
'sampleContentProvided' => false,
'userStatus' => 1,
];
$ch = curl_init($url);
$data_query = json_encode($request_data);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
curl_setopt($ch, CURLOPT_HEADER, true);
$apple_jwt_token = $this->authorbeaer();
$token = $apple_jwt_token;
if(!$token)
{
return;
}
$header = [
"Authorization: Bearer $token",
"Content-Type: application/json",
];
curl_setopt($ch, CURLOPT_HTTPHEADER,$header);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_query);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
$response = curl_exec($ch);
if ($response === false){
}
curl_close($ch);
$_db = new IQuery('AppleOrderRefund_log');
$request_data['originalTransactionId'] = $originalTransactionId;
$request_data['result'] = print_r($response,true);
$request_data['add_time'] = time();
$request_data['final_result'] = $payload_json['notificationType'];
$request_data['update_time'] = time();
$_db->setData($request_data);
$_db->add();
$time = time();
$sql = "update g_player_order set final_result='{
$payload_json['notificationType']}',update_time=$time where o_no='{
$originalTransactionId}' limit 1";
$_db->dbquery($sql);
}
五、参考文档
苹果官方文档:App Store Server API
WWDC21 - App Store Server API 实践总结
PHP App Store Server API 苹果API退款 查询订单 历史订单 PHP校验签名解码
AppStore服务端通知(订阅/退款回调通知)
总结
1、防欺诈不需要客户端参与
2、PHP实现接口处理AppStore通知的数据
3、防欺诈只能协助Apple处理退款请求,而不能100%决定退款结果