更新记录

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 的菜单名称可能随语言和版本变化。

第一步:准备应用和测试包

  1. 在 Play Console 创建应用,并按控制台要求完成 Google Play 结算所需的付款资料。确认 manifest.json 的 Android 包名与 Play Console 应用包名完全一致。
  2. 将本插件放入 uni_modules/sinea17-playbilling,使用 HBuilderX 5.26 或以上构建包含本插件的 Android 包。插件引入了原生 Billing 依赖,调试时需制作 Android 自定义基座;标准基座和 WGT 热更新不能向旧包添加该依赖。
  3. 将包含插件的 AAB 发布到 Play Console 测试和发布 > 测试 > 内部测试(Test and release > Testing > Internal testing)轨道。首次发布或变更商品后,可能需要等待 Google Play 同步。

第二步:创建订阅商品

  1. 在目标应用打开 获利 > 商品 > 订阅(Monetize with Play > Products > Subscriptions),点击创建订阅。填写商品 ID,例如 premium_access,以及面向用户的名称和权益,创建并保存。商品 ID 创建后不能修改,客户端查询时必须完全一致。
  2. 进入该订阅,点击添加基础方案(Add base plan)。输入基础方案 ID,例如 monthly,选择自动续订和每月计费;按业务需要设置宽限期和账号保留期。
  3. 在价格和适用国家/地区中启用测试账号的 Google Play 国家/地区并设置价格。保存后点击启用(Activate)。至少一个基础方案处于启用状态,商品才可购买。
  4. 可选:为基础方案添加优惠(Offer),配置优惠 ID、适用人群和免费试用/优惠价格,保存并启用。建议先验证基础方案购买,再测试优惠资格。

productId 是订阅商品 ID,basePlanId 是其基础方案 ID;如有优惠,还会有 offerId。购买时使用本次 queryProducts 返回的 offerToken,不要把这些 ID 当作 offerToken 或自行生成令牌。

第三步:添加测试账号

  1. 在内部测试 > 测试人员创建邮箱列表,加入测试 Google 账号,选中该列表并保存。测试人员用同一账号打开加入链接接受邀请,再从测试页面进入 Google Play 安装 App。
  2. 另在 Play Console 设置 > 许可测试(Settings > License testing)将该账号加入许可测试列表并保存。仅加入内部测试轨道不等于已成为许可测试账号;未加入许可测试而使用真实支付方式可能产生实际扣款。
  3. 设备需有可用的 Google Play 商店和 Play 服务。若登录多个账号,确认购买弹窗中的账号与许可测试账号一致;通常应使用该账号从测试页面安装 App。

Google 官方允许许可测试账号侧载同包名的调试包做开发测试;正式交付前仍建议通过测试轨道安装包复核分发、签名、账号资格和购买链路。

第四步:在示例页完成支付

  1. 打开示例页 pages/index/index.vue,选择订阅商品(subs),填写 premium_access;如要优先选择某个基础方案,填写 monthly。
  2. 点击连接 Google Play,看到“Billing 已连接”后点击查询商品。核对商品名称、价格和基础方案;有多个方案或优惠时,点选本次要购买的方案。页面价格应来自 Google Play 返回的 formattedPrice。
  3. 点击拉起 Google Play 购买,在弹窗确认商品、账号和测试购买标识,选择“始终批准”等测试支付方式。launchPurchase.success 只表示购买界面成功打开,不能据此发放权益。
  4. 等待 purchasesUpdated:purchaseState === 1 表示已购买,可将 purchaseToken 发给可信服务端验单,幂等交付后再确认;purchaseState === 2 表示 PENDING,此时不得发货或确认。
  5. 重启 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 的兼容承诺中。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

com.android.vending.BILLING:连接 Google Play 结算服务,查询商品和购买记录、发起购买及处理购买结果。

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件本身不向作者服务器采集或上传数据。插件调用 Google Play Billing SDK,与 Google Play 服务交互商品信息、购买状态和购买令牌,并将结果返回宿主应用;宿主应用负责隐私披露、服务端验单和权益管理。

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无

暂无用户评论。