前言

由于 Google Play 的订阅有 13 种状态,在各种状态流转的复杂性超过我的想象,甚至有推送消息内的触发时间更早的消息更晚到的情况。

这里写一篇博文来记录各种坑。

一、Google 开发者平台配置回调

使用推送订阅 貌似官方文档说不用网域验证了。

先把域名添加到 search 后台,打开 search-console,选择网域填写域名,获得要验证的 DNS 记录,填写到 DNS 配置后台的 TXT 记录里,然后直接验证,在 Cloud 控制台的 网域验证 添加白名单。

主题 页面创建好新的主题。

google-cloud-create-top

并且需要设置 google-play-developer-notifications@system.gserviceaccount.com 的账号为发布者,文档见

google-cloud-top-publisher

订阅 面板发现已经有一个 Pull 类型的订阅了,不用理,新建一个订阅设置为推送模式,设置对应要推送的地址,下面还有一个 重试政策 设置建议改成 在按照指数退避算法确定的延迟时间后重试

google-cloud-create-sub

最后通过 创收设置 来检查是否能够由 google 来通知到我们。

google-play-publish-test

下面是一个简单的 http 服务,可以选择使用 ngrok 或者自己的域名加外网服务器去实现 https

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
	"encoding/json"
	"io"
	"log"
	"net/http"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/sub", func(w http.ResponseWriter, req *http.Request) {
		defer req.Body.Close()
		log.Println("Authorization:", req.Header.Get("Authorization"))
		body, _ := io.ReadAll(req.Body)
		log.Println("Body:", string(body))
		// 返回 200 状态通知 google 这个通知已经接受
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.WriteHeader(http.StatusOK)
		_, _ = fmt.Fprintln(w, "ok")
	})
	http.ListenAndServe(":8000", mux)
}

接收到的数据会像下面的格式。

1
2
3
4
5
6
7
8
9
Authorization: Bearer eyJhbGciOiJSUzI1Ni..
Body: {
    "message":{
        "data": "eyJ2ZXJzaW9u..",
        "messageId": "3220015587906257",
        "publishTime": "2021-10-15T03:27:58.356Z"
    },
    "subscription": "上面的主题"
}

二、订阅流程

测试的订阅在购买后的 5 分钟内如果没有确认该订阅商品,否则会自动退款并关闭订阅。

subscription-flow

数据示例

订阅发起:

1
2
3
4
5
// 2021/10/25 11:49:11 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635133750992","subscriptionNotification":{"version":"1.0","notificationType":4,"purchaseToken":"token","subscriptionId":"sku"}}

// 2021/10/25 11:49:12 purchase
{"autoRenewing":true,"countryCode":"US","expiryTimeMillis":"1635134157989","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57296937369853","orderId":"GPA.61","paymentState":1,"priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635133750347"}

续订成功:

1
2
3
4
5
// 2021/10/25 11:53:29 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635134007674","subscriptionNotification":{"version":"1.0","notificationType":2,"purchaseToken":"token","subscriptionId":"sku"}}

// 2021/10/25 11:53:30 purchase
{"acknowledgementState":1,"countryCode":"US","expiryTimeMillis":"1635133706342","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57296301053176","orderId":"GPA.61..0","priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635133124357","userCancellationTimeMillis":"1635133460780"}

续订失败(进入宽待期):

1
2
3
4
5
// 2021/10/25 11:59:04 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635134341983","subscriptionNotification":{"version":"1.0","notificationType":6,"purchaseToken":"token","subscriptionId":"sku"}}

// 2021/10/25 11:59:04 purchase
{"acknowledgementState":1,"autoRenewing":true,"countryCode":"US","expiryTimeMillis":"1635134464683","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57296937369853","orderId":"GPA.61..1","priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635133750347"}

续订失败(进入保留期):

1
2
3
4
5
// 2021/10/25 12:04:01 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635134639719","subscriptionNotification":{"version":"1.0","notificationType":5,"purchaseToken":"token","subscriptionId":"com.android.499"}}

