开发者平台
主题

订阅支付#

订阅支付让买家授权一次,之后由商户后端按周期主动触发扣款,无需每期重新签名或充值。资金始终留在买家自己的钱包里;每期扣多少、多久扣一次、扣给谁,都在买家签名时就定好——商户改不了,也不能多扣。

本页面向 HTTP 卖家(订阅支付暂不支持 Agent 卖家)。订阅的定义、原理、状态机与升降级语义详见 核心概念 · 订阅支付,本页聚焦接入

适用场景#

业务特征是否适合
周期性、固定金额收费(月 / 季 / 年)
会员制 / 内容订阅 / API 套餐
需要分档(如 Basic / Pro)并支持升降级

前提条件#

  • API Key:商户需先在 OKX 开发者平台申请 API Key(API Key / Secret Key / Passphrase 三件套,对应请求头 OK-ACCESS-*、代码中的 OKX_API_KEY / OKX_SECRET_KEY / OKX_PASSPHRASE)。
  • 代币:商户可选择任意标准 ERC-20 代币作为支付币种;不支持原生币(OKB)。

业务流程#

买家一次双签(订阅条款 + Permit2 授权)创建订阅;之后每期由你触发扣款,合约按签名锁定的金额直转收款地址。

卖家接入#

接入分两种,按是否让用户切换套餐来选:

  • 基础接入 — 套餐固定,用户订阅后不切换;负责定义套餐、收款、自动续费。
  • 进阶:支持切换套餐 — 在基础之上,让用户在订阅期内升级或降级。

基础接入(套餐固定)#

