如何为付费内容收取订阅费?
结论
可以接入成熟的订阅计费服务。普通 Web 付费内容通常采用以下方案:
- 用 Stripe Billing、Paddle、Chargebee 或 Recurly 管理订阅周期、续费、退款和支付失败。
- 用户账号和内容访问权限由自己的系统管理。
- 通过支付服务的 Webhook 同步订阅状态,不能仅凭前端的“支付成功”页面开放内容。
如果需要快速上线,Stripe Checkout、Billing 和 Customer Portal 的组合通常已经够用。如果不想自行承担销售税、VAT 等商户责任,可以考虑 Paddle 这类采用 Merchant of Record 模式的服务。实际覆盖范围和费用以服务商当前条款为准。
iPad 需要另行考虑。如果以后推出原生 iPad App,数字内容订阅可能受 App Store 支付规则约束。相关规则会因地区、应用类型和分发方式而异,上线前需要核对 Apple 当时的审核指南。即使只在网页销售,再让已订阅用户登录 App 阅读,也不能直接认定这种做法一定符合审核要求。
推荐的系统结构
支付平台负责收款,应用负责判断用户能访问哪些内容:
用户注册或登录
↓
服务器创建订阅结账会话
↓
用户在支付平台完成付款
↓
支付平台发送 Webhook
↓
服务器更新本地订阅状态
↓
用户请求付费内容时检查访问权限
本地数据库至少要保存以下信息:
users
- id
- email
- password_hash 或第三方登录标识
- billing_customer_id
subscriptions
- user_id
- billing_subscription_id
- status
- current_period_end
- price_id
不要保存信用卡号、CVC 等敏感卡片信息。付款页面应由支付服务托管,也可以使用服务商提供的安全支付组件,以缩小 PCI 合规范围。
实现步骤
1. 建立用户账号体系
用户应先登录,再进入订阅页面。账号既可以由应用自行管理,也可以使用 Auth0、Clerk、Firebase Authentication 等身份服务。
无论采用哪种方式,支付平台中的 customer 都要与本地用户保持稳定映射。不要只靠电子邮箱临时匹配,因为用户可能更换邮箱,也可能存在重复账号。
2. 创建按月计费的价格
在支付平台中创建产品和循环价格,例如:
产品:高级内容
计费周期:monthly
价格:由业务确定
币种:由目标市场确定
服务端只能接受预先允许的 price_id。不要信任浏览器直接提交的价格或金额。
3. 创建订阅结账会话
下面以 Node.js、Express 和 Stripe Checkout 为例。代码没有指定具体的 SDK 版本,实际使用时应根据当前官方文档安装兼容版本。
npm install express stripe
import express from "express";
import Stripe from "stripe";
const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
app.use(express.json());
app.post("/api/create-checkout-session", requireLogin, async (req, res) => {
const user = req.user;
let customerId = user.billingCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({
email: user.email,
metadata: {
userId: String(user.id),
},
});
customerId = customer.id;
await db.users.update(user.id, {
billingCustomerId: customerId,
});
}
const session = await stripe.checkout.sessions.create({
mode: "subscription",
customer: customerId,
line_items: [
{
price: process.env.STRIPE_MONTHLY_PRICE_ID,
quantity: 1,
},
],
success_url: `${process.env.APP_URL}/account?checkout=success`,
cancel_url: `${process.env.APP_URL}/subscribe?checkout=cancelled`,
metadata: {
userId: String(user.id),
},
});
res.json({ url: session.url });
});
前端拿到 URL 后,跳转到托管结账页面:
async function subscribe() {
const response = await fetch("/api/create-checkout-session", {
method: "POST",
credentials: "include",
});
if (!response.ok) {
throw new Error("Unable to create checkout session");
}
const { url } = await response.json();
window.location.assign(url);
}
success_url 只能说明用户回到了网站,不能证明订阅仍然有效。订阅状态必须由服务端查询或通过 Webhook 确认。
4. 通过 Webhook 同步订阅状态
Webhook 是这套方案的核心。续费成功、扣款失败、取消订阅和订阅到期,都可能发生在用户离开网站以后。
Stripe 验证签名时需要原始请求体,因此 Webhook 路由应放在全局 express.json() 中间件之前,或者单独配置路由:
app.post(
"/webhooks/stripe",
express.raw({ type: "application/json" }),
async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers["stripe-signature"],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch {
return res.status(400).send("Invalid webhook signature");
}
try {
switch (event.type) {
case "customer.subscription.created":
case "customer.subscription.updated":
case "customer.subscription.deleted": {
const subscription = event.data.object;
await syncSubscription({
customerId: subscription.customer,
subscriptionId: subscription.id,
status: subscription.status,
currentPeriodEnd: getCurrentPeriodEnd(subscription),
});
break;
}
case "invoice.paid":
// 可在这里记录账单已支付,或触发收据相关业务。
break;
case "invoice.payment_failed":
// 可通知用户更新付款方式,但不要记录卡片敏感信息。
break;
}
res.sendStatus(200);
} catch (error) {
console.error("Webhook processing failed", error);
res.sendStatus(500);
}
}
);
不同 SDK 或 API 版本返回的订阅周期字段结构可能不同。示例中的 getCurrentPeriodEnd() 应按照实际响应实现,不能直接假定字段位置。
Webhook 处理还要保证幂等性。可以把 event.id 存入数据库。收到已经处理过的事件时直接返回成功,避免平台重试导致重复授权或重复通知。
5. 在服务端检查内容权限
每次返回付费内容之前,都要在服务端检查订阅状态:
async function requireActiveSubscription(req, res, next) {
const subscription = await db.subscriptions.findByUserId(req.user.id);
const allowedStatuses = new Set(["active", "trialing"]);
if (!subscription || !allowedStatuses.has(subscription.status)) {
return res.status(403).json({
error: "An active subscription is required",
});
}
next();
}
app.get(
"/api/premium/articles/:id",
requireLogin,
requireActiveSubscription,
async (req, res) => {
const article = await db.articles.findPremiumById(req.params.id);
if (!article) {
return res.sendStatus(404);
}
res.json(article);
}
);
是否允许 past_due 状态访问,是否设置宽限期,以及取消后能否继续阅读,都要由业务规则决定。例如,用户取消自动续费后,通常仍可访问到已付款周期结束,而不是立即失去权限。
只在前端隐藏按钮、模糊正文或添加 CSS 遮罩,并不能形成有效的访问控制。未授权用户不应通过接口、HTML 源码或预加载数据获得完整内容。
6. 提供自助管理入口
可以使用支付平台的 Customer Portal,让用户自行更新付款方式、查看账单或取消订阅:
app.post("/api/create-billing-portal-session", requireLogin, async (req, res) => {
if (!req.user.billingCustomerId) {
return res.status(400).json({ error: "Billing account not found" });
}
const session = await stripe.billingPortal.sessions.create({
customer: req.user.billingCustomerId,
return_url: `${process.env.APP_URL}/account`,
});
res.json({ url: session.url });
});
与自行开发信用卡更新、账单历史和取消流程相比,这种方式通常更可靠。
如何选择服务
如果已有开发团队,并且希望灵活控制付款流程、税务设置和用户体验,可以先考虑 Stripe Billing 这类支付基础设施。
如果已经接入多个支付渠道,希望将订阅编排与底层支付网关分开,可以评估 Chargebee 或 Recurly。这类服务通常仍要连接受支持的支付网关,具体组合需要查阅服务商文档。
如果希望由服务商作为 Merchant of Record,承担部分税务申报、付款和账单责任,可以评估 Paddle 等方案。这能减少一部分运营工作,但费率、结算周期、退款政策和可销售地区也可能受到更多平台限制。
选择服务时,应重点比较:
- 是否支持目标国家、币种和付款方式;
- 是否支持自动续费、试用期、优惠券和按比例计费;
- 支付失败后的重试和催收机制;
- 税费由谁计算,申报责任由谁承担;
- 如何处理退款、拒付和争议;
- 是否支持数据导出和迁移;
- Webhook、沙箱环境和客户门户是否完善;
- 月费、交易费、跨境费和货币转换费。
iPad 端的处理
如果 iPad 用户只是通过 Safari 访问网站,通常仍按 Web 订阅流程处理。
如果要开发原生 iPad App,设计前需要确认以下问题:
- App 内是否销售数字内容,或引导用户购买数字内容;
- 是否必须使用 Apple 的 In-App Purchase;
- App 是否属于允许用户登录并访问既有订阅的阅读类应用;
- 目标地区是否允许特定的外部购买链接或替代支付方式;
- 如何把 Web 订阅和 App Store 订阅绑定到同一个用户账号;
- 用户同时通过两个渠道订阅时,如何避免重复收费。
常见做法是建立统一的“访问权益”层:
Web 支付订阅 ─────┐
├─> entitlement ─> 允许访问付费内容
App Store 订阅 ──┘
应用只查询用户是否拥有有效权益,不直接依赖某个平台的订单状态。购买入口、提示文字和跳转方式仍要符合对应应用商店的现行规则。
注意事项
- Secret Key 和 Webhook Secret 只能存放在服务端环境变量中。
- 必须验证 Webhook 签名,并妥善处理重复、乱序和延迟事件。
- 不要根据客户端传来的
userId、金额或订阅状态授予权限。 - 分别为退款、拒付、支付失败、取消和到期制定明确规则。
- 记录订阅状态的变更日志,以便排查用户投诉。
- 正式上线前,要在支付平台的测试环境中覆盖首次付款、续费失败、恢复付款、取消和退款等流程。
- 清楚展示价格、计费周期、自动续费条款和取消方式。
- 在隐私政策和服务条款中说明支付处理方及必要的数据共享。
- 各地区的税务、消费者保护和自动续费规定不同。如果目标市场较多,应请专业人员确认合规责任。
- 支付服务和 App Store 政策都可能调整,实施时应以各平台当前的官方文档和合同为准。
备注:内容仅供参考。