// 2021/10/25 12:04:02 purchase
{"acknowledgementState":1,"autoRenewing":true,"countryCode":"US","expiryTimeMillis":"1635134637989","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57296937369853","orderId":"GPA.61..1","priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635133750347"}

恢复订阅(注意这个恢复需要根据业务来处理,如果你在宽带期发过周期性奖励,这里就不能再次发放,只能发放持续奖励):

1
2
3
4
5
// 2021/10/25 12:05:54 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635134752789","subscriptionNotification":{"version":"1.0","notificationType":1,"purchaseToken":"token","subscriptionId":"com.android.499"}}

// 2021/10/25 12:05:55 purchase
{"acknowledgementState":1,"autoRenewing":true,"countryCode":"US","expiryTimeMillis":"1635135172433","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57296937369853","orderId":"GPA.61..1","paymentState":1,"priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635133750347"}

用户订阅暂停计划触发通知:

1
2
3
4
5
// 2021/10/25 12:11:41 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635135099444","subscriptionNotification":{"version":"1.0","notificationType":11,"purchaseToken":"token","subscriptionId":"com.android.499"}}

// 2021/10/25 12:11:41 purchase
{"acknowledgementState":1,"autoRenewing":true,"autoResumeTimeMillis":"1635135772433","countryCode":"US","expiryTimeMillis":"1635135592433","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57296937369853","orderId":"GPA.61..2","paymentState":1,"priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635133750347"}

订阅已经暂停:

1
2
3
4
5
// 2021/10/25 12:17:55 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635135473787","subscriptionNotification":{"version":"1.0","notificationType":10,"purchaseToken":"token","subscriptionId":"com.android.499"}}

// 2021/10/25 12:17:56 purchase
{"acknowledgementState":1,"autoRenewing":true,"autoResumeTimeMillis":"1635135772433","countryCode":"US","expiryTimeMillis":"1635135472433","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57296937369853","orderId":"GPA.61..2","paymentState":1,"priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635133750347"}

从暂停计划恢复

1
2
3
4
5
// 2021/10/25 12:22:58 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635135776362","subscriptionNotification":{"version":"1.0","notificationType":1,"purchaseToken":"token","subscriptionId":"com.android.499"}}

// 2021/10/25 12:22:59 purchase
{"acknowledgementState":1,"autoRenewing":true,"countryCode":"US","expiryTimeMillis":"1635136195923","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57296937369853","orderId":"GPA.61..3","paymentState":1,"priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635133750347"}

取消订阅:

1
2
3
4
5
// 2021/10/25 12:24:26 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635135864707","subscriptionNotification":{"version":"1.0","notificationType":3,"purchaseToken":"token","subscriptionId":"com.android.499"}}

// 2021/10/25 12:24:27 purchase
{"acknowledgementState":1,"countryCode":"US","expiryTimeMillis":"1635136075923","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57296937369853","orderId":"GPA.61..3","paymentState":1,"priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635133750347","userCancellationTimeMillis":"1635135864171"}

订阅已到期:

1
2
3
4
5
// 2021/10/25 15:54:05 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635148443335","subscriptionNotification":{"version":"1.0","notificationType":13,"purchaseToken":"token","subscriptionId":"com.android.499"}}

// 2021/10/25 15:54:06 purchase
{"acknowledgementState":1,"countryCode":"US","expiryTimeMillis":"1635147242010","orderId":"GPA.44..3","priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635145747564","userCancellationTimeMillis":"1635146950247"}

取消后未到期的情况在 Play 后台恢复订阅(会把之前的uid复制过来)

1
2
3
4
5
// 2021/10/25 15:56:21 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635148579496","subscriptionNotification":{"version":"1.0","notificationType":7,"purchaseToken":"token","subscriptionId":"com.android.499"}}

// 2021/10/25 15:56:21 purchase
{"acknowledgementState":1,"autoRenewing":true,"countryCode":"US","expiryTimeMillis":"1635148830612","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57311609629510","orderId":"GPA.30","paymentState":1,"priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635148415972"}

