For AI agents: the complete documentation index is available at https://docs.halo.run/llms.txt, the full documentation bundle is available at https://docs.halo.run/llms-full.txt, and this page is available as Markdown at https://docs.halo.run/developer-guide/shop/integration/subscription-lifecycle.md.

订阅生命周期

适用范围

本页适用于 Halo 商城版 2.27.0 及以上版本。传输与鉴权约定见商城订阅对接,事件推送见订阅 Webhook。

数据模型

订阅由四层对象组成,由运营在控制台 商店 → 商品 中配置(操作说明见商城 / 订阅管理):

对象说明
产品线 ProductproductType=SUBSCRIPTION 的商品,例如「示例 Coding 套餐」。
档次 Tier产品线下的商业等级,是权益契约的归属单位。entitlements 是任意 JSON,由接入方解释;同一档次下的月付与年付共享同一份契约。
计划 Plan可购买的套餐,由档次 × 计费模式 × 周期组成,包含 billingMode、billingPeriod、periodCount、price、试用配置与数量上下限。计划与商品规格(variantId)一一对应。
订阅 Subscription某客户在某条产品线下的实例,保存状态、周期与 effectiveEntitlements 快照。

产品线策略决定同一客户能否并存多条订阅、试用是否只发一次、到期前提前几天提醒:

策略说明
singleSubscription为 true 时同一客户在同一产品线同时只有一条生效订阅,再次购买会作用于已有订阅;为 false 时每次购买都会新建一条独立订阅,各自续费、变更与取消。
trialOncePerCustomer试用是否按客户只发一次(以该客户在该产品线是否有历史订阅判断)。
renewalLeadDays到期前多少天发送续费提醒,默认 5 天。

计划字段中与周期相关的取值:

取值说明
billingMode=SUBSCRIPTION连续计费:到期前提醒,到期未付进入 PAST_DUE 宽限期,客户可手动续费。
billingMode=ONE_TIME预付一期:到期直接 EXPIRED,需要重新购买。
billingMode=LIFETIME买断:周期字段为空,永不过期。
billingPeriod=MONTHLY / YEARLY周期单位为月或年;2.27 控制台创建计划时 periodCount 固定为 1。
gracePeriodDays仅连续计费有效:到期未付后的容忍天数。
entitlements档次上的 JSON 契约,在购买、变更、续费时快照到订阅的 effectiveEntitlements。

接入方需要长期依赖的关联键:

  • customerId:商城客户 ID,用于与接入方账号对齐(控制台可通过顾客接口换取 userId、邮箱)。
  • productId:产品线 ID。
  • planId:档次内的具体套餐,请以此判断档位,不要用价格或名称。
  • variantId:计划对应的商品规格 ID,订单行使用它标识订阅行。

状态机

重新购买的复用条件

仅当产品线策略 singleSubscription=true 时,重新购买会复用 EXPIRED / CANCELLED 的历史订阅行;为 false 时每次购买都会新建订阅。此外,如果客户重新购买时仍满足试用资格(trialOncePerCustomer=false),订阅会回到 TRIALING 而非 ACTIVE。

状态含义是否仍在履约
TRIALING试用中,只收了试用价是
ACTIVE生效中是
PAST_DUE周期已到期,仍在宽限期内(连续计费)是
CANCELLED已取消否
EXPIRED已过期否

TRIALING、ACTIVE、PAST_DUE 统称为生效订阅。只有生效订阅会阻止同一客户重复购买(singleSubscription=true 时),权益查询也只返回这些订阅。

周期与时间字段

字段含义
trialStartAt / trialEndAt试用起止;没有试用时为 null。
currentPeriodStartAt当前周期起点。
currentPeriodEndAt当前周期终点。试用期间它表示「转正后第一个周期」的终点,此时首期费用尚未收取。
paidThroughAt已付费覆盖到的终点。买断(LIFETIME)为 null。

周期长度按 UTC 计算:MONTHLY 使用 plusMonths(periodCount),YEARLY 使用 plusYears(periodCount)。