开通订阅#

  1. 1
    定义套餐

    先选计费模式,再为每档配好价格、期数和促销。

    bash
    pnpm add @okxweb3/app-x402-core @okxweb3/app-x402-evm @okxweb3/app-x402-express express viem
    pnpm add -D tsx typescript @types/express @types/node
    
    ts
    import type { PlanCatalogEntry } from "@okxweb3/app-x402-core/subscription";
    
    const payTo = process.env.PAY_TO_ADDRESS!;   // 商家收款地址 (EIP-55)
    
    // 金额使用 token 最小单位 (USDT/USDC 6 位小数; "5000" = $0.005)
    export const basic: PlanCatalogEntry = {
      id: "basic_monthly",         // 套餐业务 id (访问放行时按此校验)
      tier: 1,                     // tier 越大等级越高 (决定升/降级方向)
      payTo,                       // 收款地址
      amountPerPeriod: "5000",     // 每期扣款金额
      periodSec: 2_592_000,        // 周=604800 / 月=2592000 / 年=31536000
      periodMode: 0,               // 0 = 固定间隔, 1 = 自然月
      maxPeriods: 12,              // 总授信额度上限 = maxPeriods × amountPerPeriod
      initialCharge: {             // 首扣策略 (促销用)
        periodCount: 1,            // 首扣覆盖多少期
        totalAmount: "5000",       // 首扣总额 (<= periodCount × amountPerPeriod)
      },
      name: "Basic Monthly",       // 展示元数据
      // asset: token 地址覆盖; 不填 → SDK 用当前网络的默认稳定币
    };
    
    // Pro: 更高 tier + 更高金额, 结构与 basic 一致
    export const pro: PlanCatalogEntry = {
      id: "pro_monthly",
      tier: 2,
      payTo,
      amountPerPeriod: "20000",
      periodSec: 2_592_000,
      periodMode: 0,
      maxPeriods: 12,
      initialCharge: { periodCount: 1, totalAmount: "20000" },
      name: "Pro Monthly",
    };
    
    // x402 route 需要完整的 `PaymentRequirements` accept 而非原始 plan;
    // 封装一次让所有 route 复用同一映射
    export const NETWORK = (process.env.CHAIN_NETWORK ?? "eip155:196") as `eip155:${string}`;
    export function toAccept(plan: PlanCatalogEntry) {
      return {
        scheme: "period",
        network: NETWORK,
        payTo: plan.payTo,
        asset: plan.asset,                                // 不填 → SDK 用默认稳定币
        price: { amount: plan.amountPerPeriod, asset: plan.asset },
        maxTimeoutSeconds: 600,                           // 402 挑战有效期
        extra: {
          amountPerPeriod: plan.amountPerPeriod,
          periodMode: plan.periodMode ?? 0,
          periodSec: plan.periodSec,
          maxPeriods: plan.maxPeriods,
          initialCharge: plan.initialCharge,
          plan: { id: plan.id, tier: plan.tier, name: plan.name },
        },
      };
    }
    

    计费模式

    按订阅日(每月同一天,遇月末顺延)按固定周期(每隔固定时长,如每 30 天)
    用户体验3/15 订阅 → 当天扣首期,之后 4/15、5/15…3/15 订阅、周期 30 天 → 当天扣首期,之后 4/14、5/14…(日期会漂移)
    periodMode自然月固定间隔
    periodSec0自定义秒数
    startAt0(立即开始)0(立即开始)
    billingAnchorAt= 订阅当天不适用
    • 固定间隔时长:周 = 604800、月(30 天)= 2592000、年(365 天)= 31536000;要对齐日历日期请用自然月。
    • 月末规则:按订阅当天的号数逐月扣;某月没有这个号数(如 2 月没有 31 号)就扣当月最后一天——1/31 → 2/28 → 3/31

    套餐参数——每档的价格、期数(示例:两档月付、按订阅日计费)

    套餐planIdplanTieramountPerPeriodmaxPeriods
    Basicbasic_m110 USDC12
    Propro_m230 USDC12
    • 金额按代币最小单位传(USDC 6 位小数,10 USDC = 10000000)。
    • planTier:数值越大档位越高,升降级时靠它判断方向;即使现在不做切换套餐,也建议一开始就按真实档位设好。

    促销玩法——都通过首期参数 initialChargePeriods / initialChargeAmount 实现,和你选哪种计费模式无关。以 amountPerPeriod=$30maxPeriods=12 为例:

    策略initialChargePeriodsinitialChargeAmount效果
    标准月付00逐期扣 $30
    首 N 期免费试用301~3 期免费,第 4 期起 $30
    首期折扣 / 优惠券1折后金额(如半价填 $15)第 1 期按折后价;链上不认优惠码,金额你的后端算
    首 N 期促销价预付3$30前 3 期合计预付 $30
    年付折扣(付 10 用 12)12$300一次扣 $300,立即完成
    抵扣 / 送一期1抵扣后金额(送一期填 0)把抵扣折进首期:下期 $30 抵 $20 → 填 $10
    • 约束:initialChargeAmount ≤ initialChargePeriods × amountPerPeriod(不允许加价)。
    • 抵扣只能在用户签名时设置(订阅、切换套餐);订阅开始后每期按固定金额扣,不能对单期临时减免。
  2. 2
    提供付费端口

    给付费资源开一个接口:没订阅的买家访问就返回 402 + 可选套餐;已订阅的正常返回内容。

    要不要放行,分两步鉴权:

    ① 验证身份(二选一)

    • 钱包凭证:买家用钱包签个凭证(accessProof),你验签拿到他的钱包地址。
    • 你的账户体系:订阅时把 subId 绑到你系统里的用户,之后照旧用 API Key / 登录态识别(已有账户体系推荐这种)。

    ② 检查权限:这个用户订阅有效、且他的套餐(planId)允许访问当前接口 → 放行;否则返回 402。(同一套系统里,某个接口可以只对某些档开放,比如高级接口只放 Pro。)

    订阅之后,买家每次请求服务都走这条:

    ts
    import express from "express";
    import { OKXFacilitatorClient } from "@okxweb3/app-x402-core";
    import { x402HTTPResourceServer, x402ResourceServer } from "@okxweb3/app-x402-core/server";
    import {
      InMemoryStore,
      SubscriptionClient,
      type OnBeforeAccessHook,
    } from "@okxweb3/app-x402-core/subscription";
    import { PermitSubscriptionScheme } from "@okxweb3/app-x402-evm/subscription";
    import { paymentMiddlewareFromHTTPServer } from "@okxweb3/app-x402-express";
    
    import { basic, pro, toAccept, NETWORK } from "./plans";
    
    function requireEnv(k: string): string {
      const v = process.env[k];
      if (!v) throw new Error(`Missing env: ${k}`);
      return v;
    }
    
    // ── facilitator + scheme + store: 所有 route 共用同一套装配 ───────────
    const facilitator = new OKXFacilitatorClient({
      apiKey:     requireEnv("OKX_API_KEY"),
      secretKey:  requireEnv("OKX_SECRET_KEY"),
      passphrase: requireEnv("OKX_PASSPHRASE"),
      baseUrl:    process.env.OKX_BASE_URL,   // 不填 → OKX 生产环境
    });
    
    const store  = new InMemoryStore();       // 生产环境换成持久化 store
    const scheme = new PermitSubscriptionScheme({
      facilitator,
      network: NETWORK,
      store,                                  // 与下方 SubscriptionClient 共享
    });
    const client = new SubscriptionClient({ scheme, store });   // 用于主动 charge / cancelBySeller / syncFromChain
    
    // register(network, scheme) 后 initialize() → 拉取 /supported 并缓存
    // (facilitatorAddress, subscriptionContract, permit2Contract) 三元组
    const server = new x402ResourceServer(facilitator).register(NETWORK, scheme);
    await server.initialize();
    
    // ── seller 全局 onBeforeAccess (对齐 Rust `SubscriptionSupport::on_before_access`) ──
    // 触发时点: `verifyAccess` 通过后 (签名 + payer + plan 白名单 + 周期校验),
    // route handler 执行前. 每个 access-verified 请求都会触发.
    // 返回 `{ ok: false, error }` 即拒绝 → 402.
    //
    // ctx 上可用字段:
    //   ctx.subscription       — 完整 Subscription: subId / payer / merchant /
    //                            planId / planTier / amountPerPeriod / periodSec /
    //                            periodMode / maxPeriods / state / lastChargedPeriod /
    //                            elapsedPeriods / nextChargeableAt / pendingPlanChange …
    //   ctx.request.path       — 请求 pathname, 如 "/premium"
    //   ctx.request.method     — 实际 HTTP method (非硬编码)
    //   ctx.request.headers    — 全部请求头 (小写 key)
    //   ctx.route.acceptedPlanIds — 本 route `accepts` 里的 plan id 列表
    //   ctx.route.accepts      — 完整 `PaymentRequirements[]`: 每项 `extra.plan`
    //                            = { id, tier, name }, 同时带 `extra.amountPerPeriod`
    //                            / `extra.periodSec` / `extra.periodMode` /
    //                            `extra.maxPeriods`. 需要基于套餐详情做策略 (升级
    //                            提示 / tier 上限 / 价格分层) 时直接读这里,
    //                            无需 seller 侧再维护一张 catalog 表.
    //
    // 多个 hook 累加执行: 多次调用 `.onBeforeAccess(hook)` 按注册顺序执行,
    // 任一 hook 返回 `{ ok:false }` 立即拒绝. 路由级 `RouteConfig.onBeforeAccess`
    // 在所有全局 hook 之后执行.
    const denied = new Set<string>();
    const quotaByPayer = new Map<string, number>();   // 按 payer 计数的日配额
    const DAILY_QUOTA = 10_000;
    
    const banGuard: OnBeforeAccessHook = async (ctx) => {
      if (denied.has(ctx.subscription.subId)) {
        return { ok: false, error: "access_denied_by_merchant" };
      }
      return { ok: true };
    };
    
    const quotaGuard: OnBeforeAccessHook = async (ctx) => {
      const key = ctx.subscription.payer;
      const used = (quotaByPayer.get(key) ?? 0) + 1;
      quotaByPayer.set(key, used);
      if (used > DAILY_QUOTA) {
        return { ok: false, error: "quota_exhausted", retryAfter: 86_400 };
      }
      return { ok: true };
    };
    
    const headerGuard: OnBeforeAccessHook = async (ctx) => {
      // CDN header 驱动的地区门 — 任意业务逻辑均可读 ctx.request
      if (ctx.request.headers["x-region"] === "restricted") {
        return { ok: false, error: "region_blocked" };
      }
      return { ok: true };
    };
    
    // 示例: 用 ctx.route.accepts 里的完整 plan 详情, 在当前订阅未达路由最高 tier
    // 时给出升级提示 (仅记录不拒绝)
    const upgradeHint: OnBeforeAccessHook = async (ctx) => {
      const currentTier = ctx.subscription.planTier;
      const acceptTiers = (ctx.route.accepts ?? [])
        .map((r) => (r.extra?.plan as { tier?: number } | undefined)?.tier ?? 0);
      const maxAcceptTier = Math.max(0, ...acceptTiers);
      if (currentTier < maxAcceptTier) {
        // 非拒绝 — 仅记录 / 打点. 业务逻辑仍可放行
        console.log(`sub ${ctx.subscription.subId}: 当前 tier ${currentTier}, 本路由最高接受 tier ${maxAcceptTier}`);
      }
      return { ok: true };
    };
    
    // routes.accepts = 套餐门禁: 只有订阅的 planId 命中列出的 accept 才放行,
    // 否则返回 402
    const routes = {
      "GET /weather": {
        accepts: [toAccept(basic)],              // 仅 Basic
        description: "天气数据 (Basic 套餐)",
        mimeType: "application/json",
      },
      "GET /premium": {
        accepts: [toAccept(pro)],                // 仅 Pro; Basic 订阅访问会 402
        description: "高级分析 (仅 Pro 套餐)",
        mimeType: "application/json",
      },
    };
    
    // Builder 风格装配: 在 HTTPResourceServer 上挂 seller 全局 onBeforeAccess,
    // 再交给 express factory
    const httpServer = new x402HTTPResourceServer(server, routes)
      .onBeforeAccess(banGuard)
      .onBeforeAccess(quotaGuard)
      .onBeforeAccess(headerGuard)
      .onBeforeAccess(upgradeHint);
    
    const app = express();
    app.use(express.json());
    app.use(paymentMiddlewareFromHTTPServer(httpServer));
    
    app.get("/weather", (req, res) => {
      // 中间件完成 access 校验后会把 subscription 挂到 req.x402 上
      res.json({ report: { weather: "sunny", temperature: 23 } });
    });
    app.get("/premium",  (_req, res) => res.json({ report: { premium: true } }));
    
    app.listen(4022, "0.0.0.0", () => console.log("listening on :4022"));
    
  3. 3
    创建订阅

    SDK 把买家双签转发给 Facilitator,合约创建并扣首期。

    ts
    // 订阅创建全自动: 买家把双签 payload (Permit2 PermitSingle + SubscriptionTerms)
    // POST 到任意 `accepts` 里列出了目标 plan 的订阅路由, 中间件即完成 verify + settle,
    // 商家侧无需额外代码
    
    const routes = {
      "GET /weather": {
        accepts: [toAccept(basic)],              // 本路由允许买家订阅哪些 plan
        description: "天气数据 (Basic 套餐)",
        mimeType: "application/json",
      },
    };
    
    const app = express();
    app.use(express.json());
    app.use(paymentMiddleware(routes, server));  // 启用自动订阅
    
    app.get("/weather", (req, res) => {
      // 首次成功请求, 中间件会跑完 subscribe 流程并把 subscription 挂到 req 上
      const x402 = (req as any).x402;
      res.json({
        report: { weather: "sunny", temperature: 23 },
        subId: x402?.subscription?.subId,
      });
    });
    
    // 订阅成功后中间件会写入 `PAYMENT-RESPONSE` 响应头, 内容为
    // { subId, txHash, state } (state===1 → 已激活), 失败返回 HTTP 402
    
  4. 4
    定时扣款

    nextChargeableAt 定时触发扣款。

    ts
    // ── 路径 A: 使用 SDK 层 (SubscriptionClient) ──
    // store.list() 遍历所有持久化订阅; nextChargeableAt (每次 getSubscription /
    // charge 后都会刷新) 标记下一次可扣时间点. `client.charge` 完成扣款并同步 store
    // (推进 lastChargedPeriod, 降级激活时迁移 subId). 合约侧不做补扣: 每次调用最多
    // 推进一期.
    //
    // 沿用步骤 02 中的 `store` / `client`, 不需要重新装配
    const nowSec = () => Math.floor(Date.now() / 1000);
    
    const timer = setInterval(async () => {
      const subs = await store.list();
      for (const sub of subs) {
        if (sub.state !== "active") continue;                          // canceled / completed / changed 直接跳过
        if (sub.nextChargeableAt == null) continue;                    // 未扣快照, 用 syncFromChain 刷新
        if (sub.nextChargeableAt > nowSec()) continue;                 // 还没到期
        try {
          const r = await client.charge(sub.subId);
          // r.txHash / r.state / r.planChangeTriggered — 若为 true 表示排队中的降级
          // 已激活, 新的 active subId 在 `sub.changedToSubId` 里; scheme.charge
          // 已完成 store 行迁移
        } catch (_e) {
          // dunning: 按需重试 / 通知买家 / N 次失败后自动取消
        }
      }
    }, 60_000);
    
    // ── 路径 B: 完全自定义调度 + 底层 charge ──
    // 调度 / 选择 / 状态全部自管, 直接调 facilitator 的 `chargeSubscription`,
    // 手动处理原始 ChargeResult (period / state / txHash / planChangeTriggered)
    import type { SubscriptionFacilitatorClient } from "@okxweb3/app-x402-core/subscription";
    // `facilitator` 就是 OKXFacilitatorClient, 已实现 SubscriptionFacilitatorClient
    // (subscribe / change / cancel / charge / getSubscription)
    const dueSubIds: string[] = /* 自行查询 — Redis / Postgres / cron shard … */ [];
    for (const subId of dueSubIds) {
      await (facilitator as SubscriptionFacilitatorClient).chargeSubscription(subId);
    }
    

