退款的濫用
針對退款,不同國家或地區(qū)會有不同的“無條件退款期限”,例如蘋果
- AppStore商店退款政策:
中國/美國/韓國 等大多數(shù)國家:90天有條件退款。
- 在中國區(qū)AppStore的具體退款政策:一個(gè)ID有1次無條件退款條件,一年2次有條件退款,第3次退款會非常難。至于退款到賬時(shí)間快為36小時(shí),也有7-15個(gè)工作日退還。
- 正是這些“漏洞”,所以出現(xiàn)專業(yè)的代充工作室,導(dǎo)致開發(fā)者壞賬非常嚴(yán)重。特別是火爆的游戲代充和直播行業(yè)。
退款的具體手段
- 利用淘寶店以代充打折的形式獲取玩家的游戲賬號,為玩家充值后申請退款。淘寶店獲得充值金,玩家獲得游戲商品,最終虧損的是游戲廠商。
- 收購消費(fèi)過的App Store ID賬號,收到的賬號會被用來退款和直播打賞,主播可以自己刷火箭然后申請退款。
針對海外支付,退款的方式主要是App Store和Google
App Store
-
AppStore退款****通知參考文檔
蘋果會定期通知****退款內(nèi)容,當(dāng)用戶退款后蘋果會調(diào)用配置的接口通知我們(被動(dòng)接收) - 配置url
- 登錄蘋果后臺 https://appstoreconnect.apple.com/
- 選擇需要通知的app應(yīng)用,點(diǎn)擊側(cè)邊欄的綜合->App信息
- 在AppStore服務(wù)器通知網(wǎng)址(URL)中配置我們的接口地址(必須為Https)
蘋果響應(yīng)信息
responsebody通知參數(shù)詳細(xì)文檔
從App Store在服務(wù)器通知中返回的是一條JSON數(shù)據(jù)的退款****通知,例子:
{
"notification_type": "REFUND",#觸發(fā)通知的訂閱事件。REFUND:退款
"environment": "PROD", #App Store生成收據(jù)的環(huán)境,可能是沙箱和生產(chǎn)環(huán)境
"latest_receipt": "",#不推薦使用。自2021年3月10日起,生產(chǎn)和沙箱環(huán)境中將不再提供此對象。
"latest_receipt_info": {},#不推薦使用。自2021年3月10日起,生產(chǎn)和沙箱環(huán)境中將不再提供此對象。
"unified_receipt": {
"status": 0,
"environment": "Production",
"latest_receipt_info": [{#最近100筆退款訂單信息
"quantity": "1",
"product_id": "com.xxxxxx.xmios.60",
"transaction_id": "490022793443160", #蘋果訂單號
"purchase_date": "2021-04-01 18:04:09 Etc/GMT",
"purchase_date_ms": "1617300249000",
"purchase_date_pst": "2021-04-01 11:04:09 America/Los_Angeles",
"original_purchase_date": "2021-04-01 18:04:09 Etc/GMT",
"original_purchase_date_ms": "1617300249000",
"original_purchase_date_pst": "2021-04-01 11:04:09 America/Los_Angeles",
"is_trial_period": "false",
"original_transaction_id": "490000793443160",
"cancellation_date": "2021-04-02 16:24:42 Etc/GMT", #退款時(shí)間
"cancellation_date_ms": "1617380682000", #退款時(shí)間 精確到毫秒
"cancellation_date_pst": "2021-04-02 09:24:42 America/Los_Angeles",
"cancellation_reason": "1",
"in_app_ownership_type": "PURCHASED"
}],
"latest_receipt": "" #不推薦使用。自2021年3月10日起,生產(chǎn)和沙箱環(huán)境中將不再提供此對象。
},
"bid": "com.xxxxxx.xmios",
"bvrs": "2.20"
}
這里我主要關(guān)注兩個(gè)字段
- cancellation_date(退款時(shí)間)
- transaction_id(蘋果訂單號)
unified_receipt存放著最近100筆退款****訂單信息,我們可以循環(huán)遍歷數(shù)組,通過數(shù)組下的transaction_id從數(shù)據(jù)庫中查到訂單信息,結(jié)合cancellation_date保存到退款記錄表。
-
[Google****退款****通知參考文檔]
https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.voidedpurchases/list
https://developers.google.com/android-publisher/voided-purchases
https://developers.google.com/android-publisher/getting_started#setting_up_api_access_clients
https://developers.google.com/android-publisher/api-ref/rest/v3/TokenPagination需定期請求Google****退款接口,獲取退款數(shù)據(jù)(主動(dòng)發(fā)起請求)
-
獲得訪問權(quán)限
想使用 Voided Purchases API,需要擁有查看財(cái)務(wù)信息的權(quán)限。可以使用 OAuth 客戶端或服務(wù)帳號來提供授權(quán)。這里我使用的是服務(wù)帳號,在帳號中啟用“查看財(cái)務(wù)報(bào)表”權(quán)限。- 登錄 Google Play 管理中心-》設(shè)置-》開發(fā)者賬號-》API權(quán)限
- 點(diǎn)擊服務(wù)帳號下的創(chuàng)建新的服務(wù)帳號,按照頁面上的說明創(chuàng)建您的服務(wù)帳號。
點(diǎn)擊 add 創(chuàng)建密鑰,創(chuàng)建JSON密鑰,準(zhǔn)備進(jìn)行授權(quán)的API調(diào)用。 - 在 Google Play 管理中心創(chuàng)建服務(wù)帳號后,點(diǎn)擊完成。API權(quán)限會自動(dòng)刷新。
- 點(diǎn)擊授予訪問權(quán),勾選財(cái)務(wù)數(shù)據(jù)(查看財(cái)務(wù)數(shù)據(jù)、訂單和用戶取消訂閱時(shí)對調(diào)查問卷的書面回復(fù))
-
獲得訪問授權(quán)
- 創(chuàng)建一個(gè)JWT,這里我以php為例:
class GoogleRefundCommand extends Command
{
protected $signature = 'google:refund {startTime?} {endTime?}';
private $key = "",//使用JSON里的密鑰
private $iss = ''; //服務(wù)帳戶的電子郵件地址。
private $sub = ''; //應(yīng)用程序正在請求委派訪問權(quán)限的用戶的電子郵件地址。
private $scope = 'https://www.googleapis.com/auth/androidpublisher';//以空格分隔的應(yīng)用程序請求的權(quán)限列表。
private $aud = 'https://oauth2.googleapis.com/token';//聲明的預(yù)期目標(biāo)的描述符。發(fā)出訪問令牌請求時(shí),此值始終為。https://oauth2.googleapis.com/token
private $package_name = ['','',''];
private $getTokenUrl = 'https://www.googleapis.com/androidpublisher/v3/applications/';
private $getTokenMethod = '/purchases/voidedpurchases';
private $client = "";
public function __construct()
{
parent::__construct();
$this->client = HttpAgent::getInstance();
}
private function base64url_encode($data)
{
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
/**
* 谷歌退款
* 獲取access_token 接口請求文檔 https://developers.google.com/identity/protocols/oauth2/service-account
* @return mixed
*/
public function handle()
{
//只在線上執(zhí)行
if (config('app.env') != 'production') {
Log::info('獲取谷歌獲取退款腳本執(zhí)行時(shí)間:' . date('Y-m-d H:i:s'));
return;
}
//頭部
$header = [
'alg' => 'RS256', //生成signature的算法
'typ' => 'JWT' //類型
];
$payload = ['iss' => $this->iss, 'sub' => $this->sub, 'scope' => $this->scope, 'aud' => $this->aud, '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;
dd($jwtAssertion);
// todo...
$ret = $this->client->request('post', 'https://oauth2.googleapis.com/token', ['query' => [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion' => $jwtAssertion,
]]);
}
}
這里我們使用postman測試一下
1. 請求 https://oauth2.googleapis.com/token 接口
2.grant_type = urn:ietf:params:oauth:grant-type:jwt-bearer,assertion使用 $jwtAssertion 參數(shù)

保存返回的授權(quán)令牌access_token
使用GET請求https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/voidedpurchases

startTime:您想在響應(yīng)中看到的最早作廢的購買交易的時(shí)間。默認(rèn)情況下,startTime 設(shè)為 30 天以前。注意:這里的startTime是毫秒
maxResults:每個(gè)響應(yīng)中出現(xiàn)的已作廢購買交易的數(shù)量上限。默認(rèn)情況下,此值為 1000。請注意,此參數(shù)的最大值也是 1000。
token: 之前響應(yīng)中的繼續(xù)令牌;可讓您查看更多結(jié)果。
Google響應(yīng)信息
{
"tokenPagination": {
"nextPageToken": "next_page_token"
},
"voidedPurchases": [
{
"kind": "androidpublisher#voidedPurchase",
"purchaseToken": "some_purchase_token",
"purchaseTimeMillis": "1468825200000",
"voidedTimeMillis": "1469430000000",
"orderId": "some_order_id",
"voidedSource": "0",
"voidedReason": "4"
},
{
"kind": "androidpublisher#voidedPurchase",
"purchaseToken": "some_other_purchase_token",
"purchaseTimeMillis": "1468825100000",
"voidedTimeMillis": "1470034800000",
"orderId": "some_other_order_id",
"voidedSource": "2",
"voidedReason": "5"
},
]
}
這里我主要關(guān)注兩個(gè)字段
voidedTimeMillis(退款時(shí)間)
orderId(Google訂單號)
voidedPurchases存放著maxResults條退款訂單信息,如果結(jié)果數(shù)量超過了在 maxResults請求參數(shù)中指定的數(shù)量,響應(yīng)就會包含一個(gè) nextPageToken值,這里我寫了一個(gè)遞歸函數(shù)判斷 nextPageToken是否為空,非空則將該值傳遞給后續(xù)請求來查看更多結(jié)果。