一条订阅通常经历以下阶段:

  1. 购买(PURCHASE):支付成功后开通订阅,写入首个周期与权益快照。命中试用时为 TRIALING,否则为 ACTIVE;买断订阅的周期字段为 null。
  2. 试用转正(TRIAL_CONVERT):客户在客户中心续费(试用期使用同一入口)并支付后转为 ACTIVE,首期从 trialEndAt 起算。
  3. 续费(RENEWAL):客户手动续费并支付后,覆盖终点顺延一个周期,状态保持或恢复为 ACTIVE。
  4. 到期:连续计费进入 PAST_DUE 并开始宽限期;预付一期直接 EXPIRED;已勾选到期取消则转为 CANCELLED。
  5. 计划变更(PLAN_CHANGE):支付完成(或免费变更)后立即生效,并从生效时刻重新开一期。
  6. 取消(CANCEL):客户勾选到期取消后 cancelAtPeriodEnd=true,当期继续有效,到期转为 CANCELLED。

续费与到期

Halo 不保存支付凭据,也不会自动扣款,任何续费都必须由客户主动发起:

  • 到期前 renewalLeadDays 天,Halo 发送一次 SUBSCRIPTION_RENEWAL_REMINDER 并给客户发提醒邮件;同一个到期时点只发一次,已勾选到期取消的订阅不发提醒。只有连续计费(billingMode=SUBSCRIPTION)的计划会发送提醒,预付一期到期直接过期、不提醒,买断没有到期。
  • 客户在客户中心点击续费,生成一张续费订单(连续计费)或转正订单(试用中),支付成功后周期与状态更新。
  • 续费、转正、变更订单与普通订单一致,24 小时未支付会自动过期;价格按下单时的现价计算,订阅上不保存价格快照。
  • 试用到期后不会自动生成转正订单;超过 trialEndAt 加宽限期(gracePeriodDays,最少 1 天)仍未支付即 EXPIRED。
  • 到期转移顺序:cancelAtPeriodEnd=true 优先转为 CANCELLED;否则连续计费进入 PAST_DUE,预付一期直接 EXPIRED。
代付

如需为客户钱包自动扣款,需要在客户登录态下发起续费(客户中心接口)取得订单,再由运营侧令牌调用 POST /orders/{id}/mark-as-paid 完成支付。Halo 当前没有面向接入方的「代客户创建续费单」接口。

计划变更

  • 只支持升级与平移,且两端必须是相同的 billingPeriod;降级与跨周期变更不被支持。
  • 每个方向都必须由运营显式配置一条启用中的变更规则(fromPlanId → toPlanId)。没有规则的方向一律拒绝,即使是升级。
  • 费用模式由规则决定:FREE(免费,立即生效且不产生订单)、FIXED_FEE(按固定单价 × 数量)、PRORATED(新一期全价减去当前计划未使用的剩余价值,基数为计划标价)。
  • 变更会重置周期:生效后 currentPeriodStartAt 为生效时刻,currentPeriodEndAt 与 paidThroughAt 为生效时刻加目标计划的周期。接入方必须使用事件或查询返回的新的 paidThroughAt 更新到期时间,不要沿用旧到期日,也不要按剩余天数顺延。
  • 变更不修改数量,数量沿用订阅当前值。
  • 订阅已过期或已取消、仍在试用期、存在待支付变更单时,变更会被拒绝。

取消

  • 客户在客户中心取消(默认到期取消):写入 cancelAtPeriodEnd=true,当期权益继续有效,到期时转为 CANCELLED。
  • 立即取消仅在该订阅已进入 PAST_DUE 时允许客户自助发起;运营可以在控制台对未终止的订阅立即取消。
  • 试用中的订阅勾选到期取消后,到期会按试用过期处理并进入 EXPIRED。

权益如何判定

entitlements 是运营与接入方约定的契约,Halo 只负责快照与下发,不解释也不强制执行。已付费覆盖终点不等于宽限期截止时间,不能仅用 paidThroughAt 或 currentPeriodEndAt 决定是否停用权益:

状态时间与权益处理
TRIALINGtrialEndAt 是试用覆盖终点;转正宽限内是否继续提供试用权益由接入方决定
ACTIVE已付费覆盖终点为 paidThroughAt,为空时回退 currentPeriodEndAt;到期时需核对当前状态与宽限策略
PAST_DUE已进入续费宽限期;保留宽限期访问时,截止时间为已付费覆盖终点加计划的 gracePeriodDays 天
买断(ACTIVE 且周期字段为 null)无时间终点,但取消后仍须停用权益
CANCELLED / EXPIRED停用权益,paidThroughAt 只是历史覆盖记录

