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 可能重复发送,也可能不按顺序到达,因此订单处理必须具备幂等性。
人工付款和手动授权
人工处理完全可行,尤其适合客户较少、合同金额较高,或者主要通过企业采购成交的产品。
一种简单的流程是:
- 客户提交购买申请,并提供 Workspace 域或管理员账户信息。
- 你发送报价单或发票。
- 确认款项到账后,在内部管理后台创建许可证。
- 向客户发送 Marketplace 安装链接。
- 客户按照正常流程安装应用,并授予必要权限。
- 应用后端识别客户身份并检查许可证。
- 许可证有效时启用产品功能。
这里的“手动启用”不一定需要 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 架构开发商业系统。
备注:内容仅供参考。
