订阅生命周期
本文介绍 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:00 | oem.subscription.activated.v1 | 2 | 商业状态为 active;完整 HTTP 见接收指南 |
| 10 月 20 日 08:00 | oem.subscription.cancellation_scheduled.v1 | 3 | 已预约取消,cancel_at 为 11 月 6 日,不能现在就显示“已取消” |
| 11 月 6 日 00:00 | oem.subscription.cancelled.v1 | 4 | 取消正式生效,ended_at 记录结束时间,停止按旧商业状态放行 |
预约取消的完整事件
{
"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 调用授权。
取消生效的完整事件
{
"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:
| 阶段 | 完整事件名 | 关键字段与处理 |
|---|---|---|
| 预约成功,版本 6 | oem.subscription.plan_change_scheduled.v1 | status=plan_change_pending;plan_code 仍为当前套餐;pending_plan_code=pro_monthly_demo、pending_plan_version=2、pending_plan_effective_at=2026-11-06T00:00:00Z 描述预约,不提前开放新权益 |
| 生效,版本 7 | oem.subscription.plan_changed.v1 | plan_code=pro_monthly_demo、plan_version=2 成为当前套餐;本例 status=active,仍需独立回查成员权限 |
新套餐生效时,完整事件如下;这是另一订阅,不能接在前面的取消时间线上:
{
"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 | 商业订阅结束,收紧访问,回查并保留历史 |
到期事件包含完整周期与宽限时间:
{
"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 只表示企业产品访问已开通;这些结果都不能单独授权某个成员。
- 收到
oem.access_context.changed.v1后,先限制目标成员的旧权限。 - 使用当前应用的服务身份及
openplatform.api.invoke调用POST /open-platform/v1/applications/{id}/events/{event_id}/access-snapshot,正文为{}。接口从本应用已接收的原事件确定客户与目标成员,不接受调用方另选客户,不使用当前登录用户的上下文接口替代。 - 精确核对 Application、环境、购买方 Organization 和产品。成员
product_access_allowed必须是布尔true,且身份、Membership、ProductAccess 有效,才应用当前roles、entitlements、capabilities和access_revision。 product_access_allowed为false时清除本地有效权限。字段缺失、不是布尔值、版本不确定或查询失败时,继续限制访问。企业购买、VIP角色或activeMembership 均不能单独证明成员可使用产品。
商业订阅版本与成员 access_revision 属于不同资源,不要互相比较。订阅事件和权限事件可能独立重试或乱序;按照各自的资源版本处理。更多字段见有效权限变更。
应用需启用成员权限能力。当前服务身份、应用、环境、原事件或能力失效时,回查会拒绝;依赖暂不可用时保留事件并重试。完整请求与失败处理见产品权限快照回查。
订阅与用户的关系
订阅按应用、环境、客户空间和订阅 ID 关联;统一用户别名不能合并两份订阅。自动续费不依赖最后登录用户;成员访问按当前成员关系独立核验,见多应用、用户与事件接入。