到期后通过 Play 重新订阅(没有对应的商品和账号,需要客户端配合做恢复订阅功能)

1
2
3
4
5
// 2021/10/25 16:03:38 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635149016374","subscriptionNotification":{"version":"1.0","notificationType":4,"purchaseToken":"token","subscriptionId":"com.android.499"}}

// 2021/10/25 16:03:39 purchase
{"autoRenewing":true,"countryCode":"US","expiryTimeMillis":"1635149433007","orderId":"GPA.87","paymentState":1,"priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635149015784"}

取消后在未到期的情况下再一次从 app 里购买(一次性周期奖励这里可不能再发放,不过这里没有明确的说明为已经有一个订阅在过程中,需要自己用过期时间判断)

1
2
3
4
5
// 2021/10/28 15:40:54 subscription
{"version":"1.0","packageName":"com.bingo.crown.android","eventTimeMillis":"1635406853999","subscriptionNotification":{"version":"1.0","notificationType":4,"purchaseToken":"token","subscriptionId":"600271.com.bingo.crown.android.elite.499"}}

// 2021/10/28 15:40:55 purchase
{"autoRenewing":true,"countryCode":"US","expiryTimeMillis":"1635407160887","kind":"androidpublisher#subscriptionPurchase","linkedPurchaseToken":"token","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57570050407351","orderId":"GPA.04","paymentState":1,"priceAmountMicros":"5015570","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635406853066"}

三、订阅客户端调起

和内购不一样的地方是,查询 sku 时需要设置为订阅类型,购买后的 token 也无法用于服务端确认,订阅也没有客户端消耗的逻辑。

查询 sku

1
2
3
4
5
6
ArrayList<String> list = new ArrayList<String>();
SkuDetailsParams params = SkuDetailsParams.newBuilder()
    .setType(com.android.billingclient.api.BillingClient.SkuType.SUBS)
    .setSkusList(list)
    .build();
billingClient.querySkuDetailsAsync(params, this);

发起购买:

1
2
3
4
5
6
7
SkuDetails skuDetail;
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
    .setSkuDetails(skuDetail)
    .setObfuscatedProfileId("self-order-id")
    .setObfuscatedAccountId("self-user-id")
    .build();
BillingResult result = this.bc.launchBillingFlow(this, billingFlowParams);

四、服务端处理订阅发起

最上面设置了主题和订阅的地址,在订阅发生变化时会由 google 向我们设置的地址推送消息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package main

import (
	"encoding/json"
	"io"
	"log"
	"net/http"
)


// SubscriptionNotification https://developer.android.google.cn/google/play/billing/rtdn-reference#sub
type SubscriptionNotification struct {
	Version          string `json:"version,omitempty"`          // 版本
	NotificationType int    `json:"notificationType,omitempty"` // 变化类型
	PurchaseToken    string `json:"purchaseToken,omitempty"`    // 支付令牌
	SubscriptionId   string `json:"subscriptionId,omitempty"`   // 商品 id
}

type OneTimeProductNotification struct {
	Version          string `json:"version,omitempty"`          // 版本
	NotificationType int    `json:"notificationType,omitempty"` // 变化类型
	PurchaseToken    string `json:"purchaseToken,omitempty"`    // 支付令牌
	SKU              string `json:"sku,omitempty"`
}

type TestNotification struct {
	Version string `json:"version,omitempty"` // 版本
}

type SubscriptionData struct {
	Version                    string                      `json:"version,omitempty"`
	PackageName                string                      `json:"packageName,omitempty"`
	EventTimeMillis            string                      `json:"eventTimeMillis,omitempty"`
	SubscriptionNotification   *SubscriptionNotification   `json:"subscriptionNotification,omitempty"`
	OneTimeProductNotification *OneTimeProductNotification `json:"oneTimeProductNotification,omitempty"`
	TestNotification           *TestNotification           `json:"testNotification,omitempty"`
}