取消订阅#

买家和商户都能发起取消:

  • 立即停止后续扣款
  • 已付不退
  • 仅在订阅生效中可取消
ts
// 声明 `operation` route, 中间件会中转买家侧已签好的 CancelAuth
// (或 PendingChangeCancelAuth) 给 facilitator. 没有 402 握手 — 买家直接 POST
// 已签授权, 中间件在 handler 之前完成 verify + settle

const routes = {
  // ... 资源路由 ...

  "POST /subscription/cancel": {                    // 买家 POST 已签 CancelAuth
    accepts: [toAccept(basic), toAccept(pro)],      // 列出所有支持的 plan
    description: "取消订阅",
    mimeType: "application/json",
    operation: "cancel" as const,
  },
  "POST /subscription/cancel-pending": {            // 撤销尚未生效的降级
    accepts: [toAccept(basic), toAccept(pro)],
    description: "撤销排队中的降级",
    mimeType: "application/json",
    operation: "cancel-pending-change" as const,
  },
};

const app = express();
app.use(express.json());
app.use(paymentMiddleware(routes, server));

// 注册这两个 route; 中间件会在 handler 之前拦截
app.post("/subscription/cancel",         (_req, res) => res.json({ ok: true }));
app.post("/subscription/cancel-pending", (_req, res) => res.json({ ok: true }));

