docs(api): update content-side ppv unlock docs

This commit is contained in:
qingfeng 2026-04-16 14:58:19 +08:00
parent b6b8afca9f
commit 24c5fae1ba
4 changed files with 105 additions and 44 deletions

View File

@ -114,16 +114,23 @@
|------|------|
| 接口路径 | GET /client/api/v1/media/:mediaId/signed-url |
| 接口用途 | 获取媒体文件的对外访问地址 |
| 权限要求 | 登录即可(内容层面的订阅/PPV 权限在内容接口中判定) |
| 涉及表 | media |
| 权限要求 | 需登录;是否返回真实地址由媒体所属内容的访问权限决定 |
| 涉及表 | media, post_media, post, user_subscription, ppv_unlock, content_unlock |
**接口逻辑:**
1. 查询 `media` 获取 `storage_path`、`type`、`process_status`
2. 若 `process_status != 2` → 返回 `40901 media 仍在处理中`
3. 根据 `type` 生成 URL
3. 判定当前用户是否有访问权限:
- 自己上传的媒体,直接允许
- 命中 IM 侧 `ppv_unlock`,允许
- 媒体关联到内容时:
- `free`:允许
- `subscriber`:校验有效订阅且 `tier_level >= requiredTierLevel`
- `ppv`:校验 `content_unlock`
4. 根据 `type` 生成 URL
- 图片:`img_cn_cdn + replaceExt(storage_path, ".bnc")`
- 视频:`movie_cn_cdn + storage_path + ?sign=md5(time-rand-apikey)&time=..&rand=..&uid=..`
4. 返回 URL视频签名有时效建议**即取即用**,不要缓存)
5. 返回结果;无权限时不返回 `signedUrl`,仅返回 `hasAccess=false` 及预览字段
**路径参数:**
@ -142,7 +149,7 @@
| mimeType | string | 文件MIME类型 |
| size | number | 文件大小(字节) |
| expiresAt | timestamp | 视频签名过期时间戳;图片字段为 0 |
| hasAccess | boolean | 保留字段,当前固定返回 true细粒度权限判断移至内容接口 |
| hasAccess | boolean | 当前用户是否有访问权限 |
| width | number | 媒体宽度(像素) |
| height | number | 媒体高度(像素) |
| duration | number | 视频时长(秒,图片为 0 |

View File

@ -21,23 +21,18 @@
| 3 | rejected | 审核拒绝 |
| 4 | removed | 已下架 |
### 可见性与订阅等级对应关系free + 4 档)
### 可见性与解锁关系
| visibility可见性 | DB值 | 所需 subscription_tier.tier_level | 说明 |
|---------------------|------|----------------------------------|------|
| free | 0 | 无需订阅 | 免费公开,所有用户可见 |
| junior | 1 | ≥ 1 | 初级会员及以上 |
| basic | 2 | ≥ 2 | 基础会员及以上 |
| senior | 3 | ≥ 3 | 高级会员及以上 |
| supreme | 4 | ≥ 4 | 至尊会员(独占)|
| visibilityAPI值 | DB值 | 额外字段 | 解锁规则 | 说明 |
|---------------------|------|----------|----------|------|
| free | 1 | 无 | 所有人可见 | 免费公开内容 |
| subscriber | 2 | `requiredTierLevel` | 当前用户存在有效订阅,且 `tier_level >= requiredTierLevel` | 订阅可见内容 |
| ppv | 3 | `price` | 作者本人或已存在 `content_unlock` 记录 | 内容单独付费解锁 |
> **判断逻辑:** `user_subscription.tier_level >= post.visibility` 且订阅状态有效,满足即有权限。`visibility=0` (free) 的内容无需检查订阅状态。
>
> **档位可配置说明:** 系统固定 4 个 tier_level 槽位,但每个创作者可在 SUB-07 中独立启用 1~4 档(必须从 level=1 起连续启用),并自定义档位名称、描述、价格。**因此创作者实际可选的 visibility 档位 = 该创作者已启用的 tier_level + free**。
>
> **App 发布表单约束CONTENT-01** 发布页加载时应先调用 **SUB-01** 拉取当前创作者自己已启用的档位列表(`GET /subscription/tiers/:creatorId`creatorId 传自己),按返回的 tier_level 渲染"可见档位"下拉选项;不应再硬编码 5 项。后端会校验提交的 `visibility` 必须为 `free` 或当前创作者已启用且 `is_active=true` 的 tier_level 对应字符串。
>
> **若创作者尚未配置任何档位**:仅允许选择 `free`;提交其它值返回 422。
> **发布约束:**
> 1. `visibility=subscriber` 时,必须额外传 `requiredTierLevel`,且该档位必须已在订阅设置中启用。
> 2. `visibility=ppv` 时,必须额外传 `price`,且 `price > 0`
> 3. 非 `ppv` 内容不允许传正数 `price`
### 关于评论commentCount
@ -61,8 +56,11 @@
4. 校验 tags1-3 个):每项 `id``name` 二选一。传 `id``tag` 表存在性校验;只传 `name` 时若同名 tag 已存在则复用,否则当场创建 source=2 的自定义标签(等价于内联 CONTENT-12
5. 校验 mentions查询 `user` 表 确认被 @ 用户存在
6. 若 type=2video且包含 highlights校验 time_offset_seconds < 视频时长
7. 校验 visibility若不是 `free`,查询 `subscription_tier` 表 WHERE creator_id=当前用户 AND tier_level=映射值 AND is_active=true不存在则返回 422"该档位未启用"
8. 写入 `post`status=isDraft?0:1写入 location_name/codevisibility 存映射后的 int 值;有 `coverMediaId` 时写入 `post.cover_media_id`,否则回退到首个内容媒体)
7. 校验可见性:
- `visibility=subscriber`:校验 `requiredTierLevel` 必填,且该档位在当前创作者的 `subscription_tier` 中存在且 `is_active=true`
- `visibility=ppv`:校验 `price > 0`
- 其它可见性不允许传正数 `price`
8. 写入 `post`status=isDraft?0:1写入 location_name/code、visibility、required_tier_level、price_cents`coverMediaId` 时写入 `post.cover_media_id`,否则回退到首个内容媒体)
9. 写入关系表post_media / post_tag / post_mention / post_highlight
10. 草稿isDraft=true跳过审核流程非草稿进入审核队列
11. 返回新创建的内容信息
@ -72,7 +70,7 @@
| 字段 | 类型 | 必选 | 说明 |
|------|------|------|------|
| title | string | 否 | 标题,最长 30 字 |
| content | string | | 正文内容,最长 200 字 |
| content | string | | 正文内容,最长 200 字 |
| type | string | 是 | 内容类型post/video |
| mediaIds | int64[] | 否 | 媒体ID数组图片最多9张或1个视频 |
| coverMediaId | string | 否 | 显式封面图媒体ID。`type=video` 时必填,必须是当前创作者自己的已处理完成图片媒体;其它类型可不传 |
@ -83,14 +81,16 @@
| location | object | 否 | 地点信息 |
| location.name | string | 否 | 地点显示名(如"上海 静安区"),最长 64 字 |
| location.code | string | 否 | 行政区划代码GB/T 2260最长 20 字 |
| visibility | string | 是 | 可见性:`free` 或当前创作者已启用的档位 keyjunior/basic/senior/supreme 之一)。后端会校验该 key 对应的 tier_level 在当前创作者的 `subscription_tier` 中存在且 `is_active=true`,否则返回 422 |
| visibility | string | 是 | 可见性:`free` / `subscriber` / `ppv` |
| requiredTierLevel | number | 否 | `visibility=subscriber` 时必填,表示最低可见订阅档位 |
| price | number | 否 | `visibility=ppv` 时必填,单位分;其它可见性不传或传 0 |
| highlights | array | 否 | 视频章节标记数组type=video 时),用于在播放进度条上打点跳转 |
| highlights[].timeOffsetSeconds | int | 是 | 标记时间点(秒),必须 < 视频时长 |
| highlights[].label | string | 否 | 标记说明文字(最长 40 字) |
| isDraft | boolean | 否 | 是否保存为草稿,默认 false |
**校验返回码:**
- 422: 字段超长 / 标签数量不在 1-3 / 媒体处理未完成 / highlights 时间点超出视频时长 / visibility 不在当前创作者已启用档位内
- 422: 字段超长 / 标签数量不在 1-3 / 媒体处理未完成 / highlights 时间点超出视频时长 / subscriber 档位未启用
- 403: 非创作者 / 媒体不属于当前用户
- 404: 标签/被@用户不存在
@ -106,8 +106,10 @@
| tags | object[] | 标签列表 [{id,name,source}] |
| mentions | object[] | 被 @ 用户列表 [{userId,nickname}] |
| location | object | 地点信息 |
| visibility | string | 可见性档位 keyfree/junior/basic/senior/supreme |
| visibilityTierName | string | 可见档位的展示名创作者自定义名称free 时为 null |
| visibility | string | 可见性free / subscriber / ppv |
| requiredTierLevel | number | subscriber 内容的最低可见档位 |
| visibilityTierName | string | subscriber 内容对应档位的展示名free/ppv 时为空) |
| visibilityTierPrice | number | subscriber 内容对应档位价格free/ppv 时为 0 |
| highlights | array | 视频章节标记列表 |
| status | string | 状态draft / reviewing |
| isDraft | boolean | 是否草稿 |
@ -138,7 +140,10 @@
5. 关联查询 `media` 表 获取封面图/缩略图(读)。优先使用 `post.cover_media_id` 对应媒体生成 `coverUrl`;未设置时回退到内容首张图片
6. 关联查询 `content_boost` 表 获取内容注水数据,外显数据 = 真实值 + boost值
> 注:`content_boost` 用于单条内容的注水view/like/favorite`creator_boost` 用于创作者主页的注水follower/subscriber后者在 API-08 CREATOR-02 中使用。
7. 如果当前用户已登录,批量查询 `user_subscription` 表(按本页 author_id 列表 + 当前 viewer_id 一次查齐,避免 N+1逐条按 `visibility=0 OR (订阅有效 AND user_subscription.tier_level >= post.visibility)` 计算 `isLocked`。同时关联 `subscription_tier``name` 作为 `visibilityTierName` 回填(读)
7. 如果当前用户已登录,批量查询 `user_subscription``content_unlock`
- `subscriber` 内容按 `tier_level >= requiredTierLevel` 判断是否解锁
- `ppv` 内容仅按 `content_unlock` 判断是否已购
- 返回 `isLocked / isUnlocked / ppvPrice`
8. 分页返回内容卡片列表
**请求参数Query**
@ -162,10 +167,12 @@
| list[].blurCoverUrl | url | 模糊封面URL |
| list[].type | string | 类型post/video |
| list[].duration | number | 视频时长。type 为 video 时返回,非视频或无媒体时省略 |
| list[].videoUrl | url | 视频播放签名URL。type 为 video 且 `isLocked=false` 时返回,锁定内容不返回 |
| list[].visibility | string | 可见性档位 keyfree/junior/basic/senior/supreme |
| list[].visibilityTierName | string | 该档位在当前创作者下的展示名free 时为 null |
| list[].visibilityTierPrice | number | 该档位价格free 时省略 |
| list[].videoUrl | url | 视频播放签名URL。广场/关注 Feed 中仅在 `isLocked=false` 时返回 |
| list[].visibility | string | 可见性free / subscriber / ppv |
| list[].visibilityTierName | string | subscriber 内容的档位展示名free/ppv 时为空) |
| list[].visibilityTierPrice | number | subscriber 内容档位价格free/ppv 时为 0 |
| list[].ppvPrice | number | `visibility=ppv` 时返回内容价格(分) |
| list[].isUnlocked | boolean | 当前用户是否已解锁 |
| list[].isPinned | boolean | 是否置顶 |
| list[].hasHighlights | boolean | 是否包含视频章节标记(视频类型时) |
| list[].creator | object | 创作者信息 |
@ -223,7 +230,8 @@
5. 从 `post` 中查询同上下文的候选视频流:`status=2 AND type=video`,并排除 `seedContentId`
6. `offset=0` 时,返回列表首条固定插入 seed 视频,其余 `pageSize-1` 条由候选视频补足
7. `offset>0` 时,不再重复返回 seed 视频,只返回后续候选视频
8. 列表项组装逻辑与 CONTENT-02 一致,复用 `FeedItem` 字段口径(封面、时长、视频地址、锁态、创作者信息等)
8. 列表项组装逻辑与 CONTENT-02 一致,复用 `FeedItem` 字段口径(封面、时长、锁态、创作者信息等)
9. 为兼容沉浸式播放器预加载,`video-feed` 会补充 `videoUrl`;前端应以 `isLocked / isUnlocked` 判断是否真正解锁
**请求参数Query**
@ -274,6 +282,7 @@
8. 查询 `post_like` 表 检查 isLikedWHERE user_id=me AND post_id=contentId
9. 查询 `post_favorite` 表 检查 isFavorited
10. 组装完整响应,并返回当前内容 `status`
11. `subscriber` 内容按有效订阅 + `requiredTierLevel` 判断解锁;`ppv` 内容只认 `content_unlock`,不再因为存在任意订阅而放行
**路径参数:**
@ -305,10 +314,12 @@
| videoWidth | number | 视频宽度 |
| videoHeight | number | 视频高度 |
| tags | string[] | 标签名称列表 |
| visibility | string | 可见性档位 keyfree/junior/basic/senior/supreme |
| visibilityTierName | string | 该档位在内容作者下的展示名创作者自定义free 时为 null |
| visibilityTierPrice | number | 该档位月价代币free 时为 0— 用于无权限时引导订阅 |
| isUnlocked | boolean | 当前用户是否已解锁(基于 user_subscription.tier_level ≥ post.visibility 且订阅有效) |
| visibility | string | 可见性free / subscriber / ppv |
| requiredTierLevel | number | subscriber 内容的最低可见档位 |
| visibilityTierName | string | subscriber 内容的档位展示名free/ppv 时为空) |
| visibilityTierPrice | number | subscriber 内容的档位价格free/ppv 时为 0 |
| ppvPrice | number | `visibility=ppv` 时返回内容价格(分) |
| isUnlocked | boolean | 当前用户是否已解锁ppv 内容仅按 `content_unlock` 判断 |
| isPinned | boolean | 是否置顶 |
| viewCount | number | 浏览量 |
| likeCount | number | 点赞数 |
@ -320,6 +331,41 @@
---
#### CONTENT-03A 解锁 PPV 内容
| 项目 | 说明 |
|------|------|
| 接口路径 | POST /client/api/v1/content/:contentId/unlock |
| 接口用途 | 解锁内容侧 PPV 内容 |
| 权限要求 | 需登录 |
| 涉及表 | post, content_unlock, wallet, wallet_transaction, revenue_share_record |
**接口逻辑:**
1. 查询并锁定 `post`
2. 校验内容存在、已发布且 `visibility=ppv`
3. 校验当前用户不是作者本人
4. 查询 `content_unlock` 做幂等判断:
- 已解锁:直接返回成功,`amount=0`
- 未解锁:按 `post.price_cents` 扣款、写入 `content_unlock`、写入分润记录
5. 返回最新余额与解锁结果
**路径参数:**
| 字段 | 类型 | 必选 | 说明 |
|------|------|------|------|
| contentId | string | 是 | 内容 ID |
**响应数据:**
| 字段 | 类型 | 说明 |
|------|------|------|
| contentId | string | 内容 ID |
| amount | number | 本次实际扣费金额(分);幂等命中时为 0 |
| remainingBalance | number | 扣费后的钱包余额 |
| isUnlocked | boolean | 是否已解锁,固定返回 true |
---
#### CONTENT-04 获取预设标签列表
| 项目 | 说明 |
@ -415,7 +461,7 @@
- `DELETE FROM post_tag WHERE post_id = :contentId`
- 批量 `INSERT INTO post_tag (post_id, tag_id) VALUES ...`
4. 如果传 mediaIds同样 `DELETE FROM post_media WHERE post_id = :contentId` 然后批量 INSERT 重建
5. 更新 `post` 表 的 title/content/visibility/category_id 等字段(更新)
5. 更新 `post` 表 的 title/content/visibility/required_tier_level/price_cents 等字段(更新)
6. 返回更新后的内容信息
**路径参数:**
@ -433,7 +479,9 @@
| tags | object[] | 否 | 标签数组1-3 个;结构同 CONTENT-01每项 `id``name` 二选一,仅传 `name` 时同名复用或新建自定义标签 |
| mentions | string[] | 否 | 被 @ 用户的 userId 数组 |
| location | object | 否 | 地点 {name, code},传 null 清空 |
| visibility | string | 否 | 可见性档位 key`free` 或当前创作者已启用的档位之一(同 CONTENT-01 校验规则),不传则保持原值 |
| visibility | string | 否 | 可见性:`free` / `subscriber` / `ppv`;不传则保持原值 |
| requiredTierLevel | number | 否 | `visibility=subscriber` 时必填;不传则保持原值 |
| price | number | 否 | `visibility=ppv` 时必填且 > 0其它可见性传 0 或不传 |
| highlights | array | 否 | 视频撸点(仅视频类型) |
| publishDraft | boolean | 否 | 草稿提交发布true 时把 status 从 0 改为 1 进入审核 |

