如何获取 Google Billing 已购商品及其描述
结论
onPurchasesUpdated() 返回的 purchases 中已经包含实际购买的商品 ID。题目使用的是旧版 Billing API,可以调用 Purchase#getSku() 获取 SKU,再从之前查询到的 SkuDetails 列表中找到对应商品。
当前代码中的成员变量 skuDetails 没有赋值。直接调用下面的方法会触发 NullPointerException:
skuDetails.getSku();
skuDetails.getDescription();
查询商品信息时,需要保存返回的 SkuDetails,并建立一个以 SKU 为键的映射。
商品描述不适合用作积分数值,因为描述可能被修改、翻译,或者包含非数字字符。更可靠的方式是根据固定的商品 ID 确定要增加的积分。
保存查询到的商品信息
在 Activity 中添加一个映射:
private final Map<String, SkuDetails> skuDetailsBySku = new HashMap<>();
查询成功后,按 SKU 保存每个 SkuDetails:
@Override
public void onSkuDetailsResponse(
int responseCode,
List<SkuDetails> skuDetailsList
) {
if (responseCode == BillingClient.BillingResponse.OK
&& skuDetailsList != null) {
skuDetailsBySku.clear();
for (SkuDetails details : skuDetailsList) {
skuDetailsBySku.put(details.getSku(), details);
}
loadProductToRecyclerView(skuDetailsList);
} else {
Toast.makeText(
ponerDineroActivity.this,
"No logro cargarse la lista :(",
Toast.LENGTH_SHORT
).show();
}
}
购买完成后,就可以通过 Purchase#getSku() 找到对应的 SkuDetails。
在购买回调中识别商品
旧版 API 可以这样处理:
@Override
public void onPurchasesUpdated(
int responseCode,
@Nullable List<Purchase> purchases
) {
if (responseCode != BillingClient.BillingResponse.OK) {
Toast.makeText(this, "Hubo un Error", Toast.LENGTH_SHORT).show();
return;
}
if (purchases == null || purchases.isEmpty()) {
return;
}
for (Purchase purchase : purchases) {
String purchasedSku = purchase.getSku();
SkuDetails purchasedDetails = skuDetailsBySku.get(purchasedSku);
if (purchasedDetails == null) {
// 商品详情可能尚未查询,或者应用进程已被重新创建。
queryDetailsAndProcessPurchase(purchase);
continue;
}
String description = purchasedDetails.getDescription();
Toast.makeText(
this,
"SKU: " + purchasedSku + ", description: " + description,
Toast.LENGTH_SHORT
).show();
processPurchasedProduct(purchase, purchasedDetails);
}
}
两者的关联关系如下:
Purchase.getSku()
↓
Map<String, SkuDetails>
↓
SkuDetails.getDescription()
不要根据用户最后点击的 RecyclerView 位置判断购买结果。支付期间,Activity 可能被重建,用户也可能取消、重试或恢复尚未处理的购买。购买结果应以 Google Billing 返回的 Purchase 为准。
推荐:根据 SKU 计算积分
即使能够获取 getDescription(),也不应把它直接传给 Integer.parseInt()。例如,Google Play 后台中的描述可能是:
Recibe 100 pesos
这段文本无法直接转换为整数。更安全的做法是明确维护 SKU 与积分的对应关系:
private int getPointsForSku(String sku) {
switch (sku) {
case "carga_100_pesos":
return 100;
case "carga_200_pesos":
return 200;
case "carga_300_pesos":
return 300;
default:
throw new IllegalArgumentException("Unknown SKU: " + sku);
}
}
购买处理代码如下:
private void processPurchasedProduct(
Purchase purchase,
SkuDetails details
) {
String sku = purchase.getSku();
final int purchasedPoints;
try {
purchasedPoints = getPointsForSku(sku);
} catch (IllegalArgumentException exception) {
Toast.makeText(this, "Producto desconocido: " + sku, Toast.LENGTH_SHORT).show();
return;
}
int currentPoints;
try {
currentPoints = Integer.parseInt(currentusermoney);
} catch (NumberFormatException exception) {
Toast.makeText(this, "Saldo actual no válido", Toast.LENGTH_SHORT).show();
return;
}
int total = currentPoints + purchasedPoints;
currentusermoney = String.valueOf(total);
dineroactual.setText("Usted Tiene " + currentusermoney + " ARS");
Toast.makeText(
this,
details.getDescription() + " - Total: " + total,
Toast.LENGTH_SHORT
).show();
// 验证并发放积分后,再确认或消费这笔购买。
}
商品描述可以显示在界面上,但积分规则应由 SKU 或服务端商品配置决定。
商品详情不在缓存时重新查询
SkuDetails 是商品目录信息,不是购买凭证。应用重启后,内存中的 skuDetailsBySku 会丢失,所以购买回调不能假定商品详情始终存在。
此时可以使用购买结果中的 SKU 重新查询:
private void queryDetailsAndProcessPurchase(Purchase purchase) {
String sku = purchase.getSku();
SkuDetailsParams params = SkuDetailsParams.newBuilder()
.setSkusList(Collections.singletonList(sku))
.setType(BillingClient.SkuType.INAPP)
.build();
billingClient.querySkuDetailsAsync(
params,
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(
int responseCode,
List<SkuDetails> skuDetailsList
) {
if (responseCode != BillingClient.BillingResponse.OK
|| skuDetailsList == null
|| skuDetailsList.isEmpty()) {
Toast.makeText(
ponerDineroActivity.this,
"No se encontraron los detalles del producto",
Toast.LENGTH_SHORT
).show();
return;
}
SkuDetails details = skuDetailsList.get(0);
skuDetailsBySku.put(details.getSku(), details);
processPurchasedProduct(purchase, details);
}
}
);
}
必须防止重复发放
积分是可以重复购买和消耗的权益。如果每次收到购买回调都直接累加,同一笔交易可能在应用重启、网络重试或恢复购买时被重复发放。
较稳妥的处理顺序是:
- 从
Purchase中获取 SKU、购买令牌等购买数据。 - 最好把购买令牌发送到可信服务端,并通过 Google Play 验证。
- 将购买令牌作为唯一键,检查该订单是否已经发放过积分。
- 以原子方式记录订单并增加用户积分。
- 积分成功入账后消费该购买。如果商品不是消耗品,则应确认购买。
- 消费成功后,用户才能再次购买同一个消耗型商品。
不要只在客户端或通过 Firebase 客户端直接增加余额。本地 SKU 映射和客户端数据都可能被篡改。至少需要保证数据库权限、购买验证,以及基于购买令牌的幂等处理。
关于新版 Google Play Billing
题目中的代码使用旧版 SkuDetails API。较新的 Billing Library 已经改用 ProductDetails,购买对象也可能通过商品 ID 列表返回商品标识,而不是使用单个 getSku()。具体方法名取决于项目采用的 Billing Library 版本,但处理逻辑相同:
从 Purchase 取得 product ID
→ 用 product ID 查找 ProductDetails
→ 验证购买并防止重复发放
→ 按 product ID 决定积分数量
→ 确认或消费购买
对于当前代码,直接的修复方式是使用 purchase.getSku() 查询缓存的 Map<String, SkuDetails>。在生产环境中,应将 SKU 作为稳定的业务标识,不要把可以编辑和本地化的商品描述当作积分值。
备注:内容仅供参考。
