txagent-y/docs/dev/api/API-04-内容系统.md

581 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# API-04 内容系统CONTENT
> 共 12 个接口CONTENT-01~12
### 内容类型枚举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 | 已下架 |
### 可见性与订阅等级对应关系free + 4 档)
| visibility可见性 | DB值 | 所需 subscription_tier.tier_level | 说明 |
|---------------------|------|----------------------------------|------|
| free | 0 | 无需订阅 | 免费公开,所有用户可见 |
| junior | 1 | ≥ 1 | 初级会员及以上 |
| basic | 2 | ≥ 2 | 基础会员及以上 |
| senior | 3 | ≥ 3 | 高级会员及以上 |
| supreme | 4 | ≥ 4 | 至尊会员(独占)|
> **判断逻辑:** `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。
### 关于评论commentCount
> 评论系统为 P1 扩展功能(参见数据库设计文档 `post_comment` 表)。当前阶段 `commentCount` 固定返回 `0`,评论 CRUD 接口将在 P1 阶段补充。
---
#### 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. 校验 tags1-3 个):每项 `id``name` 二选一。传 `id``tag` 表存在性校验;只传 `name` 时若同名 tag 已存在则复用,否则当场创建 source=2 的自定义标签(等价于内联 CONTENT-12
4. 校验 mentions查询 `user` 表 确认被 @ 用户存在
5. 若 type=2video且包含 highlights校验 time_offset_seconds < 视频时长
6. 校验 visibility若不是 `free`查询 `subscription_tier` WHERE creator_id=当前用户 AND tier_level=映射值 AND is_active=true不存在则返回 422"该档位未启用"
7. 写入 `post` status=isDraft?0:1写入 location_name/codevisibility 存映射后的 int
8. 写入关系表post_media / post_tag / post_mention / post_highlight
9. 草稿isDraft=true跳过审核流程非草稿进入审核队列
10. 返回新创建的内容信息
**请求参数Body**
| 字段 | 类型 | 必选 | 说明 |
|------|------|------|------|
| title | string | | 标题最长 30 |
| content | string | | 正文内容最长 200 |
| type | string | | 内容类型post/video |
| mediaIds | int64[] | | 媒体ID数组图片最多9张或1个视频 |
| 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` 或当前创作者已启用的档位 keyjunior/basic/senior/supreme 之一)。后端会校验该 key 对应的 tier_level 在当前创作者的 `subscription_tier` 中存在且 `is_active=true`否则返回 422 |
| highlights | array | | 视频章节标记数组type=video 用于在播放进度条上打点跳转 |
| highlights[].timeOffsetSeconds | int | | 标记时间点必须 < 视频时长 |
| highlights[].label | string | | 标记说明文字最长 40 |
| isDraft | boolean | | 是否保存为草稿默认 false |
**校验返回码:**
- 422: 字段超长 / 标签数量不在 1-3 / 媒体处理未完成 / highlights 时间点超出视频时长 / visibility 不在当前创作者已启用档位内
- 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 | 可见性档位 keyfree/junior/basic/senior/supreme |
| visibilityTierName | string | 可见档位的展示名创作者自定义名称free 时为 null |
| 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 参数选择排序策略
- recommend70%算法推荐 + 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_nameprofile.avatar_url
5. 关联查询 `media` 获取封面图/缩略图
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` 回填
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[].visibility | string | 可见性档位 keyfree/junior/basic/senior/supreme |
| list[].visibilityTierName | string | 该档位在当前创作者下的展示名free 时为 null |
| list[].visibilityTierPrice | number | 该档位价格free 时省略 |
| 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-03 获取内容详情
| 项目 | 说明 |
|------|------|
| 接口路径 | GET /client/api/v1/content/:contentId |
| 接口用途 | 获取单条内容详情 |
| 权限要求 | 按订阅档位判断 |
| 涉及表 | post, post_tag, tag, post_media, media, user, view_history, post_like, post_favorite, user_subscription, content_boost |
**接口逻辑:**
1. 查询 `post` 根据 contentId 获取内容
2. 浏览去重 + 计数同一事务
- `INSERT INTO view_history (user_id, post_id, last_viewed_at) VALUES (:uid, :pid, NOW()) ON CONFLICT (user_id, post_id, view_date) DO NOTHING RETURNING id`
- 仅当 RETURNING 返回行数 > 0即当天首次浏览才执行 `UPDATE post SET view_count = view_count + 1 WHERE id = :pid`
- 保证 `post.view_count``view_history` 按日去重一致
3. 关联查询 `user` 表 获取创作者信息(读)
4. 查询 `media` 表 获取关联媒体信息(读)
5. 查询 `user_subscription` 表 判断当前用户订阅状态(读):
- **有权限**签发媒体签名URL
- **无权限**返回模糊图URL、文字截断前2行、视频返回3秒模糊预览URL(media.preview_url)
6. 查询 `content_boost` 表 获取注水数据(读)
7. 查询 `post_like` 表 检查 isLikedWHERE user_id=me AND post_id=contentId
8. 查询 `post_favorite` 表 检查 isFavorited
9. 组装完整响应
**路径参数:**
| 字段 | 类型 | 必选 | 说明 |
|------|------|------|------|
| contentId | string | 是 | 内容ID |
**响应数据:**
| 字段 | 类型 | 说明 |
|------|------|------|
| contentId | string | 内容ID |
| creator | object | 创作者信息 |
| title | string | 标题 |
| content | string | 正文无权限时截断前2行 |
| type | string | 内容类型 post/video |
| images | array | 图片列表 |
| images[].mediaId | string | 媒体ID |
| images[].url | url | 原图签名URL有权限时 |
| images[].blurUrl | url | 模糊图URL |
| images[].thumbnailUrl | url | 缩略图URL |
| images[].isLocked | boolean | 是否锁定 |
| videoUrl | url | 视频签名URL有权限时 |
| videoBlurPreviewUrl | url | 3秒模糊预览URL无权限时 |
| videoCoverUrl | url | 视频封面URL |
| videoDuration | number | 视频时长(秒) |
| highlights | array | 视频章节标记列表 [{timeOffsetSeconds,label}] |
| tags | object[] | 标签列表 [{id,name,source}] |
| mentions | object[] | 被 @ 用户列表 [{userId,nickname,avatar}] |
| location | object | 地点信息 {name,code} |
| visibility | string | 可见性档位 keyfree/junior/basic/senior/supreme |
| visibilityTierName | string | 该档位在内容作者下的展示名创作者自定义free 时为 null |
| visibilityTierPrice | number | 该档位月价代币free 时为 0— 用于无权限时引导订阅 |
| isUnlocked | boolean | 当前用户是否已解锁(基于 user_subscription.tier_level ≥ post.visibility 且订阅有效) |
| isPinned | boolean | 是否置顶 |
| viewCount | number | 浏览量含boost |
| likeCount | number | 点赞数含boost |
| favoriteCount | number | 收藏数含boost |
| commentCount | number | 评论数 |
| isLiked | boolean | 是否已点赞 |
| isFavorited | boolean | 是否已收藏 |
| createdAt | timestamp | 发布时间 |
---
#### CONTENT-04 获取预设标签列表
| 项目 | 说明 |
|------|------|
| 接口路径 | GET /client/api/v1/content/tags |
| 接口用途 | 获取平台标签(系统预设 + 用户自定义热门标签) |
| 权限要求 | 公开 |
| 涉及表 | tag |
**接口逻辑:**
1. 查询 `tag` 表 WHERE is_active=1
2. 根据 source 过滤source=1 仅系统预设source=2 仅用户自定义;不传则全部
3. 根据 sort默认按 sort_order 升序sort=hot 时按 use_count DESC 排序
4. 返回标签列表(含 source 与 useCount与 CONTENT-03 详情字段对齐)
**请求参数Query**
| 字段 | 类型 | 必选 | 说明 |
|------|------|------|------|
| source | int | 否 | 1=系统预设 2=用户自定义;不传返回全部 |
| sort | string | 否 | 排序:默认按 sort_orderhot=按 use_count 倒序 |
**响应数据:**
| 字段 | 类型 | 说明 |
|------|------|------|
| tags | array | 标签列表 |
| tags[].id | string | 标签ID |
| tags[].name | string | 标签名称 |
| tags[].source | int | 1=系统预设 2=用户自定义 |
| tags[].useCount | number | 累计使用次数(用于热度展示) |
---
#### CONTENT-05 内容管理列表(创作者)
| 项目 | 说明 |
|------|------|
| 接口路径 | GET /client/api/v1/content/manage |
| 接口用途 | 创作者查看自己的内容列表 |
| 权限要求 | 创作者 |
| 涉及表 | post, media |
**接口逻辑:**
1. 校验当前用户为创作者
2. 查询 `post` 表 WHERE creator_id=当前用户,可选按 status 筛选(读)
3. 关联查询 `media` 表 获取封面图(读)
4. 分页返回,按 created_at 倒序
**请求参数Query**
| 字段 | 类型 | 必选 | 说明 |
|------|------|------|------|
| status | string | 否 | 筛选draft/reviewing/published/rejected/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 | 可见性档位 keyfree/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/category_id 等字段(更新)
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 | 否 | 可见性档位 key`free` 或当前创作者已启用的档位之一(同 CONTENT-01 校验规则),不传则保持原值 |
| 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-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=2created_by=当前用户is_active=1
6. 返回新创建的 tag
**请求参数Body**
| 字段 | 类型 | 必选 | 说明 |
|------|------|------|------|
| name | string | 是 | 标签名1-32 字 |
**响应数据:**
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int64 | 标签ID |
| name | string | 标签名 |
| source | number | 1=系统 2=自定义 |