欢迎进入Unity内购系列
你好! 这将是一个系列的文章
第一篇 介绍客户端里支付的调起以及购买。
第二篇 介绍后台对购买结果的验证以及发货(IOS)。
第三篇 介绍后台对购买结果的验证以及发货(Android)。
第四篇 介绍后台对内购退单问题的处理(IOS欺诈检测以及欺诈信息反馈)。
第三篇 后台对购买结果的验证以及发货(Android)
本篇介绍PHP后台对购买结果的验证以及发货(Android)。
这是纯后端内容,不限于Unity项目,其他项目同样可用。
重要提示:
1、本人不会PHP,这是同事提供的代码,是项目实战代码,代码里有些遗漏的还请自行添加,万一有无法解决的可以留言,我再请教同事。
2、代码中涉及到sql和db相关的代码为数据库操作,需要根据自己的项目实现
一、基本流程
1、玩家在客户端内发起购买,并成功付款
2、Google返回购买凭证给客户端
3、客户端将购买凭证发送给服务器,由服务器向Google验证真伪
4、服务器根据验证结果给玩法发放相关虚拟商品
二、GooglePlay后台设置
1、在Google开发者后台中启用Google Play Android Developer项目
2、进入GoogleAPI控制台,创建一个Web 类型的客户端 ID,具体步骤可以参考我的另一篇文章《Unity接入Google登录超详细流程》
3、打开浏览器打开这个地址:
https://accounts.google.com/o/oauth2/auth?scope=https://www.googleapis.com/auth/androidpublisher&response_type=code&access_type=offline&redirect_uri=http://localhost:8080&client_id=123122121
其中redirect_uri换成自己第二部生成的客户端 ID时填入的网址(可以随便写,但是如果是真实的地址,他会很快跳转,导致拿不到想要的东西,所以一般情况填写http://localhost:8080),client_id换成自己第二部生成的客户端 ID
访问这个地址后会叫你登录google账号,就选择开发者这个账号登录
4、登录成功后,会在浏览器的地址栏中包含code,只要拿出code的值即可:
Code=4/bAEGOfkpDnG5hhtv8E7FSkKPp-oFVQpTPeg8l_jWjKQd5BaOviZLyimEywJR9ptEoFtRqb95sZh4yXfRLI81BbM
5、获取到code后发送post请求,以下POST请求实例以postman工具来进行操作
(1)、请求头 Content-Type = application/x-www-form-urlencoded
(2)、请求地址 https://accounts.google.com/o/oauth2/token
(3)、参数
grant_type=authorization_code(为固定值)
code=第4步中获取到的code值,
client_id=客户端ID,(第二步生成的客户端ID)
client_secret=客户端密钥,(第二步生成的客户端ID,里面对应的密钥)
redirect_url=重定向链接(http://localhost:8080)
6、请求结果有一个refresh_token,这个是永久的,而且只会返回一次,这个refresh_token需要保存给php,后续步骤需要用到。
如果出现Bad Request错误,拿不到Refresh Token,说明code已经失效了,要从头再来,删除web客户端,从第二步创建web客户端开始
以下3个步骤由php代码动态实现,不需要手动操作了
以下3个步骤由php代码动态实现,不需要手动操作了
以下3个步骤由php代码动态实现,不需要手动操作了
7、POST请求:携带refresh_token可获取access_token
(1)、请求头 Content-Type = application/x-www-form-urlencoded
(2)、请求地址 https://accounts.google.com/o/oauth2/token
(3)、参数
grant_type=refresh_token为固定值,
client_id=客户端ID,(第二步生成的客户端ID下载下来直接交给php)
client_secret=客户端密钥,(第二步生成的客户端ID下载下来直接交给php)
refresh_token=第5步中获取到的refresh_token值,
8、验证订单:GET请求(不再是POST)
GET地址:
https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{purchaseToken}?access_token=access_token
相关参数:
packageName=需要查询的应用ID(包名、gradle中的applicationId )
productId=开发者后台中创建好的商品ID
purchaseToken=客户端传过来的purchaseToken
access_token=第7步中获得的access_token
三、PHP代码实现
unity客户端需要传给php的数据:
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{
//支付成功返回给客户端的数据
var product = args.purchasedProduct;
Debug.Log("Purchase Complete - Product: " + product.definition.id);
string receipt = args.purchasedProduct.receipt;
//receipt 这个要发送给php
/*
此处为自己客户端跟php通信的代码
*/
return PurchaseProcessingResult.Complete;
}
php验证订单真伪:
/********************google内购start**********************/
/**
* 1.通过内购账户获得 code code 只能用一次
* 2.通过 code 获得 refresh_token 【刷新令牌时间一般比较长】
* 3.通过 refresh_token 获得 access_token 【一般3600s】
* 4.通过 access_token 访问 google api
*
* 参考:
* https://developers.google.com/android-publisher/authorization
* https://blog.csdn.net/alex_my/article/details/82984706#2__8
*
* [check_google_buy_order_exits 检查 google 内欧订单 成功就发送发游戏服务器 加某些东西]
* @param [int] $game_id [游戏 ID]
* @param [int] $player_id [玩家 ID]
*
* @param [string] $packageName [出售inapp产品的应用程序的软件包名称(例如,“ com.some.thing”)]
* @param [string] $productId [Inapp产品SKU(例如,“ com.some.thing.inapp1”)。] 产品id对应的钻石数=[1=>66,2=>268,3=>828,4=>1436,5=>3436,6=>7416]
* @param [string] $token [购买inapp产品时,提供给用户设备的令牌。]
* @return [] ['status'=>$code,'message'=>$msg,]
*/
public function check_google_buy_order_exits(){
// ini_set("display_errors","on");
// IException::setDebugMode(true);
// error_reporting(E_ALL | E_STRICT);
/*1 获取参数*/
$game_id = IFilter::act(IReq::get('game_id','post'),'int');
$child_game_id = IFilter::act(IReq::get('child_game_id','post'),'int');//子包id
$player_id = IFilter::act(IReq::get('player_id','post'),'int');
if(empty($game_id) || empty($player_id)) util::jsonError('缺少玩家参数!!');
empty($child_game_id) && $child_game_id = $game_id;
// packageName / purchases /
// products / productId /
// tokens / 令牌
$packageName = IFilter::act(IReq::get('packageName','post'),'string');
$productId = IFilter::act(IReq::get('productId','post'),'string');
$token = IFilter::act(IReq::get('token','post'),'string');
$order_no = IFilter::act(IReq::get('order_no','post'),'string');
if(empty($packageName) || empty($productId) || empty($token)) util::jsonError('缺少查询参数!!');
/*2 检查玩家真实性**************************************************/
$player_info=info::getPlayerInfo($player_id,$game_id);
// if(empty($player_info)) util::jsonError('玩家不存在!!');
/*2 查找订单*******************************************************/
// 2.1 获取 access_token
$access_token=$this->get_google_pay_access_token($child_game_id);
$url='https://www.googleapis.com/androidpublisher/v3/applications/'.$packageName.'/purchases/products/'.$productId.'/tokens/'.$token.'?access_token='.$access_token;
set::newsetlogfile("====".$url, 'check_google_buy_order_exits');
$result=get::httpRequest($url,'','get');
set::setlogfile($player_id.'谷歌内购支付'.$result);
$zuan_sku=[
53 => [
'gold1'=>[0.99,60,4],//每日首充送8,二次无赠送
'gold2'=>[4.99,300,4],
],
];
$zuan_sku = $zuan_sku[$child_game_id];
$result_arr=json_decode($result,TRUE); //数组
// 错误 400 订单无效 403 项目未关联
if(!empty($result_arr['error']) || empty($access_token)){
$obj = new IModel("player_order");
/*3.2 成功后修改数据库*/
$ordedr_data=[
'o_no' => $order_no?$order_no:0, //系统订单号
'o_game_id' => $game_id, //游戏id
'o_pid' => $player_info['p_id'], //玩家表id
// 'o_from_os' => 1, //1.android 2.ios
'o_item_id' => 4, //道具表id
'o_item_num' => $zuan_sku[$productId][1], //道具数量
'o_money' => $zuan_sku[$productId][0],
'o_status' => 0, //订单状态 0.未完成 1.已完成
'o_pay_status' => 0, //支付状态 0.未支付 1.已支付
'o_dopay_pid' => $player_id, //实际进行支付的玩家id
'o_order_type' => 0, //订单类型0直接付 1代付
'o_date' => time(), //创建日期
'o_isChecked' => 0, //是否回执 0.否 1.是
];
$obj->setData($ordedr_data);
$obj->add();
util::jsonError('错误'.$result_arr['error']['code'].':'.$result_arr['error']['message']);
}
// 3、订单的购买状态 0已购买 1取消 2待定
switch ($result_arr['purchaseState']) {
case '0':
$obj = new IModel("player_order");
/*3.1数据库中存在充值订单*/
$player_order_info=$obj->getObj('o_no="'.$result_arr['orderId'].'"');
if(!empty($player_order_info)) util::jsonError('此充值订单已完成');
//当充值商品为第一档时查询订单判断当天是否充值过
$first_sku = current(array_keys($zuan_sku));
if($productId==$first_sku){
$firstOrderCurrDay = $obj->query('o_pid='.$player_info['p_id'].' and o_date>='.strtotime(date('Y-m-d 00:00:00')),'o_id','o_id asc',1);
if(empty($firstOrderCurrDay)){
$zuan_sku[$first_sku][1] += 8 ;
}
}
/*3.2 成功后修改数据库*/
$ordedr_data=[
'o_no' => $result_arr['orderId'], //系统订单号
'o_game_id' => $game_id, //游戏id
'o_pid' => $player_info['p_id'], //玩家表id
// 'o_from_os' => 1, //1.android 2.ios
'o_item_id' => 4, //道具表id
'o_item_num' => $zuan_sku[$productId][1], //道具数量
'o_money' => $zuan_sku[$productId][0],
'o_status' => 1, //订单状态 0.未完成 1.已完成
'o_pay_status' => 1, //支付状态 0.未支付 1.已支付
'o_dopay_pid' => $player_id, //实际进行支付的玩家id
'o_order_type' => 0, //订单类型0直接付 1代付
'o_date' => time(), //创建日期
'o_isChecked' => 0, //是否回执 0.否 1.是
// 'o_apple_receipt'=>'' //苹果内验证数据
];
$obj->setData($ordedr_data);
$add_id=$obj->add();
break;
case '1': util::jsonError('订单购买状态:取消');
# code...
break;
case '2': util::jsonError('订单购买状态:待定');
# code...
break;
}
/*4 发送游戏服务器*/
// $params='msg=set'.'&openId='.$player_id.'&cardType='.$password_new1.'&cardNum='.;
$send_data=[
'msg'=>'chargeById',
'userId'=>$player_id,
'cardType'=> $zuan_sku[$productId][2], // 2房卡 4钻石 6金币
'cardNum'=>$zuan_sku[$productId][1],
'operType'=>2, //为什么有这一笔,enum 充值 赠送 都是 2
];
$params =http_build_query($send_data ,'','&');
$result=set::setMsgToGameServer($game_id,1,$params,1);
if($result){
if($result['errorCode']>0){
set::setlogfile("发送钻石失败,与游戏服务器通信失败:".$result['errorCode'].$result['errorMsg']);
// util::jsonError('发送钻石失败,与游戏服务器通信失败:'.$result['errorCode'].$result['errorMsg']);
}
}else{
set::setlogfile("发送钻石失败,与游戏服务器通信失败:未获取返回值".$result['errorCode']);
// util::jsonError('发送钻石失败,与游戏服务器通信失败:未获取返回值'.$result['errorCode']);
}
util::jsonSuccess(['number' => $ordedr_data['o_item_num']]);
}
/**
* [get_google_pay_access_token 获取 access_token]
* @param [string] $grant_type [参数类型]
* @param [string] $client_id [客户端ID]
* @param [string] $client_secret [客户端秘钥]
* @param [string] $refresh_token [刷新token] 用户获取access_token
* @return [string] []
*/
private function get_google_pay_access_token($game_id){
// 判断 access_token 是否过期
$access_token=Common::rget('access_token');
if(!empty($access_token)){
return $access_token;
}
// 1.获取 token 通过刷新token 获取 访问 token
$config_arr=$this->get_google_pay_josn_config($game_id); //获取基本数据
// 2.拼装 paeams and url
$send_params=[
'grant_type' =>'refresh_token',
'client_id' =>$config_arr['web']['client_id'],
'client_secret' =>$config_arr['web']['client_secret'],
//'refresh_token' =>'1//06otA7GjQrGkZCgYIARAAGAYSNwF-L9IrYZR_WaEEvsCFDZZZ-5d4qlfovUSvllLxyxz1hlHJVVP4zCfGbOXaIqxcepaG2ojSiRo'
'refresh_token' => $config_arr['web']['refresh_token'],
];
set::newsetlogfile("====".print_r($send_params,true), 'check_google_buy_order_exits');
$url='https://accounts.google.com/o/oauth2/token';
$send_params_str=http_build_query($send_params, '', '&');
// 获取数据
$response_data=get::http_post_data($url,$send_params_str); //对象 no array
set::newsetlogfile("==access_token-response_data==".print_r($response_data,true), 'check_google_buy_order_exits');
// 存库 redis
if(!empty($response_data)){
Common::rset_ex('access_token',$response_data->access_token,$response_data->expires_in);
}
return !empty($response_data)?$response_data->access_token:'';
}
/**
* [get_google_pay_josn_config 获取参数信息 ]
* @return [type] [数组 或 null]
*/
private function get_google_pay_josn_config($game_id){
$file_path= IWeb::$app->getBasePath().'plugins/google_pay/client_'.$game_id.'.json.txt';
if(file_exists($file_path)){
//将整个文件内容读入到一个字符串中
$arr= json_decode(file_get_contents($file_path),true);
// var_dump(file_get_contents($file_path));
// var_dump($arr);
return $arr;
}else{
return;
}
}
总结
1、GooglePlay关联到GoogleApi项目
(Google后台操作)
2、创建GoogleApi访问的客户端ID
(Google后台操作)
3、获取refresh_token
(Google后台操作)
4、获取access_token
(PHP代码实现)
5、验证订单并发货
(PHP代码实现)