class GP
{
private $key = "";//JSON里的密钥
private $client_email = "";//JSON里的client_email
private $token_uri = "";//JSON里的token_uri
private $scope = 'https://www.googleapis.com/auth/androidpublisher';//以空格分隔的应用程序请求的权限列表。
private $package_name = "";//包名
public $st;//开始时间戳
public $et;//开始时间戳
public function __construct($st, $et)
{
$this->st = $st * 1000;//开始时间转毫秒
$this->et = $et * 1000;//开始时间转毫秒
}
/**
* 谷歌退款
* 获取access_token 接口请求文档 https://developers.google.com/identity/protocols/oauth2/service-account
* @return mixed
*/
private function getGPAccessToken()
{
//头部 标头包括两个字段,指示签名算法和确认格式。这两个字段都是必需的,每个字段只有一个值。随着其他算法和格式的引入,此标题将相应更改。
$header = [
'alg' => 'RS256', //生成signature的算法
'typ' => 'JWT' //类型
];
/**
* JWT 请求集中所需的要求如下所示。它们可以按任何顺序出现在声明集中。
名称 描述
iss 服务帐户的电子邮件地址。
scope 应用程序请求的以空格分隔的权限列表。
aud 断言的预期目标的描述。发出访问请求时,此值始终为。https://oauth2.googleapis.com/token
exp 确认的过期时间,以 1970 年 1 月 1 日 00:00:00 UTC 起的秒数指定。此值最长为发出时间后 1 小时。
iat 确认时间已给出,以 1970 年 1 月 1 日 00:00:00 UTC 起的秒数指定。
*/
$payload = ['iss' => $this->client_email, 'scope' => $this->scope, 'aud' => $this->token_uri, 'iat' => time(), 'exp' => time() + 1800];
// {Base64url encoded JSON header}
$jwtHeader = $this->base64url_encode(json_encode($header));
// {Base64url encoded JSON claim set}
$jwtClaim = $this->base64url_encode(json_encode($payload));
// The base string for the signature: {Base64url encoded JSON header}.{Base64url encoded JSON claim set}
openssl_sign($jwtHeader . "." . $jwtClaim, $jwtSig, $this->key, "sha256WithRSAEncryption");
$jwtSign = $this->base64url_encode($jwtSig);
// {Base64url encoded JSON header}.{Base64url encoded JSON claim set}.{Base64url encoded signature}
$jwtAssertion = $jwtHeader . "." . $jwtClaim . "." . $jwtSign;
return $this->requireByCurlQueryFun($this->token_uri, 'POST', urldecode(http_build_query([
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwtAssertion,
]))
);
}
private function base64url_encode($data)
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
public function rawData()
{
// 将原始数据直接存入数据库
try {
$access_token = json_decode($this->getGPAccessToken(), true);
if(isset($access_token['access_token'])) {
$access_token = $access_token['access_token'];
$data = $this->requireByCurlQueryFun('https://www.googleapis.com/androidpublisher/v3/applications/' .$this->package_name
.'/purchases/voidedpurchases?access_token='.$access_token.'&endTime='.$this->et.'&startTime='.$this->st, 'GET');
$data = json_decode($data, true);
if (is_null($data) || empty($data)) {
Log::info(__CLASS__.';'.__FUNCTION__.' No data');
} else {
if(isset($data['voidedPurchases'])) {
do {
// TODO 插入到表或其他逻辑
if(isset($data['tokenPagination']['nextPageToken']) && !empty($data['tokenPagination']['nextPageToken'])) {
$nextPageToken = $data['tokenPagination']['nextPageToken'];
$data = $this->requireByCurlQueryFun('https://www.googleapis.com/androidpublisher/v3/applications/'.$this->package_name
.'/purchases/voidedpurchases?access_token='.$access_token.'&endTime='.$this->et.'&startTime='.$this->st
.'&token='.$nextPageToken, 'GET');
$data = json_decode($data, true);
} else {
$nextPageToken = '';
}
} while(!empty($nextPageToken));
Log::info(__CLASS__.';'.__FUNCTION__.'success');
} else {
Log::error(__CLASS__.';'.__FUNCTION__.'fail:re='.json_encode($data));
}
}
} else {
Log::error(__CLASS__.';'.__FUNCTION__.'fail:token is empty; access_token='.json_encode($access_token));
}
} catch (\Exception $e) {
Log::error(__CLASS__.';'.__FUNCTION__.";获取gp退款回调失败,err=".$e->getMessage().';line:'.$e->getLine());
}
}
/**
* @param string $url URL地址
* @param string $mode GET or POST
* @param string $params 请求参数
* @param string $userAgent 是否设置useragent
* @param string $caFile 是否引用ca加密文件
* @param bool $needHeader
* @param int $timeout
* @return array|bool|string
*/
private function requireByCurlQueryFun($url, $mode, $params = '', $userAgent = "", $caFile = "", $needHeader = false , $timeout = 10)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if(!empty($userAgent)){
curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
}
if(!empty($caFile)){
curl_setopt($ch, CURLOPT_CAINFO, $caFile);
}
if ($needHeader) {
curl_setopt($ch, CURLOPT_HEADER, true);
}
if ($mode == 'POST') {
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:'));
curl_setopt($ch, CURLOPT_POST, true);
if (is_array($params)) {
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
} else {
curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
}
} else {
if (is_array($params)) {
$url .= (strpos($url, '?') === false ? '?' : '&') . http_build_query($params);
} else {
$url .= (strpos($url, '?') === false ? '?' : '&') . $params;
}
}
curl_setopt($ch, CURLOPT_URL, $url);
$result = curl_exec($ch);
if ($needHeader) {
$tmp = $result;
$result = array();
$info = curl_getinfo($ch);
$result['header'] = substr($tmp, 0, $info['header_size']);
$result['body'] = trim(substr($tmp, $info['header_size'])); //直接从header之后开始截取,因为 1.body可能为空 2.下载可能不全
//$info['download_content_length'] > 0 ? substr($tmp, -$info['download_content_length']) : '';
}
$errno = curl_errno($ch);
if ($errno) {
Log::channel('curl_err_log')->error("curl异常:", [
'url' => $url,
'mode' => $mode,
'params' => $params,
'errno' => $errno,
]);
}
curl_close($ch);
return $result;
}
}