PAYATHON 2026

如何为付费内容收取订阅费?

支付小周

结论

可以接入成熟的订阅计费服务。普通 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 政策都可能调整,实施时应以各平台当前的官方文档和合同为准。

备注:内容仅供参考。