type pushRequest struct {
	Message      pubsub.PubsubMessage `json:"message"`
	Subscription string               `json:"subscription"`
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/sub", func(w http.ResponseWriter, req *http.Request) {
		defer req.Body.Close()
		body, _ := io.ReadAll(req.Body)

		var pr pushRequest
		err = json.Unmarshal(body, &pr)
		if err != nil {
			return
		}
		dataByte, err := base64.StdEncoding.DecodeString(pr.Message.Data)
		if err != nil {
			return
		}
		var subscriptionData = &config.SubscriptionData{}
		err = json.Unmarshal(dataByte, subscriptionData)
		if err != nil {
			return
		}
		if subscriptionData.SubscriptionNotification == nil {
			return
		}
        switch subscriptionData.SubscriptionNotification.NotificationType {
            // 自行根据状态来处理
        }
		// 返回 200 状态通知 google 这个通知已经接受
		w.Header().Set("Content-Type", "text/html; charset=utf-8")
		w.WriteHeader(http.StatusOK)
		_, _ = fmt.Fprintln(w, "ok")
	})
	http.ListenAndServe(":8000", mux)
}

五、服务端处理订阅状态变化

在订阅中有几个状态需要特别注意,如果你的订阅商品除了类似 buff 的周期性奖励还有每次的一次性奖励,不注意的话会出现被刷。

取消后立即订阅

4:开始订阅 -> 3:取消订阅 -> 4:在未到期之前在 app 里又一次订阅(由于类型还是 4 可能会再一次发放奖励)。

取消订阅:

1
2
3
4
5
// 2021/10/25 12:24:26 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635135864707","subscriptionNotification":{"version":"1.0","notificationType":3,"purchaseToken":"token","subscriptionId":"com.android.499"}}

// 2021/10/25 12:24:27 purchase
{"acknowledgementState":1,"countryCode":"US","expiryTimeMillis":"1635136075923","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57296937369853","orderId":"GPA.61..3","paymentState":1,"priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635133750347","userCancellationTimeMillis":"1635135864171"}

再次从商店订阅需要自己用过期时间判断:

1
2
3
4
5
// 2021/10/28 15:40:54 subscription
{"version":"1.0","packageName":"com.bingo.crown.android","eventTimeMillis":"1635406853999","subscriptionNotification":{"version":"1.0","notificationType":4,"purchaseToken":"token","subscriptionId":"600271.com.bingo.crown.android.elite.499"}}

// 2021/10/28 15:40:55 purchase
{"autoRenewing":true,"countryCode":"US","expiryTimeMillis":"1635407160887","kind":"androidpublisher#subscriptionPurchase","linkedPurchaseToken":"token","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57570050407351","orderId":"GPA.04","paymentState":1,"priceAmountMicros":"5015570","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635406853066"}

宽待期

续订失败(进入宽待期):

1
2
3
4
5
// 2021/10/25 11:59:04 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635134341983","subscriptionNotification":{"version":"1.0","notificationType":6,"purchaseToken":"token","subscriptionId":"sku"}}

// 2021/10/25 11:59:04 purchase
{"acknowledgementState":1,"autoRenewing":true,"countryCode":"US","expiryTimeMillis":"1635134464683","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57296937369853","orderId":"GPA.61..1","priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635133750347"}

保留期

续订失败(进入保留期):

1
2
3
4
5
// 2021/10/25 12:04:01 subscription
{"version":"1.0","packageName":"com.google.android","eventTimeMillis":"1635134639719","subscriptionNotification":{"version":"1.0","notificationType":5,"purchaseToken":"token","subscriptionId":"com.android.499"}}

// 2021/10/25 12:04:02 purchase
{"acknowledgementState":1,"autoRenewing":true,"countryCode":"US","expiryTimeMillis":"1635134637989","obfuscatedExternalAccountId":"1","obfuscatedExternalProfileId":"57296937369853","orderId":"GPA.61..1","priceAmountMicros":"5017006","priceCurrencyCode":"USD","purchaseType":0,"startTimeMillis":"1635133750347"}

参考