跳到正文

订阅生命周期 ​

本文介绍 SaaS 订阅的状态变化,以及如何处理续费、取消、套餐变更和到期事件。

商业 SaaS 订阅与应用的 API 调用授权相互独立。取消其中一项不会自动取消另一项,也不会自动退款。

状态处理 ​

免费套餐需在已发布方案中明确启用后才能激活。响应包含 billing_mode: free,无需付款,因此 active 也可能表示免费订阅。

升级后的摘要包含 upgrade_from_subscription_id。付款确认前,原免费订阅继续有效。分别按 (subscription_id, resource_version) 更新两份订阅记录;旧订阅的终止事件不能覆盖新订阅。遇到乱序时,重新查询当前订阅和成员访问权限。

付费升级确认后,原免费订阅已分配的成员及用户组来源会转到新订阅,重叠来源仍按成员去重计席位。产品开通与当前成员访问检查仍须成功;升级后被明确撤销的来源不会因后续订阅修订而恢复。

关闭新免费激活不会修改历史订单。订阅事件不提供付款引用、续费处理信息或调度字段;金额和付款结果请通过已授权的订单接口查询。

订阅事件的 data 包含当前商业订阅摘要。接收后更新本地订阅记录;它不是变化字段列表,也不提供成员权限。开放产品功能前,另行检查当前成员的访问权限。

商业状态含义接入方行为
draft / pending_payment草稿或等待支付展示准备/支付状态,不据此开放功能
trialing / trial_ending试用中或即将结束展示 trial_end,访问按授权回查结果执行
active商业订阅已生效展示当前套餐与周期,单独核对成员权限
renewal_pending进入续费窗口展示续费提示,不自行延长当前周期
payment_failed / past_due / grace_period支付失败、逾期或宽限期引导恢复支付,不自行延长 grace_deadline
plan_change_pending套餐变更尚未生效展示当前套餐与预约套餐,不提前套用新权益
pause_pending / paused已预约暂停或已暂停区分预约与生效时间,按当前访问结果限制操作
cancellation_pending取消已预约展示 cancel_at,不能当作已经取消
cancelled / expired取消已生效或订阅到期收紧访问并回查,提供重新订阅入口,保留历史
suspended商业订阅被限制收紧访问,只允许获批的恢复和 Support 操作
terminated该订阅被终止停止依据该来源放行,回查当前成员权限和替代订阅,保留历史

所有事件名称、触发条件、允许的 data.status 和相关字段见完整订阅事件表。只有已获批并勾选的事件才会投递;事件目录不代表你的应用已订阅全部事件。

场景一:激活 → 预约取消 → 取消生效 ​

假设客户购买一个月付套餐,周期为 10 月 6 日至 11 月 6 日;10 月 20 日预约周期末取消。以下是同一订阅 sub_demo_cancel 的业务时间线,不保证网络到达顺序:

业务时间(UTC)完整事件名版本接入方判断
10 月 6 日 00:00oem.subscription.activated.v12商业状态为 active;完整 HTTP 见接收指南
10 月 20 日 08:00oem.subscription.cancellation_scheduled.v13已预约取消,cancel_at 为 11 月 6 日,不能现在就显示“已取消”
11 月 6 日 00:00oem.subscription.cancelled.v14取消正式生效,ended_at 记录结束时间,停止按旧商业状态放行

预约取消的完整事件 ​

json
{
  "event_id": "evt_demo_cancellation_scheduled",
  "event_type": "oem.subscription.cancellation_scheduled.v1",
  "schema_version": "1",
  "occurred_at": "2026-10-20T08:00:00Z",
  "producer": "opaque",
  "tenant_id": "org_customer_demo",
  "resource_id": "sub_demo_cancel",
  "resource_version": 3,
  "correlation_id": "req_demo_cancellation",
  "data": {
    "id": "sub_demo_cancel",
    "organization_id": "org_customer_demo",
    "open_platform_application_id": "app_demo_saas",
    "environment": "sandbox",
    "product_tenant_id": "tenant_demo_cancel",
    "party_id": "party_customer_demo",
    "order_id": "order_demo_initial",
    "product_code": "relaydesk",
    "plan_code": "basic_monthly_demo",
    "plan_version": 1,
    "status": "cancellation_pending",
    "current_period_start": "2026-10-06T00:00:00Z",
    "current_period_end": "2026-11-06T00:00:00Z",
    "cancel_at": "2026-11-06T00:00:00Z",
    "cancel_at_period_end": true,
    "status_effective_at": "2026-10-20T08:00:00Z",
    "operation_id": "op_demo_cancellation",
    "reason_code": "example_customer_cancellation",
    "resource_version": 3,
    "created_at": "2026-10-05T23:55:00Z",
    "updated_at": "2026-10-20T08:00:00Z"
  }
}