View File

@ -142,7 +142,10 @@
- `private`:仅返回 `visibility >= 1`(付费档内容)
3. 关联查询 `media` 表 获取封面图(读)
4. 查询 `content_boost` 表 获取注水数据(读)
5. 如果用户已登录:批量查询 `user_subscription` 判断订阅状态,按 `visibility=0 OR tier_level>=post.visibility` 决定 `isLocked`(读)
5. 如果用户已登录:批量查询 `user_subscription``content_unlock`
- `subscriber` 内容按 `tier_level >= requiredTierLevel` 判断是否解锁
- `ppv` 内容按 `content_unlock` 判断是否已购
- 返回 `isLocked / isUnlocked / ppvPrice`
6. 按 created_at 倒序,分页返回
**路径参数:**
@ -189,14 +192,16 @@
| list[].videos[].height | number | 视频高度 |
| list[].videos[].duration | number | 视频时长(秒) |
| list[].videos[].isLocked | boolean | 单条视频是否锁定 |
| list[].visibility | string | 可见性档位 keyfree/junior/basic/senior/supreme |
| list[].visibilityTierName | string/null | 该档位在当前创作者下的展示名 |
| list[].visibilityTierPrice | number | 该档位价格(代币) |
| list[].visibility | string | 可见性free / subscriber / ppv |
| list[].requiredTierLevel | number | subscriber 内容的最低可见档位 |
| list[].visibilityTierName | string/null | subscriber 内容的档位展示名free/ppv 时为空) |
| list[].visibilityTierPrice | number | subscriber 内容的档位价格free/ppv 时为 0 |
| list[].ppvPrice | number | `visibility=ppv` 时返回内容价格(分) |
| list[].creator | object | 创作者信息 `{userId,username,nickname,avatar,isOnline,isFollowing}` |
| list[].isUnlocked | boolean | 当前用户是否已解锁该内容 |
| list[].isLiked | boolean | 当前用户是否已点赞 |
| list[].isFavorited | boolean | 当前用户是否已收藏 |
| list[].isLocked | boolean | 是否需要订阅才能查看(锁定态展示「订阅查看全文」CTA |
| list[].isLocked | boolean | 是否仍需订阅/购买才能查看(锁定态展示对应 CTA |
| list[].favoriteCount | number | 收藏数 |
| list[].likeCount | number | 点赞数 |
| list[].commentCount | number | 评论数 |

View File

@ -72,6 +72,7 @@
- CONTENT-02b 获取关注 Feed — `GET /client/api/v1/content/feed/following`
- CONTENT-02c 获取视频播放流 — `GET /client/api/v1/content/video-feed`
- CONTENT-03 获取内容详情 — `GET /client/api/v1/content/:contentId`
- CONTENT-03A 解锁 PPV 内容 — `POST /client/api/v1/content/:contentId/unlock`
- CONTENT-04 获取预设标签列表 — `GET /client/api/v1/content/tags`
- CONTENT-05 内容管理列表(创作者) — `GET /client/api/v1/content/manage`
- CONTENT-06 编辑内容 — `PUT /client/api/v1/content/:contentId`