// 成功后中间件写入 `PAYMENT-RESPONSE` 响应头, 内容为 { subId, txHash, state },
// state===3 → 已取消
//
// 商家主动取消完全跳过买家: 本地签一份 initiator="merchant" 的 CancelAuth,
// 然后调 `client.cancelBySeller(subId, auth)`

进阶:支持切换套餐#

在基础接入之上,新增一个处理套餐切换的接口,升级和降级都通过它完成,其余不变:

示例代码

ts
// 一个 change 端点即可; 升 / 降级方向由目标 tier 与当前 tier 对比推导.
// 同一 URL 分两阶段:
//   阶段 1: 仅带 APP-Access → 返回 402 + extra.changeFrom (当前订阅信息)
//   阶段 2: 买家基于 changeFrom 签新 SubscriptionTerms, 再带 PAYMENT-SIGNATURE
//           重发 → 中间件执行 change

const routes = {
  // ... 资源路由, 取消路由 ...

  "GET /subscription/change": {
    accepts: [                                    // 列出所有可切换的 plan
      toAccept(basic),
      toAccept(pro),
    ],
    description: "切换订阅套餐",
    mimeType: "application/json",
    operation: "change" as const,
  },
};

const app = express();
app.use(express.json());
app.use(paymentMiddleware(routes, server));

