19 KiB
API-05 订阅系统(SUBSCRIPTION)
共 9 个接口:SUB-01~09
订阅档位枚举(含免费关注,最多 5 档;付费档与内容可见性 visibility 1-4 对齐)
| tier_level | API 字段值 | 说明 |
|---|---|---|
| 0 | free | 免费关注(粉丝无需付费即可订阅) |
| 1 | junior | 初级会员 |
| 2 | basic | 基础会员 |
| 3 | senior | 高级会员 |
| 4 | supreme | 至尊会员 |
创作者可按需启用 1-4 档付费档(必须连续);免费关注层(tier_level=0)始终存在且 price 必须为 0。免费档与付费档互斥:任一付费档启用时,免费档自动关闭;全部付费档关闭时,免费档自动开启。同一创作者订阅档位(含免费层)最多 5 行,付费档最多 4 个。 内容可见性 visibility=0(免费)/1(初级)/2(基础)/3(高级)/4(至尊) 对应订阅 tier_level(visibility=0 无需付费订阅,但仍需关注/订阅免费层)。
内容权限枚举(subscription_tier.permissions)
| 值 | 说明 |
|---|---|
| post | 动态(图文帖子) |
| video | 专属视频内容 |
| dm | 1对1私信聊天 |
| ppv_discount | PPV 内容享折扣 |
| live | 专属直播间 |
| custom | 专属定制素材 |
每个档位可多选 0~6 项;前端编辑层级弹框对应该字段。
钱包扣款乐观锁规范(适用于 SUB-02/03/05)
所有从钱包扣款的写入必须按以下方式执行(避免并发超扣):
-- 1. 先读取 wallet(取 balance + version)
SELECT balance, version FROM wallet WHERE user_id = :uid;
-- 2. 单条 UPDATE,受影响行数 = 0 时返回 409 由前端重试
UPDATE wallet
SET balance = balance - :amount,
total_expense = total_expense + :amount,
version = version + 1,
updated_at = NOW()
WHERE user_id = :uid
AND version = :old_version
AND balance >= :amount;
写入 wallet_transaction 流水必须与上述 UPDATE 在同一事务内。
时间戳字段命名规范
API 响应中的时间戳字段统一使用 At 后缀:
| 字段 | 说明 |
|---|---|
| startAt | 订阅开始时间 |
| expireAt | 订阅到期时间 |
| createdAt | 创建时间 |
对应 DB 字段使用
_at后缀(started_at、expired_at、created_at)。
订阅状态流转
无订阅 ──SUB-02──→ active(生效中)
│
SUB-03 升级 → 更高档位(即时生效)
SUB-04 降级 → 标记 next_tier_level(下周期生效)
SUB-08 取消 → 标记 auto_renew=0(到期后不续费)
│
到期 → grace_period(3天宽限期)
│
SUB-05 续费 → active
│
宽限期结束 → expired
SUB-01 获取创作者订阅档位
| 项目 | 说明 |
|---|---|
| 接口路径 | GET /client/api/v1/subscription/tiers/:creatorId |
| 接口用途 | 获取某创作者的订阅档位信息 |
| 权限要求 | 公开(游客可看价格) |
| 涉及表 | subscription_tier, user_subscription |
接口逻辑:
- 查询
subscription_tier表 WHERE creator_id=creatorId AND is_active=1 ORDER BY tier_level ASC(读,仅返回当前启用档位;若开启任意付费档,则不会返回 free) - 如果用户已登录,查询
user_subscription表 WHERE user_id=me AND creator_id=creatorId(读) - 组装当前订阅信息(currentSubscription),未订阅时为 null;若用户仍处于一个已关闭档位的有效周期内,仍需回填该档位名称
- 返回档位列表 + 当前订阅状态
路径参数:
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| creatorId | string | 是 | 创作者ID |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| tiers | array | 订阅档位列表(仅已开启的) |
| tiers[].tierId | string | 档位ID |
| tiers[].tierLevel | number | 档位等级:0=免费关注 / 1=初级 / 2=基础 / 3=高级 / 4=至尊 |
| tiers[].name | string | 档位名称 |
| tiers[].price | number | 月订阅价格(糖心币) |
| tiers[].description | string | 档位权益说明 |
| tiers[].permissions | string[] | 内容权限多选(来自 subscription_tier.permissions,枚举见顶部「内容权限枚举」) |
| tiers[].permissionLabels | string[] | 权限展示文案(按 permissions 顺序映射的中文标签) |
| currentSubscription | object/null | 当前订阅信息 |
| currentSubscription.subscriptionId | string | 订阅ID |
| currentSubscription.tierLevel | number | 当前档位等级 |
| currentSubscription.tierName | string | 当前档位名称 |
| currentSubscription.expireAt | timestamp | 到期时间 |
| currentSubscription.isActive | boolean | 是否有效 |
| currentSubscription.autoRenew | boolean | 是否自动续费 |
SUB-02 购买订阅
| 项目 | 说明 |
|---|---|
| 接口路径 | POST /client/api/v1/subscription/purchase |
| 接口用途 | 用户订阅某创作者的某个档位 |
| 权限要求 | 需登录 |
| 涉及表 | subscription_tier, subscription_order, user_subscription, wallet, wallet_transaction, revenue_share_record, follow |
接口逻辑:
- 查询
subscription_tier表 根据 tierId 获取档位信息和价格(读) - 幂等校验:查询
user_subscription表 检查是否已订阅同档位(WHERE user_id=me AND creator_id AND status=1)(读) - 查询
wallet表 检查 balance ≥ 档位价格(读)→ 不足返回422 - 扣款:更新
wallet表 的 balance -= price(原子操作,更新) - 创作者收入先按全额入钱包:更新创作者
wallet表的 balance/frozen_balance、total_income += 全额金额(更新) - 写入
subscription_order表(order_type=1新订阅,amount=price,subscription_tier_id=tierId)(写) - 写入/更新
user_subscription表(status=1生效中,设置 started_at、expired_at=+30天)(写) - 写入
wallet_transaction表(用户端 type=2订阅扣款 + 创作者端 type=5订阅收入,创作者端设置 frozen_until=当前时间+7天)(写) - 写入
revenue_share_record表(order_id=subscription_order._id, order_type=1, 记录三方分润明细 + 收入口径标记, settlement_status=1冻结中, available_at=当前时间+7天)(写) - 自动关注:查询
follow表 是否已关注,未关注则写入(写follow) - 更新
creator_profile表 的 subscriber_count+1(更新) - 返回订阅信息
关注/取关对称性说明: 订阅时自动关注(单向),但取消订阅/到期后不自动取关。关注是独立行为(免费),用户可通过 CREATOR-05 手动取关。
请求参数(Body):
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| creatorId | string | 是 | 创作者ID |
| tierId | string | 是 | 订阅档位ID |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| subscriptionId | string | 订阅ID |
| creatorId | string | 创作者ID |
| tierId | string | 档位ID |
| tierName | string | 档位名称 |
| price | number | 扣除的糖心币 |
| startAt | timestamp | 订阅开始时间 |
| expireAt | timestamp | 订阅到期时间 |
| remainingBalance | number | 扣费后余额 |
SUB-03 升级订阅
| 项目 | 说明 |
|---|---|
| 接口路径 | POST /client/api/v1/subscription/:subscriptionId/upgrade |
| 接口用途 | 从低档升到高档(即时生效,补差价) |
| 权限要求 | 需登录 |
| 涉及表 | user_subscription, subscription_tier, subscription_order, wallet, wallet_transaction |
接口逻辑:
- 查询
user_subscription表 根据 subscriptionId 获取当前订阅(读) - 校验 user_subscription.user_id = 当前用户
- 查询
subscription_tier表 获取当前档位和目标档位的价格(读) - 校验目标档位 > 当前档位(否则走降级)
- 计算补差价 = (高档日单价 - 低档日单价) × 剩余天数(向上取整)
- 查询
wallet表 检查 balance ≥ 补差价(读)→ 不足返回422 - 扣款:更新
wallet表 的 balance -= 补差价(原子操作,更新) - 创作者收入先按全额入钱包,同时保留分润明细用于提现结算
- 更新
user_subscription表 的 tier_level = 目标档位(更新) - 写入
subscription_order表(order_type=2升级,amount=补差价)(写) - 写入
wallet_transaction表(写) - 返回升级结果
路径参数:
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| subscriptionId | string | 是 | 当前订阅ID |
请求参数(Body):
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| targetTierId | string | 是 | 目标高档位ID |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| subscriptionId | string | 订阅ID |
| oldTierId | string | 原档位ID |
| newTierId | string | 新档位ID |
| oldTierLevel | number | 原档位等级 |
| newTierLevel | number | 新档位等级 |
| priceDiff | number | 补差价金额 |
| newExpireAt | timestamp | 到期时间(不变) |
| remainingBalance | number | 扣费后余额 |
SUB-04 降级订阅
| 项目 | 说明 |
|---|---|
| 接口路径 | POST /client/api/v1/subscription/:subscriptionId/downgrade |
| 接口用途 | 设置下个周期降级到低档(当前周期不变) |
| 权限要求 | 需登录 |
| 涉及表 | user_subscription |
接口逻辑:
- 查询
user_subscription表 根据 subscriptionId 获取当前订阅(读) - 校验 user_subscription.user_id = 当前用户
- 校验目标档位 < 当前档位
- 更新
user_subscription表 的 next_tier_level = 目标档位等级(更新) - 返回降级预告信息
路径参数:
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| subscriptionId | string | 是 | 当前订阅ID |
请求参数(Body):
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| targetTierId | string | 是 | 目标低档位ID |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| subscriptionId | string | 订阅ID |
| currentTierId | string | 当前档位ID |
| currentTierLevel | number | 当前档位等级 |
| pendingDowngradeTierLevel | number | 下周期降级到的档位等级 |
| pendingDowngradeTierId | string | 下周期降级到的档位ID |
| currentExpireAt | timestamp | 当前周期到期时间 |
SUB-05 手动续费
| 项目 | 说明 |
|---|---|
| 接口路径 | POST /client/api/v1/subscription/:subscriptionId/renew |
| 接口用途 | 订阅到期后用户手动续费 |
| 权限要求 | 需登录 |
| 涉及表 | user_subscription, subscription_tier, subscription_order, wallet, wallet_transaction |
接口逻辑:
- 查询
user_subscription表 根据 subscriptionId 获取订阅(读) - 确定续费档位:如果 next_tier_level 不为 null → 按降级后的档位续费,否则按当前 tier_level
- 查询
subscription_tier表 获取续费价格(读) - 校验 subscription_tier.is_active=1(创作者未关闭该档位)→ 关闭则返回422
- 查询
wallet表 检查 balance ≥ 续费价格(读) - 扣款:更新
wallet表(原子操作,更新) - 创作者收入先按全额入钱包,同时保留分润明细用于提现结算
- 更新
user_subscription表(tier_level=续费档位,status=1,expired_at=+30天,next_tier_level=null)(更新) - 写入
subscription_order表(order_type=4续费)(写) - 写入
wallet_transaction表(写) - 返回续费结果
路径参数:
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| subscriptionId | string | 是 | 订阅ID |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| subscriptionId | string | 订阅ID |
| tierId | string | 续费档位ID |
| tierLevel | number | 续费档位等级 |
| tierName | string | 续费档位名称 |
| price | number | 扣除的糖心币 |
| newExpireAt | timestamp | 新的到期时间 |
| remainingBalance | number | 扣费后余额 |
SUB-06 查询我的订阅列表
| 项目 | 说明 |
|---|---|
| 接口路径 | GET /client/api/v1/subscription/mine |
| 接口用途 | 查看用户当前订阅的创作者列表 |
| 权限要求 | 需登录 |
| 涉及表 | user_subscription, user, subscription_tier |
接口逻辑:
- 查询
user_subscription表 WHERE user_id=当前用户,可选按 status 筛选(读) - 关联查询
user表 获取创作者信息(profile.display_name、profile.avatar_url)(读) - 关联查询
subscription_tier表 获取档位名称和价格(读) - 分页返回
请求参数(Query):
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| status | string | 否 | 状态:active/expired |
| page | number | 否 | 页码 |
| pageSize | number | 否 | 每页条数 |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| list | array | 订阅列表 |
| list[].subscriptionId | string | 订阅ID |
| list[].creator | object | 创作者信息 |
| list[].creator.userId | string | 创作者ID |
| list[].creator.nickname | string | 创作者昵称 |
| list[].creator.avatar | url | 创作者头像 |
| list[].tierId | string | 档位ID |
| list[].tierLevel | number | 档位等级 |
| list[].tierName | string | 档位名称 |
| list[].price | number | 订阅价格 |
| list[].status | string | 状态:active/expired/grace_period |
| list[].startAt | timestamp | 开始时间 |
| list[].expireAt | timestamp | 到期时间 |
| list[].autoRenew | boolean | 是否自动续费 |
| list[].pendingDowngradeTierLevel | number/null | 下周期降级档位等级 |
| pagination | object | 分页信息 |
SUB-07 设置订阅档位(创作者)
| 项目 | 说明 |
|---|---|
| 接口路径 | PUT /client/api/v1/subscription/tiers/settings |
| 接口用途 | 创作者设置/修改订阅档位(含免费关注 + 1~4 个付费档)的名称、价格、权限、开关 |
| 权限要求 | 创作者 |
| 涉及表 | subscription_tier, user_subscription |
接口逻辑:
- 校验当前用户为创作者
- 校验: - 必须包含 tier=free(tier_level=0)且 price=0 - 付费档(tier_level 1-4)数量 ≤ 4,且启用的档位 tier_level 必须从 1 开始连续(不能跳级) - 免费档与付费档互斥:任一付费档启用时,后端自动关闭 free;全部付费档关闭时,后端自动开启 free - 每个档位 permissions 仅允许枚举值
- 遍历请求中的 tiers 数组:
- 查询/更新
subscription_tier表(WHERE creator_id=me AND tier_level=对应等级)(读+更新) - 如果记录不存在则创建(首次设置时写入对应记录)(写)
- 查询/更新
- 更新 is_active、price、tier_name、description、permissions 字段
- 统计各档位订阅人数:查询
user_subscription表 COUNT(WHERE creator_id=me AND tier_level=X AND status=1)(读) - 返回更新后的档位列表
删除档位语义: 客户端「删除」按钮等同于将该档位
enabled=false(软关闭,is_active=0)。已订阅该档位的用户保留至到期;关闭后用户无法再购买、续费或降级到该档位。免费档不单独手工开关,而是由后端按互斥规则自动开关。
请求参数(Body):
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| tiers | array | 是 | 档位设置数组(含 free 与 1~4 个付费档) |
| tiers[].tier | string | 是 | 档位标识:free/junior/basic/senior/supreme |
| tiers[].enabled | boolean | 是 | 是否开启;付费档按传值生效,free 的最终开关状态由后端按互斥规则自动归一 |
| tiers[].price | number | 是 | 月价格(糖心币,free=0;付费档 >0) |
| tiers[].name | string | 否 | 自定义档位名称(默认使用枚举名) |
| tiers[].description | string | 否 | 档位权益说明 |
| tiers[].permissions | string[] | 是 | 内容权限多选,枚举:post/video/dm/ppv_discount/live/custom |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| tiers | array | 更新后的档位列表 |
| tiers[].tierId | string | 档位ID |
| tiers[].tierLevel | number | 档位等级 |
| tiers[].name | string | 档位名称 |
| tiers[].price | number | 价格 |
| tiers[].description | string | 权益说明 |
| tiers[].enabled | boolean | 是否开启 |
| tiers[].permissions | string[] | 内容权限多选 |
| tiers[].subscriberCount | number | 当前订阅人数 |
SUB-09 创作者查看自己的档位列表
| 项目 | 说明 |
|---|---|
| 接口路径 | GET /client/api/v1/subscription/tiers/mine |
| 接口用途 | 创作者进入「订阅设置」页时拉取自己的全部档位(含未启用),用于回填编辑表单 |
| 权限要求 | 创作者 |
| 涉及表 | subscription_tier, user_subscription |
接口逻辑:
- 校验当前用户为创作者(role=2)
- 查询
subscription_tier表 WHERE creator_id=me(含 is_active=0)ORDER BY tier_level ASC(读) - 若免费层(tier_level=0)不存在,默认补一个空记录返回(不落库),方便前端首次进入即可渲染;其 enabled 状态应遵循当前互斥规则
- 对每个档位调用
TierSubscriberCount统计当前订阅人数(status=1) - 返回完整档位列表,每项包含 permissions/permissionLabels/subscriberCount
与 SUB-01 的区别:SUB-01 仅返回 is_active=1 的档位(粉丝视角);SUB-09 返回创作者本人全部档位含未启用项(创作者视角,用于编辑回填)。
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| tiers | array | 档位列表(含未启用) |
| tiers[].tierId | string | 档位ID(默认 free 占位行为 0) |
| tiers[].tierLevel | number | 档位等级 0/1/2/3/4 |
| tiers[].tierKey | string | free/junior/basic/senior/supreme |
| tiers[].name | string | 档位名称 |
| tiers[].price | number | 月价格 |
| tiers[].description | string | 权益说明 |
| tiers[].enabled | boolean | 是否启用 |
| tiers[].permissions | string[] | 内容权限多选 |
| tiers[].permissionLabels | string[] | 权限展示文案 |
| tiers[].subscriberCount | number | 当前订阅人数 |
SUB-08 取消订阅
| 项目 | 说明 |
|---|---|
| 接口路径 | POST /client/api/v1/subscription/:subscriptionId/cancel |
| 接口用途 | 取消自动续费,当前周期仍有效,到期后不再续费 |
| 权限要求 | 需登录 |
| 涉及表 | user_subscription |
接口逻辑:
- 查询
user_subscription表 根据 subscriptionId 获取订阅(读) - 校验 user_subscription.user_id = 当前用户
- 校验 status=1(生效中)→ 非生效状态返回422
- 更新
user_subscription表 的 auto_renew=0(更新) - 当前周期权益不受影响,到期后自动转为 expired
- 返回取消结果
注:取消订阅不会立即失效,也不会退款。用户仍可在到期前通过 SUB-05 手动续费恢复。
路径参数:
| 字段 | 类型 | 必选 | 说明 |
|---|---|---|---|
| subscriptionId | string | 是 | 订阅ID |
响应数据:
| 字段 | 类型 | 说明 |
|---|---|---|
| subscriptionId | string | 订阅ID |
| status | string | 状态:active(当前周期仍有效) |
| autoRenew | boolean | 自动续费状态(false) |
| expireAt | timestamp | 到期时间(届时失效) |