本文写于 2024-1-26
接上一次苹果内购记录-CSDN博客,我又接了一把 Google Play 内购。
Google Play 内购有啥好说的?一般人都不用。
只能说时代变了,没想到啊没想到,Google Play 这个浓眉大眼的的家伙也叛变了。
你看,真叛变了吧?
1. 准备事项
- Google Play 账号
- Google Cloud 账号
- 付款信息
- 提供一个账号给开发
- 配置内购产品信息:名称和价格这些属性
嘿嘿,以上这些事情统统交给产品。如果没有产品,那就交给老板吧。

如果他们不愿意,和他们说的时候带上这个

3. 开搞前缕缕思路
思路和苹果内购应该是差不多的,毕竟苹果内购"珠玉在前"嘛。

和苹果内购一样,这里最关键的一步是:【完成交易】。如果这一步出问题,那就是用户给钱了,但没给用户发服务。和苹果一脉相承的掉单问题。老规矩:
第一步:穿上西装站直 第二步:弯腰90度 第三步:虔诚地说出:su mi ma sen 第四步:请不要着急,如果没收到权益,3天后自动给您退款
Google Play 这波是被掉单搞怕了,哈哈。
言归正传:
第一步发现问题:
- 用户写邮件来对线了
- 每天查一下 Google Play 后台交易成功的数量和后台数据库交易成功的数量能不能对上
- 客户端和服务端加上日志
第二步排查问题:
- 用户找你的时候要他提供订单号,然后可以通过Google Play 后台查询到这笔订单的信息
- Google Play 后台数据和服务后台数据对不上的时候赶紧查日志去
第三步解决问题:
- 客户端做另外的掉单处理,比如说轮训查还有没有交易是没完成的,然后重新提交服务端
- 手动给用户补发
- 退款(Google Play 后台可以退,终于不用被别人掐住脖子了)
4. 差不多了,开搞
官方整合文档:Google Play 结算系统 | Google Play's billing system | Android Developers
4.1 准备
- 创建一个 Google Cloud Project,并启用相关API和服务
- Google Play 应用关联 Google Cloud Project
- Google Cloud 创建ServiceAccount服务账号
- Google Play 添加服务账号,并分配权限
- Google Cloud Pub/Sub创建主题以及订阅
- Google Play 启用实时通知,并配置Google Cloud 中创建的主题
- 创建Google Cloud Project,并启用相关API和服务

- Google Play 中关联 Google Cloud Project

- Google Cloud 创建ServiceAccount服务账号

创建完账号,把密钥也创建好,下载到本地

- Google Play 添加服务账号,并分配权限

把创建好的账号填进来

这一步完成,其实就可以使用服务账号的密钥信息调用 Google Play API了。下面只是为了接收Google Play 通知的时候用的,不用的可以不看,当然我建议你看看,不然用户退款的时候,我估计老板会找你。

Google Play 把产品购买分成两种类型:
- 一次性购买
- 订阅
对于一次性购买来说,实时通知只是为了接收退款的回调,购买成功是没有回调的,喵的,文档里面写了有事件,实际又没发。这个破问题了困扰了我一天,最后社区问到的答案。
RTDN messages are not sent to Topic when using Test licences - Google Play Developer Community(*...+#@>此处省略1800个字)
对于订阅来说,不说了。我没做订阅,你自己去看吧。
订阅生命周期 | Google Play's billing system | Android Developers
- Google Cloud Pub/Sub创建主题以及订阅

给Google Play 固定的服务账号加权限,注意这个账号是一个固定的,不是刚刚创建的哪个服务账号 [email protected]


- Google Play 启用实时通知,并配置Google Cloud 中创建的主题

点击发送测试通知,测试一下。然后在 Pub/Sub 中看看能不能拉取到消息

没问题之后,把订阅的类型改成推送,这样就能通过http发送到我们的服务了。

重试策略看需要调整,感觉退避算法合理点。这个自己选择,也就是出问题你自己背锅的意思。
4.2 交易凭证验证
准备就绪,正片上代码。
- 依赖导入
xml
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>1.19.0</version>
</dependency>
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-androidpublisher</artifactId>
<version>v3-rev20231115-2.0.0</version>
</dependency>
- 配置和配置类
properties
google-play.packageName=com.xxx.xxx
# 这个json就是刚刚创建服务账号密钥的时候下载的
google-play.serviceAccountJson=xxxxx.json
java
@Data
@Configuration
@ConfigurationProperties(prefix = "google-play")
public class GooglePlayConfig {
private String packageName;
private String serviceAccountJson;
@Bean
public GoogleCredentials googleCredentials() throws IOException {
// 懒得搞配置文件了,直接丢resources读进来
return GoogleCredentials.fromStream(new ClassPathResource(serviceAccountJson).getInputStream())
.createScoped(AndroidPublisherScopes.ANDROIDPUBLISHER);
}
@Bean
public AndroidPublisher androidPublisher(GoogleCredentials credentials) throws IOException, GeneralSecurityException {
return new AndroidPublisher.Builder(
GoogleNetHttpTransport.newTrustedTransport(),
GsonFactory.getDefaultInstance(),
new HttpCredentialsAdapter(credentials)
).setApplicationName(packageName).build();
}
}
- 验签逻辑,其实就是根据凭证查下这笔单在不在
java
@Component
@Slf4j
public class GooglePlayComponent {
@Resource
private GooglePlayConfig googlePlayConfig;
@Resource
private AndroidPublisher androidPublisher;// 注入进来咔咔用
public ProductPurchase productPurchase(String sku, String purchaseToken) {
try {
return androidPublisher
.purchases().products()
.get(googlePlayConfig.getPackageName(), sku, purchaseToken)
.execute();
} catch (IOException e) {
log.error("failed to query product purchase. {}", purchaseToken, e);
ServiceException.throwInternalServerEx("failed to query product purchase:" + purchaseToken);
return null;
}
}
}
4.3 接收回调
java
@PostMapping("/google_play_webhook")
public Object googlePlayWebhook(@RequestBody String body) {
log.info("Google play subscription webhook: {}", body);
if (StringUtils.isBlank(body)) {
log.warn("Google play subscription webhook body is EMPTY");
return ResponseEntity.status(400).body("Empty body");
}
DeveloperNotification developerNotification;
try {
developerNotification = JacksonUtils.parseJson(body, DeveloperNotification.class);
} catch (Exception e) {
log.error("failed to parse body. {}", body, e);
return ResponseEntity.status(400).body(e.getMessage());
}
// TODO 处理过程自己写去
return ResponseEntity.ok().body("OK");
}
Ref
官方集成文档:做好准备 | Google Play's billing system | Android Developers
回调的一些Bean代码:blog.csdn.net/jack2350536...
一次性购买无法收到回调问题: support.google.com/googleplay/...