status_effective_at 是进入 cancellation_pending 的时间,cancel_at 是取消生效时间,二者含义不同。cancel_at_period_end: true 表示本例在周期末取消。此时 plan_code 仍是当前套餐,不表示已退款,也不撤销 API 调用授权。

取消生效的完整事件 ​

json
{
  "event_id": "evt_demo_cancelled",
  "event_type": "oem.subscription.cancelled.v1",
  "schema_version": "1",
  "occurred_at": "2026-11-06T00:00:00Z",
  "producer": "opaque",
  "tenant_id": "org_customer_demo",
  "resource_id": "sub_demo_cancel",
  "resource_version": 4,
  "correlation_id": "req_demo_cancellation",
  "data": {
    "id": "sub_demo_cancel",
    "organization_id": "org_customer_demo",
    "open_platform_application_id": "app_demo_saas",
    "environment": "sandbox",
    "product_tenant_id": "tenant_demo_cancel",
    "party_id": "party_customer_demo",
    "order_id": "order_demo_initial",
    "product_code": "relaydesk",
    "plan_code": "basic_monthly_demo",
    "plan_version": 1,
    "status": "cancelled",
    "current_period_start": "2026-10-06T00:00:00Z",
    "current_period_end": "2026-11-06T00:00:00Z",
    "cancel_at": "2026-11-06T00:00:00Z",
    "status_effective_at": "2026-11-06T00:00:00Z",
    "ended_at": "2026-11-06T00:00:00Z",
    "operation_id": "op_demo_cancellation",
    "reason_code": "example_customer_cancellation",
    "resource_version": 4,
    "created_at": "2026-10-05T23:55:00Z",
    "updated_at": "2026-11-06T00:00:00Z"
  }
}

版本 4 表示同一资源的新摘要。若版本 4 先于版本 3 到达,先持久保存缺口并回查,不能回头让迟到的版本 3 恢复访问。取消和到期是不同原因的终态,不要把 cancelled 一律改写成 expired。

场景二:预约套餐变更 → 新套餐生效 ​

另一订阅 sub_demo_plan 当前使用 basic_monthly_demo。客户预约下个周期切换到 pro_monthly_demo:

阶段完整事件名关键字段与处理
预约成功,版本 6oem.subscription.plan_change_scheduled.v1status=plan_change_pending;plan_code 仍为当前套餐;pending_plan_code=pro_monthly_demo、pending_plan_version=2、pending_plan_effective_at=2026-11-06T00:00:00Z 描述预约,不提前开放新权益
生效,版本 7oem.subscription.plan_changed.v1plan_code=pro_monthly_demo、plan_version=2 成为当前套餐;本例 status=active,仍需独立回查成员权限

新套餐生效时,完整事件如下;这是另一订阅,不能接在前面的取消时间线上:

json
{
  "event_id": "evt_demo_plan_changed",
  "event_type": "oem.subscription.plan_changed.v1",
  "schema_version": "1",
  "occurred_at": "2026-11-06T00:00:00Z",
  "producer": "opaque",
  "tenant_id": "org_customer_demo",
  "resource_id": "sub_demo_plan",
  "resource_version": 7,
  "correlation_id": "req_demo_plan_change",
  "data": {
    "id": "sub_demo_plan",
    "organization_id": "org_customer_demo",
    "open_platform_application_id": "app_demo_saas",
    "environment": "sandbox",
    "product_tenant_id": "tenant_demo_plan",
    "party_id": "party_customer_demo",
    "order_id": "order_demo_plan_change",
    "product_code": "relaydesk",
    "plan_code": "pro_monthly_demo",
    "plan_version": 2,
    "status": "active",
    "current_period_start": "2026-11-06T00:00:00Z",
    "current_period_end": "2026-12-06T00:00:00Z",
    "status_effective_at": "2026-11-06T00:00:00Z",
    "operation_id": "op_demo_plan_change",
    "reason_code": "example_plan_change_effective",
    "resource_version": 7,
    "created_at": "2026-10-05T23:55:00Z",
    "updated_at": "2026-11-06T00:00:00Z"
  }
}

事件可能不提供旧套餐。展示变更前后差异时,将此前已核验的订阅记录与新摘要比较。金额和使用权限需单独查询,不添加事件未提供的 old_plan、new_plan 等字段。

plan_version 表示套餐定义版本,resource_version 表示订阅版本,两者分别使用。

场景三:续费失败 → 宽限期 → 到期 ​

订阅 sub_demo_expiry 的当前周期在 11 月 6 日结束。续费失败后,本例的宽限期截止到 11 月 9 日。实际期限使用响应中的时间,不能固定为三天:

阶段完整事件名接入方行为
进入续费窗口oem.subscription.renewal_upcoming.v1读取 renewal_window_start,不把通知当作续费成功
支付失败oem.subscription.payment_failed.v1读取实际 status,可能为 payment_failed、past_due 或 grace_period
逾期/宽限oem.subscription.past_due.v1按 grace_deadline 展示恢复期限;访问由当前回查结果决定
恢复成功(另一条分支)oem.subscription.renewed.v1新摘要为 active,读取新的周期边界,不自行按月加时间
未恢复而到期(本例)oem.subscription.expired.v1商业订阅结束,收紧访问,回查并保留历史

