https://developers.google.com/android-publisher
链接中开发者文档中Voided Purchases API已经给出了查询退款订单的步骤,本文给出了文档中没有提到的access_token的获取方式,并给出一个简单的demo
查询链接
GET https://www.googleapis.com/androidpublisher/v3/applications/${
yourPkgName}/purchases/voidedpurchases?access_token=${
AccessTokne}
接口限制
其他参数说明
AccessToken获取
// PS:首先你应该能拿到应用的包名和授权账号密钥对应的公钥,这里使用的是.json格式的公钥
import (
"context"
"net/http"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/androidpublisher/v3"
)
const credentials = []byte(``) // 公钥的json串
func getGoogleApiAccessToken() (token string, err error) {
c := &http.Client{
Timeout: 10 * time.Second}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
conf, err := google.JWTConfigFromJSON(credentials, androidpublisher.AndroidpublisherScope)
if err != nil {
return "", err
}
val := conf.Client(ctx).Transport.(*oauth2.Transport)
tokenObj, err := val.Source.Token()
if err != nil {
return "", err
}
return tokenObj.AccessToken, nil
}
获取退款数据
// 需要注意的地方已经写在代码里了
import (
"context"
"encoding/json"
"github.com/gomodule/redigo/redis"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2/google"
"io/ioutil"
"net/http"
"strconv"
"time"
)
// 注意数据项里的voidedSource和voidedReason,API文档里给的是字符串类型,但实际返回的为整型
// 返回数据json采取驼峰命名方式
type voidedPurchasesItem struct {
Kind string `json:"kind"`
PurchaseToken string `json:"purchaseToken"` // 收据信息
PurchaseTimeMillis string `json:"purchaseTimeMillis"` // 购买时间
VoidedTimeMillis string `json:"voidedTimeMillis"` // 退款时间
OrderId string `json:"orderId"` // google 生成的orderId
VoidedSource int `json:"voidedSource"`
VoidedReason int `json:"voidedReason"`
}
type googleVoidedPurchases struct {
TokenPagination struct {
NextPageToken string `json:"next_page_token"` // next page token不为空时,证明当前时间区间内数据超过了1000条,但只返回了1000条,可以传递此token继续获取
} `json:"tokenPagination"` // 数据少于1000条时,tokenPagination字段不存在
VoidedPurchases []voidedPurchasesItem `json:"voidedPurchases"`
}
const googleQueryApi = "https://www.googleapis.com/androidpublisher/v3/applications/${pkg_name}/purchases/voidedpurchases?access_token="
func pollingGoogleRefundOrder() {
accessToken, err := getGoogleApiAccessToken([]byte(credentials))
if err != nil {
logrus.Errorf("[pollingGoogleRefundOrder] get access token error:%s", err.Error())
return
}
// 获取上一次调用时间和token,用于增量拉取数据
// 这里使用redis记录数据,当然也可以使用文件、内存、数据库等其他方式存储
// time为上次调用时间 token用于30次循环仍没有处理完数据的情况
latestPollingTimeKey, purchasesTokenKey := "time", "token"
paramMap,err := redis.StringMap(client2.GetRedisConn().Do("HGetAll","google_polling_param"))
// 没有正确取到运行的参数,直接返回。若不返回使用默认参数去取,会处理较多的重复数据
if err != nil && err != redis.ErrNil {
logrus.Errorf("[pollingGoogleRefundOrder] get latest purchases tokene error:%s", err.Error())
return
}
client := &http.Client{
Timeout: time.Second * 5}
startRunningTime := strconv.Itoa(time.Now().Nanosecond()/1e6)
// API限制:1. 每天 6000 次查询(一天是从太平洋时间午夜开始,并于次日同一时间结束)。2.在任何时长为 30 秒的期限内查询 30 次。
// startTime 在响应中看到的最早作废的购买交易的时间。默认情况下,startTime 设为 30 天以前
// endTime 在响应中看到的最晚作废的购买交易的时间。默认情况下,endTime 设为当前时间。
// maxResults 每个响应中出现的已作废购买交易的数量上限。默认情况下,此值为 1000。最大值1000。
// token 之前响应中的继续令牌
// type 0,则系统只会返回已作废的应用内购买。1 时,系统将返回已作废的应用内购买和已作废的订阅购买。默认值为 0。
circleQueryCount := 0
latestPollingTime, latestPurchaseToken := paramMap[latestPollingTimeKey], paramMap[purchasesTokenKey]
for circleQueryCount < 30 {
// 对应API限制1
queryUrl := googleQueryApi + accessToken
if latestPollingTime != "" {
queryUrl += "&startTime=" + latestPollingTime
}
if latestPurchaseToken != "" {
queryUrl += "&token=" + latestPurchaseToken
}
res, err := client.Get(queryUrl)
if err != nil {
logrus.Errorf("[pollingGoogleRefundOrder] query url error:%s", err.Error())
time.Sleep(time.Second)
continue
}
if res.Body == nil || res.StatusCode != http.StatusOK {
logrus.WithField("res", res).Errorf("[pollingGoogleRefundOrder] request failed")
continue
}
bodyContent, err := ioutil.ReadAll(res.Body)
if err != nil {
logrus.WithField("res", res).Errorf("[pollingGoogleRefundOrder] read res body error:%s", err.Error())
res.Body.Close()
time.Sleep(time.Second)
continue
}
var voidedPurchasesInfo googleVoidedPurchases
err = json.Unmarshal(bodyContent, &voidedPurchasesInfo)
if err != nil {
logrus.WithField("res", res).Errorf("[pollingGoogleRefundOrder] unmarshal purchases info error:%s", err.Error())
res.Body.Close()
time.Sleep(time.Second)
continue
}
res.Body.Close()
for _, purchasesItem := range voidedPurchasesInfo.VoidedPurchases {
// todo something 执行更新订单状态等业务操作
logrus.Infoln(purchasesItem)
}
latestPurchaseToken = voidedPurchasesInfo.TokenPagination.NextPageToken
// 没有下一页,直接推出循环
if voidedPurchasesInfo.TokenPagination.NextPageToken == "" || len(voidedPurchasesInfo.VoidedPurchases) < 1000 {
break
}
circleQueryCount++
}
// 如果本时间区间的数据已经读取完成,记录下次轮训开始时间为本次执行时间
if latestPurchaseToken == "" {
latestPollingTime = startRunningTime
}
// 记录下一次的执行参数
client2.GetRedisConn().Do("HMSet",redis.Args{
}.Add("google_polling_param").AddFlat([]interface{
}{
latestPollingTimeKey, latestPollingTime, purchasesTokenKey, latestPurchaseToken})...)
if err != nil {
logrus.WithFields(logrus.Fields{
"ori": paramMap, "latest_polling_time":latestPollingTime,"latest_purchases_token":latestPurchaseToken}).Errorf(
"[pollingGoogleRefundOrder] cache polling param error:%s", err.Error())
}
// TODO 这里可以输出一些统计数据
logrus.Infof("[pollingGoogleRefundOrder] finish")
}