// 只有 change 成功才会到达 handler; 新的 subId / txHash / state 在
// `PAYMENT-RESPONSE` 头里, req.x402.settleResult.data 同时携带这些字段
app.get("/subscription/change", (req, res) => {
  const data = (req as any).x402?.settleResult?.data;
  res.json({
    result: "订阅套餐已切换",
    newSubId: data?.newSubId,
    operationType: data?.operationType,          // "upgrade" | "downgrade"
    scheduledFromPeriod: data?.scheduledFromPeriod,
  });
});

// 升级立即生效 (state===1); 降级在期末生效, 买家在期末前会看到
// subscription.pendingPlanChange 里带目标 planId. 期末生效前要回滚,
// 用步骤 05 的 `cancel-pending-change` 路由

升级 vs 降级

升级降级
方向新档 planTier 更高新档 planTier 更低(同档会被拒)
什么时候生效立即(切换那刻自动完成)当前周期用完后——需你的后端触发一次扣款才切换
生效时扣多少扣新档首期:默认全额;想只补差价 / 做抵扣,就自己算好金额填 initialChargeAmount扣新档(低档)首期
subId 变化当场返回新 subId登记时先返回一个(状态待生效 pending),生效后启用

例子(Basic $10/月、Pro $30/月,按订阅日计费,用"账单日不变")

  • 3/15 订阅 Basic → 每月 15 号扣 $10。
  • 6/20 升级 Pro(立即生效):当天补本期差价 $30 − $10 = $20;7/15 起按 Pro 扣 $30,账单日仍是 15 号。
  • 9/20 降回 Basic(期末生效):当天只登记、9 月继续用 Pro;10/15 你触发扣款 → 切到 Basic 扣 $10;已付的 Pro 不退。