到期事件包含完整周期与宽限时间:

json
{
  "event_id": "evt_demo_expired",
  "event_type": "oem.subscription.expired.v1",
  "schema_version": "1",
  "occurred_at": "2026-11-09T00:00:00Z",
  "producer": "opaque",
  "tenant_id": "org_customer_demo",
  "resource_id": "sub_demo_expiry",
  "resource_version": 7,
  "correlation_id": "req_demo_renewal_failure",
  "data": {
    "id": "sub_demo_expiry",
    "organization_id": "org_customer_demo",
    "open_platform_application_id": "app_demo_saas",
    "environment": "sandbox",
    "product_tenant_id": "tenant_demo_expiry",
    "party_id": "party_customer_demo",
    "order_id": "order_demo_renewal",
    "product_code": "relaydesk",
    "plan_code": "basic_monthly_demo",
    "plan_version": 1,
    "status": "expired",
    "current_period_start": "2026-10-06T00:00:00Z",
    "current_period_end": "2026-11-06T00:00:00Z",
    "grace_deadline": "2026-11-09T00:00:00Z",
    "status_effective_at": "2026-11-09T00:00:00Z",
    "ended_at": "2026-11-09T00:00:00Z",
    "operation_id": "op_demo_renewal",
    "reason_code": "example_grace_expired",
    "resource_version": 7,
    "created_at": "2026-10-05T23:55:00Z",
    "updated_at": "2026-11-09T00:00:00Z"
  }
}

上面的 JSON 都是移除传输 Verification Token 后的完整业务事件,所有 ID、套餐、时间和原因码是合成值。producer: "opaque" 仅作不透明来源占位;实际来源枚举与原因码语义按已签发 Schema 处理。可下载预约取消、取消生效、套餐变更和到期用于实现核对。

更新订阅状态 ​

状态 API 和 resource_version 是最终判断依据。不要用旧版本 Webhook 覆盖新状态,也不要因为暂时没有收到事件就自行延长访问权限。

本地记录版本为 N、先收到 N+2 时,保存该事件并等待 N+1。超过接口约定的等待时间后,使用已授权的 Snapshot 或状态查询接口核对。

请同时订阅 oem.subscription.updated.v1。取消或恢复请求受理后,先以该事件提供新版本及真实当前状态;它不表示支付平台已确认。确认后才投递 oem.subscription.cancellation_scheduled.v1 或 oem.subscription.resumed.v1。等待期间保留当前周期,避免提前展示“已取消”或“已恢复”。仅开启旧的终态事件可能遗漏中间修订。

重新订阅会创建新的订阅和订单。不能把 expired 记录直接改回 active,也不能删除历史支付/退款证据或审计记录。

当前第三方 SaaS 的商业方案只支持固定月付或年付使用费,不提供储值余额、按量计费或超额扣费。

订阅变化后如何处理成员权限 ​

用户登录成功只表示身份认证完成,oem.subscription.activated.v1 只表示商业订阅激活,oem.product.activated.v1 只表示企业产品访问已开通;这些结果都不能单独授权某个成员。

  1. 收到 oem.access_context.changed.v1 后,先限制目标成员的旧权限。
  2. 使用当前应用的服务身份及 openplatform.api.invoke 调用 POST /open-platform/v1/applications/{id}/events/{event_id}/access-snapshot,正文为 {}。接口从本应用已接收的原事件确定客户与目标成员,不接受调用方另选客户,不使用当前登录用户的上下文接口替代。
  3. 精确核对 Application、环境、购买方 Organization 和产品。成员 product_access_allowed 必须是布尔 true,且身份、Membership、ProductAccess 有效,才应用当前 roles、entitlements、capabilities 和 access_revision。
  4. product_access_allowed 为 false 时清除本地有效权限。字段缺失、不是布尔值、版本不确定或查询失败时,继续限制访问。企业购买、VIP 角色或 active Membership 均不能单独证明成员可使用产品。

商业订阅版本与成员 access_revision 属于不同资源,不要互相比较。订阅事件和权限事件可能独立重试或乱序;按照各自的资源版本处理。更多字段见有效权限变更。

应用需启用成员权限能力。当前服务身份、应用、环境、原事件或能力失效时,回查会拒绝;依赖暂不可用时保留事件并重试。完整请求与失败处理见产品权限快照回查。

订阅与用户的关系 ​

订阅按应用、环境、客户空间和订阅 ID 关联;统一用户别名不能合并两份订阅。自动续费不依赖最后登录用户;成员访问按当前成员关系独立核验,见多应用、用户与事件接入。

开发者文档。第三方接入不需要内部运营平台权限。