902 lines
36 KiB
Markdown
902 lines
36 KiB
Markdown
# API-04 内容系统(CONTENT)
|
||
|
||
> 共 17 个接口:CONTENT-01~17
|
||
|
||
### 内容类型枚举(post.type)
|
||
|
||
| DB值 | API值 | 说明 |
|
||
|------|------|------|
|
||
| 1 | post | 动态(图文,1-9 张图)|
|
||
| 2 | video | 视频(不区分时长)|
|
||
|
||
> PPV 素材**不在此枚举**,独立走 API-06 PPV 系统(数据模型完全不同)。发布页 UI 上"发布 PPV 素材"按钮直接跳转到 PPV 创建流程。
|
||
|
||
### 状态枚举映射(post.status)
|
||
|
||
| DB值 | API响应值 | 说明 |
|
||
|------|----------|------|
|
||
| 0 | draft | 草稿(仅创作者本人可见) |
|
||
| 1 | reviewing | 审核中 |
|
||
| 2 | published | 已上架 |
|
||
| 3 | rejected | 审核拒绝 |
|
||
| 4 | removed | 已下架 |
|
||
|
||
### 可见性与解锁关系
|
||
|
||
| visibility(API值) | DB值 | 额外字段 | 解锁规则 | 说明 |
|
||
|---------------------|------|----------|----------|------|
|
||
| free | 1 | 无 | 所有人可见 | 免费公开内容 |
|
||
| subscriber | 2 | `requiredTierLevel` | 当前用户存在有效订阅,且 `tier_level >= requiredTierLevel` | 订阅可见内容 |
|
||
| ppv | 3 | `price` | 作者本人或已存在 `content_unlock` 记录 | 内容单独付费解锁 |
|
||
|
||
> **发布约束:**
|
||
> 1. `visibility=subscriber` 时,必须额外传 `requiredTierLevel`,且该档位必须已在订阅设置中启用。
|
||
> 2. `visibility=ppv` 时,必须额外传 `price`,且 `price > 0`。
|
||
> 3. 非 `ppv` 内容不允许传正数 `price`。
|
||
|
||
### 关于评论(commentCount)
|
||
|
||
> 评论系统已在 P1 阶段上线,对应接口为 CONTENT-13~17。`commentCount` 由 `post.comment_count` 字段实时返回。
|
||
|
||
---
|
||
|
||
#### CONTENT-01 发布内容(含草稿)
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | POST /client/api/v1/content |
|
||
| 接口用途 | 创作者发布动态/视频,或保存为草稿 |
|
||
| 权限要求 | 创作者(user.role=2) |
|
||
| 涉及表 | post, post_media, post_tag, post_mention, post_highlight, media, tag |
|
||
|
||
**接口逻辑:**
|
||
1. 校验当前用户 user.role=2(创作者权限)
|
||
2. 校验 mediaIds:查询 `media` 表 确认 process_status=2 且 user_id=当前用户
|
||
3. 若 `type=video`:校验 `coverMediaId` 必传,且对应 `media` 必须为当前创作者自己的已处理完成图片媒体;非视频内容可不传,传了也按同样规则校验
|
||
4. 校验 tags(1-3 个):每项 `id` 与 `name` 二选一。传 `id` 走 `tag` 表存在性校验;只传 `name` 时若同名 tag 已存在则复用,否则当场创建 source=2 的自定义标签(等价于内联 CONTENT-12)
|
||
5. 校验 mentions:查询 `user` 表 确认被 @ 用户存在
|
||
6. 若 type=2(video)且包含 highlights:校验 time_offset_seconds < 视频时长
|
||
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. 返回新创建的内容信息
|
||
|
||
**请求参数(Body):**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| title | string | 否 | 标题,最长 30 字 |
|
||
| content | string | 是 | 正文内容,最长 200 字 |
|
||
| type | string | 是 | 内容类型:post/video |
|
||
| mediaIds | int64[] | 否 | 媒体ID数组(图片最多9张,或1个视频) |
|
||
| coverMediaId | string | 否 | 显式封面图媒体ID。`type=video` 时必填,必须是当前创作者自己的已处理完成图片媒体;其它类型可不传 |
|
||
| tags | object[] | 是 | 标签数组,1-3 个;每项 `id` 与 `name` 二选一 |
|
||
| tags[].id | string | 否 | 已存在的标签 ID(系统预设或已建好的自定义标签) |
|
||
| tags[].name | string | 否 | 仅在不传 `id` 时使用:同名复用,否则新建自定义标签(source=2),长度 1~32 |
|
||
| mentions | string[] | 否 | 被 @ 用户的 userId 数组,最多 10 个 |
|
||
| location | object | 否 | 地点信息 |
|
||
| location.name | string | 否 | 地点显示名(如"上海 静安区"),最长 64 字 |
|
||
| location.code | string | 否 | 行政区划代码(GB/T 2260),最长 20 字 |
|
||
| 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 时间点超出视频时长 / subscriber 档位未启用
|
||
- 403: 非创作者 / 媒体不属于当前用户
|
||
- 404: 标签/被@用户不存在
|
||
|
||
**响应数据:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| contentId | string | 内容ID |
|
||
| title | string | 标题 |
|
||
| content | string | 正文 |
|
||
| type | string | 内容类型 |
|
||
| mediaIds | int64[] | 媒体ID列表 |
|
||
| tags | object[] | 标签列表 [{id,name,source}] |
|
||
| mentions | object[] | 被 @ 用户列表 [{userId,nickname}] |
|
||
| location | object | 地点信息 |
|
||
| 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 | 是否草稿 |
|
||
| createdAt | timestamp | 创建时间 |
|
||
|
||
---
|
||
|
||
#### CONTENT-02 获取广场 Feed
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | GET /client/api/v1/content/feed |
|
||
| 接口用途 | 获取广场信息流(双列瀑布流) |
|
||
| 权限要求 | 公开(游客可访问) |
|
||
| 涉及表 | post, post_tag, post_media, tag, category, category_tag, user, media, content_boost, recommend_slot |
|
||
|
||
**接口逻辑:**
|
||
1. 根据 sort 参数选择排序策略:
|
||
- recommend:70%算法推荐 + 20%新创作者池 + 10%运营位(查询 `recommend_slot` 表,读)
|
||
- latest:按 post.created_at 倒序
|
||
- hot:按浏览量+点赞数加权排序
|
||
2. 处理分类筛选(categoryId 与 tag 二选一):
|
||
- 如果传 categoryId:`SELECT post.* FROM post JOIN post_tag pt ON pt.post_id = post.id JOIN category_tag ct ON ct.tag_id = pt.tag_id WHERE ct.category_id = :categoryId`(读)
|
||
- 如果传 tag:`SELECT post.* FROM post JOIN post_tag pt ON pt.post_id = post.id JOIN tag t ON t.id = pt.tag_id WHERE t.name = :tag`(读)
|
||
- 如果都不传:不加标签过滤,返回全部
|
||
3. 查询 `post` 表 WHERE status=2(已上架)(读)
|
||
4. 关联查询 `user` 表 获取创作者信息(profile.display_name、profile.avatar_url)(读)
|
||
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` 与 `content_unlock`:
|
||
- `subscriber` 内容按 `tier_level >= requiredTierLevel` 判断是否解锁
|
||
- `ppv` 内容仅按 `content_unlock` 判断是否已购
|
||
- 返回 `isLocked / isUnlocked / ppvPrice`
|
||
8. 分页返回内容卡片列表
|
||
|
||
**请求参数(Query):**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| sort | string | 否 | 排序方式:recommend/latest/hot,默认recommend |
|
||
| categoryId | string | 否 | 分类ID(首页 Tab 分类筛选,内部通过 `category_tag` JOIN `post_tag` 匹配帖子) |
|
||
| tag | string | 否 | 标签筛选(tag.name),与 categoryId 二选一 |
|
||
| page | number | 否 | 页码,默认1 |
|
||
| pageSize | number | 否 | 每页条数,默认20 |
|
||
|
||
**响应数据:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| list | array | 内容卡片列表 |
|
||
| list[].contentId | string | 内容ID |
|
||
| list[].title | string | 标题 |
|
||
| list[].coverUrl | url | 封面图URL |
|
||
| list[].blurCoverUrl | url | 模糊封面URL |
|
||
| list[].type | string | 类型:post/video |
|
||
| list[].duration | number | 视频时长(秒)。type 为 video 时返回,非视频或无媒体时省略 |
|
||
| 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 | 创作者信息 |
|
||
| list[].creator.userId | string | 创作者ID |
|
||
| list[].creator.nickname | string | 创作者昵称 |
|
||
| list[].creator.avatar | url | 创作者头像 |
|
||
| list[].creator.isOnline | boolean | 是否在线 |
|
||
| list[].viewCount | number | 浏览量(含boost) |
|
||
| list[].likeCount | number | 点赞数(含boost) |
|
||
| list[].favoriteCount | number | 收藏数(含boost) |
|
||
| list[].commentCount | number | 评论数 |
|
||
| list[].isLocked | boolean | 是否需订阅/付费 |
|
||
| list[].tags | object[] | 标签列表 [{id,name}] |
|
||
| list[].createdAt | timestamp | 发布时间 |
|
||
| pagination | object | 分页信息 |
|
||
|
||
---
|
||
|
||
#### CONTENT-02b 获取关注 Feed
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | GET /client/api/v1/content/feed/following |
|
||
| 接口用途 | 首页"关注" tab:返回当前用户已关注创作者发布的内容 |
|
||
| 权限要求 | 需登录(JWT) |
|
||
| 涉及表 | user_follow, post, post_media, media, user_profile, subscription_tier, user_subscription |
|
||
|
||
**接口逻辑:**
|
||
1. 从 JWT 取 viewer_id
|
||
2. 查询 `user_follow` 表,取当前用户全部 `followee_id`(`deleted_at IS NULL`);若为空直接返回空列表
|
||
3. 调用与 CONTENT-02 相同的 `ListFeed`,在 `post` 上追加 `author_id = ANY($followeeIds)` 过滤,status=2,按 published_at 倒序
|
||
4. 组装逻辑与 CONTENT-02 第 4~7 步一致(作者资料、媒体封面/时长、订阅档位、isLocked 计算)
|
||
5. 分页返回
|
||
|
||
**请求参数(Query):** 与 CONTENT-02 一致(sort/categoryId/tag/page/pageSize)
|
||
|
||
**响应数据:** 与 CONTENT-02 `ContentFeedResp` 完全一致(list[] 为 FeedItem)
|
||
|
||
---
|
||
|
||
#### CONTENT-02c 获取视频播放流
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | GET /client/api/v1/content/video-feed |
|
||
| 接口用途 | 从首页/分类页点击某个视频后,进入沉浸式视频流;返回列表首条为当前点击的视频 |
|
||
| 权限要求 | 公开(游客可访问) |
|
||
| 涉及表 | post, post_tag, tag, category, post_media, media, user_profile, subscription_tier, user_subscription |
|
||
|
||
**接口逻辑:**
|
||
1. 根据 `seedContentId` 查询种子内容;不存在返回 40401
|
||
2. 校验种子内容必须为 `video`,否则返回 40001
|
||
3. 校验种子内容状态必须为已发布;未发布/已下架按不存在处理
|
||
4. 复用广场 Feed 的上下文参数:`sort/categoryId/tag`
|
||
5. 从 `post` 中查询同上下文的候选视频流:`status=2 AND type=video`,并排除 `seedContentId`
|
||
6. `offset=0` 时,返回列表首条固定插入 seed 视频,其余 `pageSize-1` 条由候选视频补足
|
||
7. `offset>0` 时,不再重复返回 seed 视频,只返回后续候选视频
|
||
8. 列表项组装逻辑与 CONTENT-02 一致,复用 `FeedItem` 字段口径(封面、时长、锁态、创作者信息等)
|
||
9. 为兼容沉浸式播放器预加载,`video-feed` 会补充 `videoUrl`;前端应以 `isLocked / isUnlocked` 判断是否真正解锁
|
||
|
||
**请求参数(Query):**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| seedContentId | string | 是 | 当前点击的视频内容 ID |
|
||
| sort | string | 否 | 排序方式:recommend/latest/hot,默认 recommend |
|
||
| categoryId | string | 否 | 分类 ID,沿用首页/分类页上下文 |
|
||
| tag | string | 否 | 标签筛选,沿用来源页上下文 |
|
||
| offset | number | 否 | 候选视频偏移量,默认 0 |
|
||
| pageSize | number | 否 | 每页条数,默认 20,最大 50 |
|
||
|
||
**响应数据:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| list | array | 视频播放流列表,`list[0]` 在 `offset=0` 时固定为 seed 视频 |
|
||
| list[] | FeedItem | 字段结构同 CONTENT-02 |
|
||
| nextOffset | number | 下一次请求应继续使用的 offset |
|
||
| hasMore | boolean | 是否还有后续视频 |
|
||
|
||
---
|
||
|
||
#### CONTENT-03 获取内容详情
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | GET /client/api/v1/content/:contentId |
|
||
| 接口用途 | 获取单条内容详情 |
|
||
| 权限要求 | 公开;作者本人带登录态时可查看自己未下架的草稿/审核中/拒绝内容 |
|
||
| 涉及表 | post, post_tag, tag, post_media, media, user_profile, post_like, post_favorite, subscription_tier, user_subscription |
|
||
|
||
**接口逻辑:**
|
||
1. 查询 `post` 表 根据 contentId 获取内容(读)
|
||
2. 访问控制:
|
||
- `status=published`:公开可访问
|
||
- `status!=published`:仅作者本人带登录态且内容未下架(`status!=removed`)时可访问
|
||
- 其它情况统一返回 `40401 内容不存在`
|
||
3. 浏览计数:
|
||
- 仅 `published` 内容计入公开浏览量
|
||
- 作者查看自己的草稿/审核中/拒绝内容不增加 `viewCount`
|
||
4. 关联查询 `user_profile` 表 获取创作者信息(读)
|
||
5. 查询 `media` 表 获取关联媒体信息(读)
|
||
6. 查询 `user_subscription` 表 判断当前用户订阅状态(读):
|
||
- **有权限**:返回图片原图 URL / 视频播放 URL
|
||
- **无权限**:图片不返回原图 URL,仅返回 `blurUrl`;视频不返回 `videoUrl`,返回 `videoBlurPreviewUrl`
|
||
7. 查询 `subscription_tier` 表 回填当前内容的 `visibilityTierName/visibilityTierPrice`(读)
|
||
8. 查询 `post_like` 表 检查 isLiked(WHERE user_id=me AND post_id=contentId)(读)
|
||
9. 查询 `post_favorite` 表 检查 isFavorited(读)
|
||
10. 组装完整响应,并返回当前内容 `status`
|
||
11. `subscriber` 内容按有效订阅 + `requiredTierLevel` 判断解锁;`ppv` 内容只认 `content_unlock`,不再因为存在任意订阅而放行
|
||
|
||
**路径参数:**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| contentId | string | 是 | 内容ID |
|
||
|
||
**响应数据:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| contentId | string | 内容ID |
|
||
| creator | object | 创作者信息 `{userId,username,nickname,avatar,isOnline,isFollowing}` |
|
||
| title | string | 标题 |
|
||
| content | string | 正文 |
|
||
| type | string | 内容类型 post/video |
|
||
| status | string | 内容状态:draft / reviewing / published / rejected / removed |
|
||
| images | array | 图片列表(图文内容返回;视频内容通常为空数组) |
|
||
| images[].mediaId | string | 媒体ID |
|
||
| images[].url | url | 原图签名URL(已解锁时返回) |
|
||
| images[].blurUrl | url | 模糊图URL(未解锁时返回) |
|
||
| images[].thumbnailUrl | url | 缩略图URL |
|
||
| images[].width | number | 图片宽度 |
|
||
| images[].height | number | 图片高度 |
|
||
| images[].isLocked | boolean | 是否锁定 |
|
||
| videoUrl | url | 视频签名URL(已解锁时返回) |
|
||
| videoBlurPreviewUrl | url | 视频预览字段(未解锁时返回) |
|
||
| videoCoverUrl | url | 视频封面URL |
|
||
| videoWidth | number | 视频宽度 |
|
||
| videoHeight | number | 视频高度 |
|
||
| tags | string[] | 标签名称列表 |
|
||
| 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 | 点赞数 |
|
||
| favoriteCount | number | 收藏数 |
|
||
| commentCount | number | 评论数 |
|
||
| isLiked | boolean | 是否已点赞 |
|
||
| isFavorited | boolean | 是否已收藏 |
|
||
| createdAt | timestamp | 发布时间 |
|
||
|
||
---
|
||
|
||
#### 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 获取预设标签列表
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | GET /client/api/v1/content/tags |
|
||
| 接口用途 | 获取平台标签(系统预设 + 用户自定义热门标签) |
|
||
| 权限要求 | 公开 |
|
||
| 涉及表 | tag |
|
||
|
||
**接口逻辑:**
|
||
1. 未传 `keyword` 时,查询 `tag` 表中启用的系统预设标签(`is_active=TRUE AND source=1`)
|
||
2. 传入 `keyword` 时,按标签名称做模糊匹配(`ILIKE %keyword%`),返回命中的启用标签
|
||
3. keyword 搜索默认返回前 20 条结果
|
||
4. 返回标签列表
|
||
|
||
**请求参数(Query):**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| keyword | string | 否 | 标签关键字,按名称模糊搜索;不传则返回默认预设标签列表 |
|
||
|
||
**响应数据:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| tags | array | 标签列表 |
|
||
| tags[].id | string | 标签ID |
|
||
| tags[].name | string | 标签名称 |
|
||
|
||
---
|
||
|
||
#### CONTENT-05 内容管理列表(创作者)
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | GET /client/api/v1/content/manage |
|
||
| 接口用途 | 创作者查看自己的内容列表 |
|
||
| 权限要求 | 创作者 |
|
||
| 涉及表 | post, media |
|
||
|
||
**接口逻辑:**
|
||
1. 校验当前用户为创作者
|
||
2. 查询 `post` 表 WHERE creator_id=当前用户:
|
||
- 若显式传 `status`,按该状态精确筛选
|
||
- 若不传 `status`,默认排除 `removed`,避免已删除/已下架内容仍出现在默认管理列表中(读)
|
||
3. 关联查询 `media` 表 获取封面图(读)
|
||
4. 分页返回,按 created_at 倒序
|
||
|
||
**请求参数(Query):**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| status | string | 否 | 筛选:draft/reviewing/published/rejected/removed;不传时默认返回除 removed 外的内容 |
|
||
| page | number | 否 | 页码 |
|
||
| pageSize | number | 否 | 每页条数 |
|
||
|
||
**响应数据:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| list | array | 内容列表 |
|
||
| list[].contentId | string | 内容ID |
|
||
| list[].title | string | 标题 |
|
||
| list[].coverUrl | url | 封面图URL |
|
||
| list[].type | string | 类型 |
|
||
| list[].status | string | 状态 |
|
||
| list[].rejectReason | string | 拒绝原因(rejected时) |
|
||
| list[].visibility | string | 可见性档位 key(free/junior/basic/senior/supreme) |
|
||
| list[].visibilityTierName | string | 档位展示名(free 时为 null) |
|
||
| list[].viewCount | number | 浏览量 |
|
||
| list[].likeCount | number | 点赞数 |
|
||
| list[].favoriteCount | number | 收藏数 |
|
||
| list[].commentCount | number | 评论数 |
|
||
| list[].isPinned | boolean | 是否置顶 |
|
||
| list[].createdAt | timestamp | 发布时间 |
|
||
| pagination | object | 分页信息 |
|
||
|
||
---
|
||
|
||
#### CONTENT-06 编辑内容
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | PUT /client/api/v1/content/:contentId |
|
||
| 接口用途 | 创作者修改已发布的内容 |
|
||
| 权限要求 | 创作者(只能编辑自己的) |
|
||
| 涉及表 | post, post_tag, post_media, category_tag |
|
||
|
||
**接口逻辑(全程单事务):**
|
||
1. 查询 `post` 表 根据 contentId 获取内容(读)
|
||
2. 校验 post.creator_id = 当前用户
|
||
3. 如果传 tags:在同一事务内
|
||
- `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/required_tier_level/price_cents 等字段(更新)
|
||
6. 返回更新后的内容信息
|
||
|
||
**路径参数:**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| contentId | string | 是 | 内容ID |
|
||
|
||
**请求参数(Body):**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| title | string | 否 | 标题,最长 30 字 |
|
||
| content | string | 否 | 正文内容,最长 200 字 |
|
||
| tags | object[] | 否 | 标签数组,1-3 个;结构同 CONTENT-01:每项 `id` 与 `name` 二选一,仅传 `name` 时同名复用或新建自定义标签 |
|
||
| mentions | string[] | 否 | 被 @ 用户的 userId 数组 |
|
||
| location | object | 否 | 地点 {name, code},传 null 清空 |
|
||
| visibility | string | 否 | 可见性:`free` / `subscriber` / `ppv`;不传则保持原值 |
|
||
| requiredTierLevel | number | 否 | `visibility=subscriber` 时必填;不传则保持原值 |
|
||
| price | number | 否 | `visibility=ppv` 时必填且 > 0;其它可见性传 0 或不传 |
|
||
| highlights | array | 否 | 视频撸点(仅视频类型) |
|
||
| publishDraft | boolean | 否 | 草稿提交发布:true 时把 status 从 0 改为 1 进入审核 |
|
||
|
||
**响应数据:** 同 CONTENT-01 响应结构
|
||
|
||
---
|
||
|
||
#### CONTENT-07 删除/下架内容
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | DELETE /client/api/v1/content/:contentId |
|
||
| 接口用途 | 创作者删除或下架内容 |
|
||
| 权限要求 | 创作者(只能操作自己的) |
|
||
| 涉及表 | post |
|
||
|
||
**接口逻辑:**
|
||
1. 查询 `post` 表 根据 contentId 获取内容(读)
|
||
2. 校验 post.creator_id = 当前用户
|
||
3. 更新 `post` 表 的 status=4(已下架)(更新)
|
||
4. 返回成功
|
||
|
||
**路径参数:**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| contentId | string | 是 | 内容ID |
|
||
|
||
**响应数据:** null
|
||
|
||
---
|
||
|
||
#### CONTENT-08 点赞/取消点赞
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | POST /client/api/v1/content/:contentId/like |
|
||
| 接口用途 | 用户对内容点赞或取消(Toggle) |
|
||
| 权限要求 | 需登录 |
|
||
| 涉及表 | post_like, post |
|
||
|
||
**接口逻辑:**
|
||
1. 查询 `post_like` 表 检查是否已有记录(WHERE user_id=me AND post_id=contentId)(读)
|
||
2. **已点赞 → 取消**:
|
||
- 软删除 `post_like` 表 中的记录(更新 deleted_at)
|
||
- 更新 `post` 表 的 like_count-1(更新)
|
||
3. **未点赞 → 点赞**:
|
||
- 写入 `post_like` 表(写)
|
||
- 更新 `post` 表 的 like_count+1(更新)
|
||
4. 返回当前点赞状态和最新点赞数
|
||
|
||
**路径参数:**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| contentId | string | 是 | 内容ID |
|
||
|
||
**响应数据:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| isLiked | boolean | 当前点赞状态 |
|
||
| likeCount | number | 最新点赞数 |
|
||
|
||
---
|
||
|
||
#### CONTENT-09 收藏/取消收藏
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | POST /client/api/v1/content/:contentId/favorite |
|
||
| 接口用途 | 用户收藏或取消收藏内容(Toggle) |
|
||
| 权限要求 | 需登录 |
|
||
| 涉及表 | post_favorite, post |
|
||
|
||
**接口逻辑:**
|
||
1. 查询 `post_favorite` 表 检查是否已有记录(WHERE user_id=me AND post_id=contentId)(读)
|
||
2. **已收藏 → 取消**:
|
||
- 软删除 `post_favorite` 表 中的记录(更新 deleted_at)
|
||
- 更新 `post` 表 的 favorite_count-1(更新)
|
||
3. **未收藏 → 收藏**:
|
||
- 写入 `post_favorite` 表(写)
|
||
- 更新 `post` 表 的 favorite_count+1(更新)
|
||
4. 返回当前收藏状态和最新收藏数
|
||
|
||
**路径参数:**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| contentId | string | 是 | 内容ID |
|
||
|
||
**响应数据:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| isFavorited | boolean | 当前收藏状态 |
|
||
| favoriteCount | number | 最新收藏数 |
|
||
|
||
---
|
||
|
||
#### CONTENT-10 获取收藏列表
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | GET /client/api/v1/content/favorites |
|
||
| 接口用途 | 用户查看自己收藏的内容 |
|
||
| 权限要求 | 需登录 |
|
||
| 涉及表 | post_favorite, post, media, user |
|
||
|
||
**接口逻辑:**
|
||
1. 查询 `post_favorite` 表 WHERE user_id=当前用户,按 created_at 倒序(读)
|
||
2. 关联查询 `post` 表 获取内容信息(读)
|
||
3. 关联查询 `media` 表 获取封面图(读)
|
||
4. 关联查询 `user` 表 获取创作者信息(读)
|
||
5. 分页返回,结构同 CONTENT-02 列表,多一个 favoritedAt 字段
|
||
|
||
**请求参数(Query):**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| page | number | 否 | 页码 |
|
||
| pageSize | number | 否 | 每页条数 |
|
||
|
||
**响应数据:** 同 CONTENT-02 列表结构,额外增加 `favoritedAt`(timestamp)字段
|
||
|
||
---
|
||
|
||
#### CONTENT-10A 批量取消收藏
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | DELETE /client/api/v1/content/favorites |
|
||
| 接口用途 | 收藏列表场景下批量取消收藏 |
|
||
| 权限要求 | 需登录 |
|
||
| 涉及表 | post_favorite, post |
|
||
|
||
**接口逻辑:**
|
||
1. 读取请求体 `ids`,要求至少 1 个,最多 100 个
|
||
2. 对 `ids` 做去重;若存在空值则返回 `40001`
|
||
3. 将当前用户在这些 `post_id` 上仍处于有效状态的收藏记录批量软删除(更新 `deleted_at`)
|
||
4. 仅对本次实际删除成功的内容执行 `post.favorite_count - 1`
|
||
5. 对于未收藏、已取消收藏或重复传入的 id,不报错,直接忽略
|
||
6. 返回实际删除成功的 `removedIds` 和 `removedCount`
|
||
|
||
**请求参数(Body):**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| ids | string[] | 是 | 要批量取消收藏的内容 ID 列表,最多 100 个 |
|
||
|
||
**响应数据:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| removedIds | string[] | 本次实际取消收藏成功的内容 ID 列表 |
|
||
| removedCount | number | 本次实际取消收藏成功的数量 |
|
||
|
||
---
|
||
|
||
#### CONTENT-11 获取首页分类列表
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | GET /client/api/v1/content/categories |
|
||
| 接口用途 | 获取首页分类 Tab 列表(美妆/旅行/美食等),用于首页顶部 Tab 渲染 |
|
||
| 权限要求 | 公开 |
|
||
| 涉及表 | category |
|
||
|
||
**接口逻辑:**
|
||
1. 查询 `category` 表 WHERE is_active=1,按 sort_order 正序(读)
|
||
2. 返回分类列表(仅返回 id/name/icon 等展示字段,不返回标签关联细节)
|
||
3. 前端拿到 categoryId 后,请求 CONTENT-02 `/client/api/v1/content/feed?categoryId=xxx` 获取该分类下的内容列表
|
||
|
||
> **分类与帖子的关联方式:**
|
||
> - 分类在后台配置时绑定多个预设标签(关系存于 `category_tag` 关联表)
|
||
> - 帖子的标签存于 `post_tag` 关联表,帖子本身不直接绑定 category
|
||
> - 首页按分类取内容 = `JOIN post_tag pt ON pt.post_id = post.id JOIN category_tag ct ON ct.tag_id = pt.tag_id WHERE ct.category_id = :categoryId`
|
||
|
||
**请求参数:** 无
|
||
|
||
**响应数据:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| categories | array | 分类列表 |
|
||
| categories[].categoryId | string | 分类ID |
|
||
| categories[].name | string | 分类名称(如:美妆、旅行、美食) |
|
||
| categories[].icon | url | 分类图标URL(可空) |
|
||
| categories[].sortOrder | number | 排序序号 |
|
||
|
||
---
|
||
|
||
#### CONTENT-12 创建自定义标签
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | POST /client/api/v1/content/tags/custom |
|
||
| 接口用途 | 标签管理场景下预先创建自定义标签。发布/编辑内容(CONTENT-01/06)已支持在 `tags[].name` 内联新建,常规发布流程无需先调本接口 |
|
||
| 权限要求 | 创作者 |
|
||
| 涉及表 | tag |
|
||
|
||
**接口逻辑:**
|
||
1. 校验当前用户为创作者
|
||
2. 校验 name 长度 1-32 字
|
||
3. 查询 `tag` 表 WHERE name=输入值,若已存在直接返回该 tag(去重)
|
||
4. 命中敏感词库 → 422 拒绝
|
||
5. 写入 `tag` 表(source=2,created_by=当前用户,is_active=1)
|
||
6. 返回新创建的 tag
|
||
|
||
**请求参数(Body):**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| name | string | 是 | 标签名,1-32 字 |
|
||
|
||
**响应数据:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | int64 | 标签ID |
|
||
| name | string | 标签名 |
|
||
| source | number | 1=系统 2=自定义 |
|
||
|
||
---
|
||
|
||
### 评论系统(CONTENT-13~17)
|
||
|
||
#### CommentVO 结构
|
||
|
||
所有评论接口返回的评论对象结构如下:
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | string | 评论ID(int64 序列化为字符串) |
|
||
| postId | string | 所属帖子ID |
|
||
| parentId | string | 父评论ID;顶层评论时为空串 |
|
||
| userId | string | 评论者用户ID |
|
||
| nickname | string | 评论者昵称 |
|
||
| avatar | string | 评论者头像URL |
|
||
| content | string | 评论内容 |
|
||
| likeCount | number | 点赞数 |
|
||
| isLiked | boolean | 当前用户是否已点赞;匿名访问时固定 false |
|
||
| replyCount | number | 回复数;仅顶层评论列表有效,回复列表中固定 0 |
|
||
| isAuthor | boolean | 评论者是否为帖子作者 |
|
||
| isMine | boolean | 是否为当前用户所发;匿名访问时固定 false |
|
||
| createdAt | number | 发布时间(Unix 秒) |
|
||
|
||
---
|
||
|
||
#### CONTENT-13 获取帖子评论列表
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | GET /client/api/v1/content/:contentId/comments |
|
||
| 接口用途 | 获取指定帖子的顶层评论列表,支持分页 |
|
||
| 权限要求 | 可匿名(登录后返回 isLiked/isMine 真实值) |
|
||
| 涉及表 | post_comment, post_comment_like, user_profile |
|
||
|
||
**接口逻辑:**
|
||
1. 校验 contentId 非空,查询 `post` 表确认帖子存在(读)
|
||
2. 查询 `post_comment` 表 WHERE post_id=contentId AND parent_id IS NULL AND deleted_at IS NULL,按 created_at 倒序分页(读)
|
||
3. 批量查询 `user_profile` 获取评论者昵称/头像(读)
|
||
4. 批量统计各顶层评论的 reply_count(子查询 WHERE parent_id IN (...))(读)
|
||
5. 若已登录,批量查询 `post_comment_like` 获取当前用户点赞状态;匿名时 isLiked/isMine 固定 false(读)
|
||
6. 返回分页列表
|
||
|
||
**路径参数:**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| contentId | string | 是 | 帖子ID |
|
||
|
||
**请求参数(Query):**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| page | number | 否 | 页码,默认 1 |
|
||
| pageSize | number | 否 | 每页条数,默认 20 |
|
||
|
||
**响应数据:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| list | CommentVO[] | 评论列表,见上方 CommentVO 结构 |
|
||
| pagination.total | number | 总条数 |
|
||
| pagination.page | number | 当前页码 |
|
||
| pagination.pageSize | number | 每页条数 |
|
||
|
||
---
|
||
|
||
#### CONTENT-14 获取评论回复列表
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | GET /client/api/v1/content/comments/:commentId/replies |
|
||
| 接口用途 | 获取指定顶层评论下的一级回复列表,支持分页 |
|
||
| 权限要求 | 可匿名(登录后返回 isLiked/isMine 真实值) |
|
||
| 涉及表 | post_comment, post_comment_like, user_profile |
|
||
|
||
**接口逻辑:**
|
||
1. 解析 commentId(int64),查询 `post_comment` 表确认评论存在(读)
|
||
2. 校验该评论为顶层评论(parent_id IS NULL),否则返回 400
|
||
3. 查询 `post_comment` 表 WHERE parent_id=commentId AND deleted_at IS NULL,按 created_at 正序分页(读)
|
||
4. 批量查询 `user_profile` 获取昵称/头像(读)
|
||
5. 若已登录,批量查询 `post_comment_like` 获取点赞状态;匿名时固定 false(读)
|
||
6. 返回分页列表(replyCount 固定为 0,不递归嵌套)
|
||
|
||
**路径参数:**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| commentId | string | 是 | 顶层评论ID |
|
||
|
||
**请求参数(Query):**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| page | number | 否 | 页码,默认 1 |
|
||
| pageSize | number | 否 | 每页条数,默认 20 |
|
||
|
||
**响应数据:** 同 CONTENT-13,replyCount 固定为 0
|
||
|
||
---
|
||
|
||
#### CONTENT-15 发表评论
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | POST /client/api/v1/content/:contentId/comments |
|
||
| 接口用途 | 用户对帖子发表评论或回复顶层评论(一级嵌套) |
|
||
| 权限要求 | 需登录 |
|
||
| 涉及表 | post_comment, post, user_profile |
|
||
|
||
**接口逻辑:**
|
||
1. 校验 content 非空且不超过 500 字(Unicode 字符数)
|
||
2. 查询 `post` 表确认帖子存在(读)
|
||
3. 若传了 parentId:
|
||
- 查询 `post_comment` 表确认父评论存在(读)
|
||
- 校验父评论必须为顶层评论(不允许回复回复,只支持一级嵌套)
|
||
- 校验父评论的 post_id 与当前 contentId 一致,防跨帖回复
|
||
4. 写入 `post_comment` 表(写)
|
||
5. 返回新评论的 CommentVO
|
||
|
||
**路径参数:**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| contentId | string | 是 | 帖子ID |
|
||
|
||
**请求参数(Body):**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| content | string | 是 | 评论内容,最长 500 字 |
|
||
| parentId | string | 否 | 父评论ID;不传或传空串表示顶层评论 |
|
||
|
||
**响应数据:** CommentVO(见上方结构)
|
||
|
||
---
|
||
|
||
#### CONTENT-16 删除评论
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | DELETE /client/api/v1/content/comments/:commentId |
|
||
| 接口用途 | 评论作者删除自己的评论(软删除) |
|
||
| 权限要求 | 需登录;仅评论作者本人可删 |
|
||
| 涉及表 | post_comment |
|
||
|
||
**接口逻辑:**
|
||
1. 解析 commentId,查询 `post_comment` 表确认评论存在(读)
|
||
2. 校验当前用户 = 评论的 user_id,不匹配返回 403
|
||
3. 软删除:更新 `post_comment.deleted_at`,同步更新 `post.comment_count - 1`(更新)
|
||
4. 返回 null
|
||
|
||
**路径参数:**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| commentId | string | 是 | 评论ID |
|
||
|
||
**响应数据:** null
|
||
|
||
---
|
||
|
||
#### CONTENT-17 评论点赞/取消点赞
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 接口路径 | POST /client/api/v1/content/comments/:commentId/like |
|
||
| 接口用途 | 对评论点赞或取消点赞(Toggle) |
|
||
| 权限要求 | 需登录 |
|
||
| 涉及表 | post_comment_like, post_comment |
|
||
|
||
**接口逻辑:**
|
||
1. 解析 commentId,查询 `post_comment` 表确认评论存在(读)
|
||
2. 查询 `post_comment_like` 表检查是否已点赞(WHERE user_id=me AND comment_id=commentId)(读)
|
||
3. **已点赞 → 取消**:
|
||
- 删除 `post_comment_like` 记录(更新)
|
||
- `post_comment.like_count - 1`(更新)
|
||
4. **未点赞 → 点赞**:
|
||
- 写入 `post_comment_like`(写)
|
||
- `post_comment.like_count + 1`(更新)
|
||
5. 返回当前点赞状态和最新点赞数
|
||
|
||
**路径参数:**
|
||
|
||
| 字段 | 类型 | 必选 | 说明 |
|
||
|------|------|------|------|
|
||
| commentId | string | 是 | 评论ID |
|
||
|
||
**响应数据:**
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| isLiked | boolean | 当前点赞状态 |
|
||
| likeCount | number | 最新点赞数 |
|