更新记录
1.0.0(2026-10-19)
- 接入 Google Play Billing Library 9.1.0。
- 支持一次性商品、订阅、购买结果监听、购买恢复、确认购买与消费。
- 支持订阅基础方案与优惠、多个一次性购买选项和服务自动重连。
平台兼容性
uni-app(5.26)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| × | √ | × | × | √ | × | 6.0 | × | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | × | × | × | × | × | × | × | × |
Google Play Billing 9.1.0 支付与订阅
面向经典 uni-app Vue 3 Android App 的 UTS 插件,直接封装 Google Play Billing Library 9.1.0。
支持范围
- Android 6.0 / API 23 及以上
- HBuilderX 5.26 或以上
- 一次性商品和订阅商品查询
- 多个一次性购买选项、订阅基础方案和优惠
- Google Play 购买流程
- PENDING 交易
- 查询和恢复现有购买
- 确认非消耗型商品或订阅
- 消耗型一次性商品
obfuscatedAccountId、obfuscatedProfileId- 个性化优惠标记
- Billing 服务自动重连
当前发布版的平台声明仅覆盖经典 uni-app Vue 3 的 Android App。uni-app x Android VDOM 与蒸汽模式的独立最小工程均已通过 HBuilderX 5.26 CLI 编译,生成插件 Kotlin 源码;蒸汽模式还生成了 JS 到 UTS 插件的调用代理。尚未制作并安装这两种模式的 APK,也未在 Google Play 测试环境完成实机支付验证,因此暂不承诺完整兼容 uni-app x 或蒸汽模式,市场兼容性选项仍未勾选。iOS、HarmonyOS、Web、小程序不提供支付能力。utssdk/app-ios 仅是经典 uni-app APP-PLUS 编译占位实现。
验证范围
- 当前插件 ID
sinea17-playbilling已使用 HBuilderX 5.26.2026091411 的 uni-app x Android VDOM、蒸汽模式最小工程完成 CLI 编译与插件 Kotlin 源码生成;这一步不等于 APK 构建和实机运行。可在仓库运行scripts/verify-uniappx-compat.ps1复现。 - 已使用 DCloud 5.24 离线 SDK AAR、Billing 9.1.0 和 Android 构建工具完成 Android Library 编译验证;该结果不代表 HBuilderX 5.24 可编译本插件。
- Google Play 测试轨道中的真实购买、订阅、恢复、PENDING 状态和服务端验单尚未完成端到端验证。接入正式业务前,请使用自己的商品、测试账号和服务端完成回归。
授权与支持
插件市场提供普通授权版和源码授权版。两种授权均以购买时绑定的 AppID 和应用包名为准;普通授权版使用 DCloud 云端传统打包,源码授权版可自行评估离线集成。
接入问题和缺陷反馈请通过插件市场的用户消息联系作者。反馈时建议提供 HBuilderX 版本、Android 版本、商品类型、Billing 响应码和脱敏后的复现步骤;请勿发送购买令牌、服务账号私钥或用户个人信息。
安装
在插件市场点击“下载插件并导入 HBuilderX”,将插件导入目标项目;或将插件目录放到:
uni_modules/sinea17-playbilling
加入插件后必须重新制作并安装 Android 自定义基座或重新生成正式安装包。WGT 热更新不能向旧安装包添加 Billing 原生依赖。
插件市场页面提供“导入示例项目”。示例工程包含连接、商品查询、购买、恢复、确认和消费页面;导入后还需从市场安装本付费插件,示例 ZIP 不附带插件实现源码。展示截图和动图仅用于说明页面布局与操作顺序,不是 Google Play 真实交易结果。
| 检查项 | 要求 |
|---|---|
| 项目 | 经典 uni-app Vue 3,发布到 Android App |
| 开发工具 / 系统 | HBuilderX 5.26+;Android 6.0(API 23)+ |
| 应用身份 | manifest.json 的 Android 包名与 Play Console 应用一致 |
| 运行环境 | 设备具备可用的 Google Play 商店及服务;商品已在目标应用中启用 |
| 安装包 | 使用包含插件原生依赖的自定义基座或正式包,不能只更新 WGT |
本插件提供 Google Play 客户端能力。商品上架、测试轨道、服务端验单、权益交付与订阅状态管理仍由接入方实现。
从零配置订阅与支付测试
以下以自动续订月度订阅为例,premium_access 和 monthly 是示例 ID,使用时请换成自己创建的值。Play Console 的菜单名称可能随语言和版本变化。
第一步:准备应用和测试包
- 在 Play Console 创建应用,并按控制台要求完成 Google Play 结算所需的付款资料。确认
manifest.json的 Android 包名与 Play Console 应用包名完全一致。 - 将本插件放入
uni_modules/sinea17-playbilling,使用 HBuilderX 5.26 或以上构建包含本插件的 Android 包。插件引入了原生 Billing 依赖,调试时需制作 Android 自定义基座;标准基座和 WGT 热更新不能向旧包添加该依赖。 - 将包含插件的 AAB 发布到 Play Console 测试和发布 > 测试 > 内部测试(Test and release > Testing > Internal testing)轨道。首次发布或变更商品后,可能需要等待 Google Play 同步。
第二步:创建订阅商品
- 在目标应用打开 获利 > 商品 > 订阅(Monetize with Play > Products > Subscriptions),点击创建订阅。填写商品 ID,例如
premium_access,以及面向用户的名称和权益,创建并保存。商品 ID 创建后不能修改,客户端查询时必须完全一致。 - 进入该订阅,点击添加基础方案(Add base plan)。输入基础方案 ID,例如
monthly,选择自动续订和每月计费;按业务需要设置宽限期和账号保留期。 - 在价格和适用国家/地区中启用测试账号的 Google Play 国家/地区并设置价格。保存后点击启用(Activate)。至少一个基础方案处于启用状态,商品才可购买。
- 可选:为基础方案添加优惠(Offer),配置优惠 ID、适用人群和免费试用/优惠价格,保存并启用。建议先验证基础方案购买,再测试优惠资格。
productId 是订阅商品 ID,basePlanId 是其基础方案 ID;如有优惠,还会有 offerId。购买时使用本次 queryProducts 返回的 offerToken,不要把这些 ID 当作 offerToken 或自行生成令牌。
第三步:添加测试账号
- 在内部测试 > 测试人员创建邮箱列表,加入测试 Google 账号,选中该列表并保存。测试人员用同一账号打开加入链接接受邀请,再从测试页面进入 Google Play 安装 App。
- 另在 Play Console 设置 > 许可测试(Settings > License testing)将该账号加入许可测试列表并保存。仅加入内部测试轨道不等于已成为许可测试账号;未加入许可测试而使用真实支付方式可能产生实际扣款。
- 设备需有可用的 Google Play 商店和 Play 服务。若登录多个账号,确认购买弹窗中的账号与许可测试账号一致;通常应使用该账号从测试页面安装 App。
Google 官方允许许可测试账号侧载同包名的调试包做开发测试;正式交付前仍建议通过测试轨道安装包复核分发、签名、账号资格和购买链路。
第四步:在示例页完成支付
- 打开示例页
pages/index/index.vue,选择订阅商品(subs),填写premium_access;如要优先选择某个基础方案,填写monthly。 - 点击连接 Google Play,看到“Billing 已连接”后点击查询商品。核对商品名称、价格和基础方案;有多个方案或优惠时,点选本次要购买的方案。页面价格应来自 Google Play 返回的
formattedPrice。 - 点击拉起 Google Play 购买,在弹窗确认商品、账号和测试购买标识,选择“始终批准”等测试支付方式。
launchPurchase.success只表示购买界面成功打开,不能据此发放权益。 - 等待
purchasesUpdated:purchaseState === 1表示已购买,可将purchaseToken发给可信服务端验单,幂等交付后再确认;purchaseState === 2表示 PENDING,此时不得发货或确认。 - 重启 App,点击查询已有购买,检查购买恢复和服务端权益。示例页的确认购买按钮仅用于服务端验单并交付后的调试;订阅不得点击消费一次性商品。
建议再分别测试“始终拒绝”、用户取消、延迟批准/拒绝(PENDING)、重启恢复和订阅续订/失效。许可测试订阅会加速续订,不能据此推断正式周期;未及时确认的许可测试购买可能很快被退款。
查不到商品时
依次核对包名、productId 和 productType(订阅为 subs);基础方案是否已启用并覆盖测试账号所在的 Google Play 国家/地区;测试 AAB 是否包含插件并已发布;账号是否接受测试邀请、加入许可测试、从正确的 Google Play 账号安装;最后检查 unfetchedProducts 和 Billing 响应码。商品和轨道配置也可能仍在同步。不要把 purchaseToken、服务账号密钥或用户个人信息发到公开渠道。
参考:Google 订阅配置、内部测试、许可测试、Billing 支付测试。
接入前先认清几个 ID
| 名称 | 从哪里取得 | 用在哪里 |
|---|---|---|
productId |
Play Console 的订阅或一次性商品页面 | queryProducts.productIds、launchPurchase.productId |
basePlanId |
某个订阅下的基础方案 | 在查询结果的 subscriptionOffers 中选定方案 |
offerId |
某个基础方案下的优惠,可为空 | 选择目标优惠;空值通常是基础方案本身 |
offerToken |
本次 queryProducts 返回的选项 |
订阅购买必传;一次性商品有多个购买选项时传入所选项 |
purchaseToken |
购买回调或 queryPurchases 返回 |
发给自己的服务端验单,之后用于确认或消费 |
productId、basePlanId、offerId 不能代替 offerToken。不要自行拼接令牌,也不要把价格写死;展示价格应取当前选项的 formattedPrice。
最短接入流程(订阅示例)
下面的代码展示调用顺序。先完成上面的 Play Console 配置,再把示例 ID 和 verifyAndDeliver 换成自己的业务实现。verifyAndDeliver 必须调用自己的可信服务端,由服务端向 Google 核验购买并幂等发放权益;示例函数只是占位,不能直接复制上线。
// 仅在经典 uni-app 的 Android App 中引用和执行
// #ifdef APP-PLUS
import {
initializeBilling, onPurchasesUpdated, offPurchasesUpdated,
queryProducts, launchPurchase, queryPurchases,
acknowledgePurchase, isBillingReady
} from '@/uni_modules/sinea17-playbilling';
const productId = 'premium_access';
const basePlanId = 'monthly';
let selectedOffer = null;
function verifyAndDeliver(purchase) {
// TODO: 将 purchase.purchaseToken、商品 ID 交给自己的服务端。
// 服务端验单、幂等交付成功后,才调用 acknowledgePurchase。
}
function handlePurchase(purchase) {
if (purchase.purchaseState === 2) {
// PENDING:等待付款完成,不能交付或确认。
return;
}
if (purchase.purchaseState !== 1) return;
verifyAndDeliver(purchase);
}
export function connectBilling() {
onPurchasesUpdated((event) => {
if (event.billingResult.responseCode !== 0) return;
event.purchases.forEach(handlePurchase);
});
initializeBilling({
success() {
queryProducts({
productIds: [productId], productType: 'subs',
success(result) {
const product = result.products.find((item) => item.productId === productId);
selectedOffer = product?.subscriptionOffers.find(
(item) => item.basePlanId === basePlanId && !item.offerId
) ?? null;
if (!selectedOffer) {
console.error('未找到可售基础方案', result.unfetchedProducts);
return;
}
console.log('当前价格', selectedOffer.pricingPhases);
},
fail(error) { console.error('查询商品失败', error.errCode, error.billingResult); }
});
},
fail(error) { console.error('连接失败', error.errCode, error.billingResult); }
});
}
export function buySubscription() {
if (!isBillingReady() || !selectedOffer) return;
launchPurchase({
productId, productType: 'subs', offerToken: selectedOffer.offerToken,
success() { console.log('支付界面已打开,等待购买回调'); },
fail(error) { console.error('拉起购买失败', error.errCode, error.billingResult); }
});
}
export function restoreSubscription() {
if (!isBillingReady()) return;
queryPurchases({
productType: 'subs',
success(result) { result.purchases.forEach(handlePurchase); }
});
}
export function leaveBillingPage() {
offPurchasesUpdated();
}
// #endif
进入支付页时调用 connectBilling();用户点击购买时调用 buySubscription();启动 App 或返回前台、登录账号变化后调用 restoreSubscription(),并让服务端重新确认权益。离开支付页时调用 leaveBillingPage()。当前版本仅保存一个全局购买监听器,重复注册会覆盖旧监听器。launchPurchase.success 只代表支付界面成功打开,不能作为发货依据。
服务端验单和幂等交付完成后,针对未确认的订阅或非消耗型一次性商品调用 acknowledgePurchase({ purchaseToken, ... })。消耗型商品改用 consumePurchase({ purchaseToken, ... });订阅不能消费。更完整的按钮式操作可直接打开仓库示例页 pages/index/index.vue。
接口速查
所有异步方法都使用 success、fail、complete 回调;complete 无论成功失败都会触发。fail 包含 errCode、errMsg,Google Play 拒绝请求时还包含 billingResult。除 onPurchasesUpdated 外,各方法的详细类型以 utssdk/interface.uts 为准。
| 方法 | 必填参数 | 成功返回 / 用途 |
|---|---|---|
initializeBilling(options) |
无 | BillingResultInfo;先连接 Billing 服务,可重复调用 |
queryProducts(options) |
productIds: string[]、productType: 'subs' \| 'inapp' |
products、unfetchedProducts 和 billingResult;读取商品、方案、价格 |
launchPurchase(options) |
productId、productType;订阅还需 offerToken |
BillingResultInfo;仅表示购买流程已拉起 |
queryPurchases(options) |
productType |
purchases 和 billingResult;查询当前账号已有购买 |
acknowledgePurchase(options) |
purchaseToken |
BillingResultInfo;确认订阅/非消耗型商品 |
consumePurchase(options) |
purchaseToken |
purchaseToken 和 billingResult;消费可重复购买的一次性商品 |
onPurchasesUpdated(callback) |
回调函数 | 接收支付结果;需及时注册,只保留最后一个监听器 |
offPurchasesUpdated() |
无 | 清除购买监听器 |
isBillingReady() |
无 | boolean;检查当前连接状态 |
endBillingConnection() |
无 | 主动结束连接并清理监听器;下次使用需重新初始化 |
queryPurchases.includeSuspendedSubscriptions 可选,默认为 false。launchPurchase 还支持 obfuscatedAccountId、obfuscatedProfileId(最长 64 字符)和 isOfferPersonalized。一次性商品有多个购买选项时,也应传入所选项的 offerToken。
关键返回字段
| 字段 | 含义 / 正确处理 |
|---|---|
billingResult.responseCode |
Google Play Billing 响应码;0 表示该次 SDK 操作成功,不等于已付款 |
billingResult.debugMessage |
Google Play 返回的调试信息,排查失败时一并记录 |
products[].subscriptionOffers[] |
basePlanId、offerId、offerToken、pricingPhases;从这里选择订阅方案 |
products[].oneTimePurchaseOffers[] |
purchaseOptionId、offerToken、formattedPrice 等一次性商品选项 |
unfetchedProducts[] |
未查询到的商品及 statusCode;查不到商品时优先检查 |
purchases[].purchaseState |
1 已购买,2 待处理;只有已购买且服务端验单通过才能交付 |
purchases[].purchaseToken |
验单和幂等处理的关键标识,应提交服务端;不要输出完整令牌到日志 |
purchases[].acknowledged |
是否已经确认,避免重复确认;恢复购买仍需同步服务端权益 |
插件错误码
errCode |
含义 | 排查方向 |
|---|---|---|
9012001 |
原生依赖未集成 | 重新制作含插件的基座或安装包 |
9012002 |
BillingClient 未连接 | 先完成 initializeBilling |
9012003 |
没有可用的 Android Activity | 在前台页面发起购买 |
9012004 |
商品类型不是 inapp 或 subs |
检查 productType |
9012005 |
参数无效 | 检查商品 ID、订阅 offerToken、购买令牌 |
9012006 |
商品未查询到或类型不匹配 | 检查商品配置和当前账号资格 |
9012100 |
Google Play Billing 返回失败 | 查看 billingResult.responseCode、debugMessage |
9012099 |
插件内部错误或连接被结束 | 查看 errMsg,重新初始化后重试 |
分步骤调用示例
引用
// #ifdef APP-PLUS
import {
acknowledgePurchase,
consumePurchase,
endBillingConnection,
initializeBilling,
isBillingReady,
launchPurchase,
offPurchasesUpdated,
onPurchasesUpdated,
queryProducts,
queryPurchases
} from '@/uni_modules/sinea17-playbilling';
// #endif
插件使用 uni-app 风格的 success、fail 和 complete 回调。
推荐调用流程
initializeBilling
→ onPurchasesUpdated
→ queryProducts
→ 根据 basePlanId / offerId / offerTags 选择 offer
→ launchPurchase
→ 收到 purchasesUpdated
→ 服务端使用 purchaseToken 验单并幂等发货
→ acknowledgePurchase 或 consumePurchase
launchPurchase.success 只表示 Google Play 购买界面成功拉起。购买是否完成必须以 onPurchasesUpdated 返回的状态和业务服务端验单结果为准。
初始化
initializeBilling({
success(result) {
console.log('Billing ready', result.responseCode);
},
fail(error) {
console.error(error.errCode, error.billingResult?.responseCode);
}
});
Billing 9.1.0 自动启用一次性商品、预付订阅的 PENDING 支持和服务自动重连。
监听购买结果
onPurchasesUpdated((event) => {
if (event.billingResult.responseCode !== 0) return;
for (const purchase of event.purchases) {
if (purchase.purchaseState === 1) {
// 将 purchase.purchaseToken 发送到可信服务端验单。
} else if (purchase.purchaseState === 2) {
// PENDING:不得发货,稍后使用 queryPurchases 恢复状态。
}
}
});
当前版本维护一个全局购买监听器。再次调用 onPurchasesUpdated 会替换之前的监听器;不再需要时调用 offPurchasesUpdated()。
查询商品
queryProducts({
productIds: ['your_subscription_product_id'],
productType: 'subs',
success(result) {
const product = result.products[0];
const offers = product?.subscriptionOffers ?? [];
const selected = offers.find((item) => item.basePlanId === 'your_base_plan_id');
console.log(selected?.pricingPhases);
console.log(result.unfetchedProducts);
}
});
一次性商品使用 productType: 'inapp',查询结果位于 oneTimePurchaseOffers。界面价格必须使用 Google Play 返回的 formattedPrice,不要硬编码货币和金额。
发起购买
订阅商品必须传入查询结果中的有效 offerToken:
launchPurchase({
productId: 'your_subscription_product_id',
productType: 'subs',
offerToken: selectedOffer.offerToken,
obfuscatedAccountId: 'server-generated-stable-hash',
obfuscatedProfileId: 'optional-profile-hash',
isOfferPersonalized: false,
success(result) {
console.log('Billing UI launched', result.responseCode);
},
fail(error) {
console.error(error.errCode, error.billingResult?.responseCode);
}
});
obfuscatedAccountId 和 obfuscatedProfileId 最长 64 字符。不要传邮箱、手机号、姓名或可直接识别用户的内部 ID。
一次性商品使用 productType: 'inapp';当商品包含多个一次性购买选项时,应把目标购买选项的 offerToken 传入。
插件会在拉起购买前重新查询目标 ProductDetails,避免复用已经过期的 Android 原生对象。
查询和恢复购买
queryPurchases({
productType: 'subs',
includeSuspendedSubscriptions: true,
success(result) {
for (const purchase of result.purchases) {
// 每个 purchaseToken 都必须重新提交服务端验证。
}
}
});
本地查询结果不是服务端验单结果,不能据此直接发放永久权益。
确认购买
服务端验单并完成幂等发货后,再确认非消耗型商品或订阅:
acknowledgePurchase({
purchaseToken,
success(result) {
console.log(result.responseCode);
}
});
消耗购买
仅用于允许重复购买的消耗型一次性商品:
consumePurchase({
purchaseToken,
success(result) {
console.log(result.billingResult.responseCode);
}
});
订阅不得调用 consumePurchase。
关闭连接
if (isBillingReady()) {
endBillingConnection();
}
安全要求
- 不要在日志或持久化存储中保存 purchaseToken。
- 不要在前端放置 Google Play Developer API 服务账号、私钥或访问令牌。
- 客户端购买结果必须提交业务服务端验证。
- PENDING 交易不得发货。
- 服务端应以 purchaseToken 建立唯一约束,避免重复发货。
- RTDN 只是状态变化通知,收到通知后仍需查询 Google Play Developer API。
离线打包
普通加密授权不能用于 Android 离线 SDK。源码版可配合仓库中的 scripts/sync-offline-android.ps1 和 docs/offline-android.md 集成,但仍需匹配具体 DCloud 离线 SDK、Gradle、Kotlin 和 Android SDK 版本。
当前未包含
- 订阅升级、降级及 replacementMode
- Google Play 应用内消息
- External Offer
- Alternative Billing Only
- User Choice Billing
这些能力没有包含在 1.0.0 的兼容承诺中。

收藏人数:
https://gitee.com/sinea17/google-play-billing-uts
购买源码授权版(
试用
使用 HBuilderX 导入示例项目
赞赏(0)
下载 0
赞赏 0
下载 12694417
赞赏 1956
赞赏
京公网安备:11010802035340号