升级后的账单日

升级后仍按原来的账单日和到期日扣款,升级当天只补本期差价。适合需要统一对账的商户。

例:原本每月 15 号扣,用户 6/20 升级 → 6/20 当天补本期差价,7/15 起按新档扣,账单日仍是 15 号。

参数:

  • startAt = 当前账期起点
  • initialChargePeriods = 1
  • initialChargeAmount = 新档当期应收 − 旧档当期已付(≥0)
  • maxPeriods = 原期数 − 当前是第几期 + 1

连续切换

操作序列怎么办
升级 → 又想降回对新订阅发起降级(升级不可即时撤销,只能期末降)
已登记降级 → 又想升级先撤回降级再升级(同时只能有一个待生效计划)
同档变更直接被拒

对接要点与接口#

  • 定时扣款要可靠 — 漏期不补扣,一次最多扣 1 期;按 nextChargeableAt 选出到期订阅、幂等触发。
  • 失败同步可知 — 无 webhook。入参错误直接接口报错;链上结果要读响应 data.state —— 可能返回 pending(0),需轮询查询接口确认终态。
  • 授权额度要覆盖整个订阅期 — 多条订阅共用一份 Permit2 额度、新授权覆盖旧的;升到更贵 / 更长套餐时,买家要重签更大额度。
  • subId 会变 — 升降级后用接口返回的新 subId 更新映射,无需追溯历史。

订阅的查询接口(订阅详情 / 扣款流水 / 待生效降级 / 授权状态 / 买家订阅列表)、字段、鉴权与完整错误码,详见 API 参考

买家接入#

买家用支持 Onchain OS 的 AI Agent(如 Claude Code、Cursor)来订阅——用自然语言告诉 Agent 要做什么即可。

  1. 1
    装好 Onchain OS + 登录钱包

    让 Agent 安装 skills、再用邮箱登录 Agentic Wallet(首次登录自动创建钱包,私钥在 TEE 里生成):

    运行 npx skills add okx/onchainos-skills,然后用邮箱登录 Agentic Wallet

    详见 安装 Agentic Wallet

  2. 2
    订阅卖家服务

    让 Agent 访问卖家的付费端口。Agent 会收到 402 和可选套餐,你选好要订的档位、按提示确认,即完成订阅授权:

    访问 <卖家的付费端口>,订阅 Pro 套餐
  3. 3
    开始使用

    之后再访问同一端口就能拿到服务;后续每期由卖家自动扣款,你无需再操作。

  4. 4
    升级/降级

    让 Agent 完成:

    升级/降级 <卖家的付费端口>,至 Pro 套餐
  5. 5
    取消订阅

    让 Agent 完成:

    取消 <卖家的付费端口> 的 Pro 套餐订阅

边界与权衡#

什么时候不该用订阅支付

常见问题#

能给用户退款吗?
链上不退款。升级补差价、降级、取消都不退已付金额。
如需退款,由你的后端走链下单独转账;或在下一次升降级时用首期金额做抵扣。
旧订阅立即变为“已切换”、不再扣款;同时创建一条新订阅并返回新的 subId。
把你侧的“用户↔订阅”映射更新到新的 subId 即可,无需追溯历史。
降级在当前周期末生效,让用户先用完已付费的当期,符合主流订阅惯例。
到期后必须由你的后端触发一次扣款才会切换,且要赶在待生效计划失效前。
漏期不补扣,一次最多扣 1 期,跨多期只扣当前期、中间期作废。
所以定时扣款任务要可靠、按时触发。
不支持暂停。需要的话只能取消,之后让用户重新订阅。
每期金额全程固定。想做阶梯价,只能用首期参数覆盖前 N 期——比如前 3 期促销价、第 4 期起统一价。

下一步#

最近更新:2026-06-30