PAYATHON 2026

Google Apps Marketplace 商业化计费实施步骤

支付老李

结论

Google Apps Marketplace 不会替应用收款,也不会自动管理商业许可证。一般需要自行搭建“支付、订单、授权”闭环,或者使用 Stripe、PayPal 等第三方平台处理支付,再由自己的后端判断客户是否有权使用产品。

人工收款也是可行的。款项到账后,管理员可以在后台为客户创建许可证,或生成一次性激活码供客户启用产品。不过,令牌只能用于控制产品功能或激活许可证,不能用来绕过 Google 的安装和 OAuth 授权流程。

开发时间取决于需要做到什么程度:

  • 简单的人工授权通常需要数天到两周。
  • 接入第三方支付并自动开通,一般需要两到六周。
  • 如果还要支持订阅续费、税务、退款、宽限期、审计和客户自助管理,通常需要一个月以上。

这些时间只是工程估算,并非 Google 的官方承诺。实际周期还要考虑现有后端、支付服务商、合规范围和测试要求。

Gmail Contextual Gadget 是较早期的产品形态。如果这是一个现有旧项目,应先确认 Google Workspace Marketplace 目前是否仍接受该类型的新应用或更新,以及项目应迁移到哪种扩展机制。下面的计费架构仍然适用。

基本计费流程

一套最小可用的商业化方案通常包括以下环节。

1. 确定计费主体

先明确许可证卖给谁:

  • 单个 Google 用户
  • 一个 Google Workspace 域
  • 一个组织内的指定席位
  • 某个套餐包含的固定用户数

企业应用通常按 Workspace 客户或域授权,而不是只认邮箱字符串。数据库最好同时保存稳定的 Google 用户或客户标识,域名只作为辅助信息。这样可以避免用户改名、使用邮箱别名或变更域名后出现误判。

2. 选择收款方式

常见选择包括:

  • 人工处理银行转账、发票或采购订单
  • 使用 Stripe、PayPal 等支付服务
  • 使用订阅管理或 Merchant of Record 服务处理续费、税务和发票

Google Marketplace listing 主要承担应用发现、安装和授权。购买页面可以跳转到你的站点,由你的系统完成付款和账户开通。

3. 建立许可证数据

后端至少需要记录:

customer_id
google_customer_id
primary_domain
plan
seat_limit
status
starts_at
expires_at
created_at
updated_at

status 可以设计为:

pending
active
past_due
suspended
canceled
expired

不要只保存一个 paid = true。订阅到期、退款、付款失败和人工暂停都需要各自对应的明确状态。

4. 将付款结果转换为授权

自动计费的常见流程如下:

客户下单
  -> 支付服务商完成付款
  -> 支付服务商调用 webhook
  -> 后端验证 webhook 签名
  -> 创建或更新许可证
  -> 用户安装 Marketplace 应用并完成 OAuth
  -> 应用按用户或 Workspace 客户标识查询许可证
  -> 允许或拒绝付费功能

许可证必须由服务端检查。不要把“已付款”状态只保存在浏览器、本地存储或 Gadget 参数中,这些内容很容易被修改。

5. 处理续费与异常状态

订阅产品还要处理:

  • 首次付款成功
  • 自动续费成功
  • 付款失败
  • 取消订阅
  • 退款或拒付
  • 套餐升级与降级
  • 席位数量变化
  • 试用期和宽限期
  • 应用卸载后的数据与许可证处理

支付 webhook 可能重复发送,也可能不按顺序到达,因此订单处理必须具备幂等性。

人工付款和手动授权

人工处理完全可行,尤其适合客户较少、合同金额较高,或者主要通过企业采购成交的产品。

一种简单的流程是:

  1. 客户提交购买申请,并提供 Workspace 域或管理员账户信息。
  2. 你发送报价单或发票。
  3. 确认款项到账后,在内部管理后台创建许可证。
  4. 向客户发送 Marketplace 安装链接。
  5. 客户按照正常流程安装应用,并授予必要权限。
  6. 应用后端识别客户身份并检查许可证。
  7. 许可证有效时启用产品功能。

这里的“手动启用”不一定需要 token。如果能够可靠获取 Workspace 客户标识,直接为该客户创建授权通常更简单。

如果客户先付款、后安装,可以使用一次性激活码,将付款记录与之后登录的 Google 身份关联起来。例如:

activation_code
order_id
expected_domain
expires_at
redeemed_at
redeemed_by

激活接口可以写成:

async function redeemActivationCode({ code, googleUser }) {
  const activation = await db.activationCodes.findByHash(hash(code));

  if (!activation) {
    throw new Error("INVALID_ACTIVATION_CODE");
  }

  if (activation.redeemedAt) {
    throw new Error("ACTIVATION_CODE_ALREADY_USED");
  }

  if (activation.expiresAt <= new Date()) {
    throw new Error("ACTIVATION_CODE_EXPIRED");
  }

  if (
    activation.expectedDomain &&
    activation.expectedDomain !== googleUser.hostedDomain
  ) {
    throw new Error("DOMAIN_MISMATCH");
  }

  return db.transaction(async (tx) => {
    await tx.licenses.create({
      customerId: googleUser.customerId,
      plan: activation.plan,
      status: "active",
      startsAt: new Date(),
      expiresAt: activation.licenseExpiresAt
    });

    await tx.activationCodes.markRedeemed({
      id: activation.id,
      redeemedAt: new Date(),
      redeemedBy: googleUser.subject
    });
  });
}

这只是结构示例。customerId、subject 和域信息必须来自经过验证的 Google 身份凭据,不能直接相信浏览器提交的字段。

许可证检查示例

每次访问重要的付费功能时,应用都应在服务端判断用户是否有权使用:

async function requireActiveLicense(req, res, next) {
  const identity = await verifyGoogleIdentity(req);

  const license = await db.licenses.findActive({
    googleCustomerId: identity.customerId,
    userSubject: identity.subject
  });

  if (!license) {
    return res.status(403).json({
      error: "LICENSE_REQUIRED",
      purchaseUrl: "https://example.com/pricing"
    });
  }

  if (license.expiresAt && license.expiresAt <= new Date()) {
    return res.status(403).json({
      error: "LICENSE_EXPIRED",
      renewalUrl: "https://example.com/billing"
    });
  }

  req.identity = identity;
  req.license = license;
  next();
}

实际实现可以使用缓存,但不能无限期缓存授权结果。否则,客户在退款、许可证过期或被人工停用后,仍可能继续使用产品。

支付 webhook 示例

使用第三方支付平台时,不能因为客户返回了支付成功页面就开通许可证。开通操作应以经过签名验证的服务端 webhook 为准:

app.post(
  "/billing/webhook",
  rawBodyMiddleware,
  async (req, res) => {
    let event;

    try {
      event = verifyWebhookSignature(
        req.body,
        req.headers["payment-signature"]
      );
    } catch {
      return res.status(400).send("Invalid signature");
    }

    if (await db.webhookEvents.exists(event.id)) {
      return res.sendStatus(200);
    }

    await db.transaction(async (tx) => {
      await tx.webhookEvents.insert({
        eventId: event.id,
        eventType: event.type
      });

      switch (event.type) {
        case "payment.succeeded":
          await activateLicense(tx, event.data);
          break;

        case "subscription.canceled":
        case "payment.refunded":
          await revokeOrExpireLicense(tx, event.data);
          break;

        case "payment.failed":
          await markLicensePastDue(tx, event.data);
          break;
      }
    });

    res.sendStatus(200);
  }
);

事件名称、签名头和 SDK 调用方式取决于所选的支付服务商。示例中的占位名称不能直接当作真实 API 使用。

时间如何估算

比起笼统估算“接入支付”需要多久,按功能拆分会更准确:

范围常见工作量
人工收款、后台手动开通许可证数天至两周
一次性激活码、过期和撤销约一至两周
支付页面、webhook、自动开通约两至四周
自动续费、失败重试、取消和退款再增加一至三周
发票、税务、席位、自助门户和审计视地区与业务复杂度增加数周

如果已有登录、数据库、后台管理和部署体系,开发时间可能明显缩短。如果还要从零建立账户体系、支付商户、隐私政策、服务条款和客户支持流程,整体周期会更长。

注意事项

  • 安装成功不代表付款成功,完成 OAuth 也不代表拥有许可证。
  • 不要把秘密 token 写入 Gadget、前端 JavaScript 或 Marketplace listing。
  • 激活码应随机生成、短期有效且只能使用一次。数据库最好只保存激活码的哈希值。
  • 身份绑定应以经过验证的 Google 身份信息为准,不能只依赖用户填写的域名。
  • 应明确规定试用、到期、付款失败、退款和宽限期对应的产品行为。
  • 应限制 webhook 重放,并确保处理过程具备幂等性。
  • 保留订单、授权变更和人工操作的审计记录。
  • 收集付款信息会带来安全和合规责任。通常应由支付服务商托管结账页面,避免自己的服务器接触银行卡数据。
  • 税率、发票、消费者退款权和数据保护义务因销售地区而异,需要根据实际经营主体咨询财税或法律专业人士。
  • 开始开发计费系统前,应先确认当前 Marketplace 产品类型、安装方式、OAuth 权限和审核要求,以免为已经迁移或不再接受新发布的旧式 Gadget 架构开发商业系统。

备注:内容仅供参考。