宽限期内的访问

PAST_DUE 仍是生效订阅,权益查询会继续返回其契约。接入方可以保留访问或按业务规则限流;不要仅因为 paidThroughAt 已过就把它当作 EXPIRED。

2.27 调度器按 (paidThroughAt ?? currentPeriodEndAt) + gracePeriodDays × 24 小时 计算续费宽限截止时间,达到截止时间后转为 EXPIRED。gracePeriodDays 为空时按 0 计算;它来自订阅当前计划,并非订阅快照字段。需要计算截止时间时,先通过 planId 查询计划,不能假设 Webhook 或订阅详情已经包含宽限天数。

例如已付费覆盖到 10 月 16 日 00:00 UTC、计划宽限期为 3 天,保留宽限期访问的截止时间是 10 月 19 日 00:00 UTC。试用转正宽限的规则不同,为 trialEndAt + max(gracePeriodDays, 1) × 24 小时;在此期间状态仍为 TRIALING。

状态转换由定时任务执行,Webhook 也可能延迟或乱序。接入方应查询当前订阅及计划核对状态和截止时间,不要仅等待过期通知,也不要使用旧快照延长已取消订阅的权益。

需要注意:

  • TRIALING 期间 currentPeriodStartAt / currentPeriodEndAt 描述的是转正后的第一个周期,不能当作当前有效周期,直接使用会把到期时间显示成「试用结束加一个周期」。
  • 权益快照在购买、变更应用、续费时重算。运营修改档次的 entitlements 不会立即推送给已有订阅,要等下一次续费或变更。
  • 使用查询确认的当前快照整体更新本地权益,不要在本地累加周期或额度;Webhook 可能乱序,不能直接用迟到的事件快照覆盖当前状态,处理流程见幂等与顺序。

查询与对账接口

Console API(前缀 https://{host}/apis/console.api.ecommerce.halo.run/v1alpha1,需要管理员令牌):

方法路径用途
GET/subscriptions?customerId=&productId=&status=&page=&size=订阅列表
GET/subscriptions/{id}订阅详情,含 effectiveEntitlements 与 upcomingRenewal
GET/subscription-plans/{id}订阅当前计划,用 planId 查询,包含 gracePeriodDays 等计费配置
GET/customers/{customerId}/entitlements?productId=权益二次确认:返回 { customerId, productId, entitlements },没有生效订阅时 entitlements 为 {}
GET/subscription-changes?subscriptionId=变更流水,含 orderId、计价结果与前后快照
POST/subscriptions/{id}/cancel运营立即取消
POST/orders/{id}/mark-as-paid代付:把续费或变更订单标记为已支付
POST/orders/{id}/cancel取消未支付的续费或变更订单

客户中心 API(前缀 https://{host}/apis/uc.api.ecommerce.halo.run/v1alpha1,需要客户登录态)由 Halo 页面使用,接入方通常不需要直接调用;如需代客户发起,必须在客户登录态下调用:

方法路径说明
GET/subscriptions、/subscriptions/{id}我的订阅与详情
POST/subscriptions/{id}/changes/quote、/subscriptions/{id}/changes变更报价与执行
POST/subscriptions/{id}/cancel取消,请求体 {"immediate": false} 表示到期取消
POST/subscriptions/{id}/renew手动续费出单,试用期为转正单
GET/subscription-changes?subscriptionId=变更时间线
客户中心接口路径没有

/uc 段 客户中心接口的路径是 /apis/uc.api.ecommerce.halo.run/v1alpha1/subscriptions,不要再加一层 /uc。写成 /v1alpha1/uc/subscriptions 会因权限校验把首段路径当作资源名而返回 403。

完整契约

以上仅为常用接口。字段与状态码的完整定义以运行实例的 API 文档为准,也可以查阅 Halo 商城版 2.27 RESTful API 中的 Console 和 User-center 分组,对应 console.api.ecommerce.halo.run 与 uc.api.ecommerce.halo.run。