前言
由于需要对接海外版本,所以需要对接谷歌内购, 注意: Google Pay 和 Google 应用内购不是一回事, 内购不需要支付网关,而前者需要支付网关
Google Play 应用内购(In-App Purchase)通过 Google Play Billing 系统处理,不需要额外的支付网关(如 Stripe、PayPal)。原因如下:
- Google Play Billing 的作用:
- Google Play Billing 是 Google 提供的内置支付系统,直接处理用户通过 Google Play 购买应用内商品(一次性产品或订阅)的支付流程。
- 用户使用 Google Play 账户绑定的支付方式(如信用卡、Google Pay、礼品卡等)完成购买,Google 负责扣款、退款等资金流处理。
- 后端通过 Google Play Developer API(如 https://androidpublisher.googleapis.com/androidpublisher/v3/applications/%s/purchases/products/%s/tokens/%s)验证购买 token(purchaseToken),无需额外网关。
流程图
开发
后端需要对接四个接口
1. 调用获取accessToken的方法,google也是采用了oauth2协议来做的
2. 调用get获取token关联的订单支付信息结果进行判断
3. 调用 acknowledge 方法进行确认订单
4. 调用consum 方法进行消耗,否则无法发起二次购买
注意:3-4步都只能调用1次,第二次会报错
所需依赖
<dependency>
<groupId>com.google.api-client</groupId>
<artifactId>google-api-client</artifactId>
<version>2.7.0</version>
</dependency><dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>1.27.0</version> <!-- 检查最新版本 -->
</dependency><dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-androidpublisher</artifactId>
<version>v3-rev142-1.18.0-rc</version>
</dependency>
代码如下,已加注释
@Service
@Slf4j
public class GooglePayService {
//获取accessToken
public final static String GOOGLE_ACCESS_TOKEN = "https://www.googleapis.com/auth/androidpublisher";
//获取支付结果情况
public final static String GOOGLE_VERIFY = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/%s/purchases/products/%s/tokens/%s";
public final static String GOOGLE_CONSUME = "https://androidpublisher.googleapis.com/androidpublisher/v3/applications/%s/purchases/products/%s/tokens/%s:consume";
public String getAccessToken() throws IOException {
GoogleCredential credential = GoogleCredential
.fromStream(getCredential())
.createScoped(Collections.singleton(GOOGLE_ACCESS_TOKEN));
credential.refreshToken();
return credential.getAccessToken();
}
public InputStream getCredential() throws IOException{
ClassPathResource classPathResource = new ClassPathResource("/config/google-pay/credentials.json");
return classPathResource.getInputStream();
}
public static void main(String[] args) throws Exception{
System.out.println(new GooglePayService().getAccessToken());
}
public ResponseEntity<?> verifyPurchase(GooglePayRequestDTO request) throws IOException {
String accessToken = getAccessToken();
String packageName = request.getPackageName();
String productId = request.getProductId();
String purchaseToken = request.getPurchaseToken();
HttpTransport transport = new NetHttpTransport();
HttpRequestFactory requestFactory = transport.createRequestFactory();
GenericUrl url = new GenericUrl(
String.format("https://androidpublisher.googleapis.com/androidpublisher/v3/applications/%s/purchases/products/%s/tokens/%s",
packageName, productId, purchaseToken)
);
HttpRequest apiRequest = requestFactory.buildGetRequest(url);
apiRequest.getHeaders().setAuthorization("Bearer " + accessToken);
String response = apiRequest.execute().parseAsString();
// 解析响应,检查订单状态
JSONObject jsonResponse = JSONObject.parseObject(response);
if ("PURCHASED".equals(jsonResponse.getString("purchaseState"))) {
// 订单有效,处理发货逻辑
// orderRepository.updateOrderStatus(request.getOrderId(), "COMPLETED");
return ResponseEntity.ok("Purchase verified and processed");
} else {
return ResponseEntity.badRequest().body("Invalid purchase");
}
}
public void consumePurchase(String packageName, String productId, String purchaseToken, String accessToken) throws IOException {
HttpTransport transport = new NetHttpTransport();
HttpRequestFactory requestFactory = transport.createRequestFactory();
GenericUrl url = new GenericUrl(
String.format("https://androidpublisher.googleapis.com/androidpublisher/v3/applications/%s/purchases/products/%s/tokens/%s:consume",
packageName, productId, purchaseToken)
);
HttpRequest apiRequest = requestFactory.buildPostRequest(url, null);
apiRequest.getHeaders().setAuthorization("Bearer " + accessToken);
apiRequest.execute();
}
public void acknowledgePurchase(String packageName, String productId, String purchaseToken, String accessToken) throws IOException {
HttpTransport transport = new NetHttpTransport();
HttpRequestFactory requestFactory = transport.createRequestFactory();
GenericUrl url = new GenericUrl(
String.format("https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}:acknowledge",
packageName, productId, purchaseToken)
);
HttpRequest apiRequest = requestFactory.buildPostRequest(url, null);
apiRequest.getHeaders().setAuthorization("Bearer " + accessToken);
apiRequest.execute();
}
}
其实并不复杂, 只需要根据调用处理即可,还有就是为了保证一定消耗需要做其他的业务逻辑处理,一般就是加字段以及定时器处理了
以及安全问题可以多从不同字段去判断或者自己再加一些数据进行维护,以上几个接口及返回值含义可以参考官网的文档,如下
REST Resource: purchases.products | Google Play Developer API | Google for Developers
存在的问题
在获取accessToken的时候用的GoogleCredential 这个虽然也可以拿到,但是已经过期了可能不太安全,都是推荐用新版的,如下
public String getAccessToken() throws IOException {
GoogleCredentials credential = GoogleCredentials
.fromStream(getCredential())
.createScoped(Collections.singleton(GOOGLE_ACCESS_TOKEN));
AccessToken accessToken = credential.getAccessToken();
if(accessToken == null) {
credential.refresh(); // 刷新,这里才是真正调用
accessToken = credential.getAccessToken();
}
//还是为空就报错吧
if(accessToken == null) {
throw new AppException(ErrorCode.SYS_OPERATOR_ERROR.code(), "accessToken error");
} else {
System.out.println(accessToken.getExpirationTime()); //过期时间
}
return accessToken.getTokenValue();
}
问题-1
但是一直报错
Exception in thread "main" java.lang.NoClassDefFoundError: com/google/auth/Retryable
就是找不到 com.google.auth.Retryable 类,
但是查了好多资料都说引入 google-auth-library-oauth2-http 和 google-api-services-androidpublisher这个即可,明明都引入了,但是还是不行,直接查Retryable也差不多这个定义
解决如下
后来还是先引入了
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
<version>26.1.0</version> <!-- 检查最新版本 -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement><dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
</dependency>
这样就正常了,这时候再去查Retryable这个类,发现是位于 google-auth-library-credentials 这个包下的,但是网上没提到过,不知道是不是新的改版了,所以只需要再单独引入这个包就行了,如下
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-credentials</artifactId>
<version>1.8.1</version> <!-- 检查最新版本 -->
</dependency>
害,这个问题居然折腾了挺久的,好在是最终也是解决了, 还有就是可以获取到token的过期时间,那么就缓存一下到redis中,提前20分钟进行重新刷新token即可
问题-2
调用谷歌获取/消耗时需要对应的账号开权限,可以参考以下文章,这里就不细说了
问题-3
在调用消费时可能会发生 The product purchase is not owned by the user. 错误,可以参考文章