docs: 更新草原指南 OSS 前端契约

这个提交包含在:
API Changelog Bot
2026-07-12 14:50:57 +08:00
父节点 1df4ac1a01
当前提交 6ed33870b9
共修改 2 个文件,包含 293 行新增和 524 行删除
@@ -0,0 +1,293 @@
# 草原指南管理、OSS 素材上传与小程序接口(一期)
> 日期:2026-07-12
>
> 后端 Issue:[HL #4919](https://git.1814.love:8443/wx/HL/issues/4919)
>
> 后端 PR:[HL #4921](https://git.1814.love:8443/wx/HL/pulls/4921)
>
> 测试分支:`dev-v3`
>
> 正式环境:一期发布时沿用本文契约
## 1. 最终方案
草原指南是独立的内容管理模块,视频文件仍统一进入素材库,不再使用阿里云 VOD。
- 后台新增一级菜单“草原指南管理”。
- 素材库新增系统内置顶级分类“草原指南”,固定编码 `grassland_guide`。
- 草原指南视频只接受 OSS 原始 `.mp4`,一个视频只返回一个 `videoUrl`。
- 不转码、不生成多档清晰度、不接入 VOD 播放 SDK、不请求播放凭证。
- 视频和封面都只能由后台管理员上传;小程序用户没有上传接口和上传权限。
- 视频可以单独上传一张图片作为封面;未指定时由 OSS 自动截取视频首帧。
- 小程序列表支持分页,详情直接把 `videoUrl` 交给原生 `<video>` 组件播放。
前端必须删除旧版草原指南 VOD 方案中的以下内容:
- `/admin/grassland-guide/vod/**`
- `/mp/grassland-guide/videos/{videoId}/play-auth`
- `vodFileId`、`playAuth`、`expiresInSeconds`
- 阿里云 VOD Web SDK、转码状态轮询、清晰度选择器
## 2. 菜单、分类与权限
### 2.1 后台菜单
- 菜单名称:`草原指南管理`
- 菜单路径:`/grassland-guide`
- 菜单权限:`grassland-guide:list`
按钮权限:
| 功能 | 权限码 |
|---|---|
| 新增 | `grassland-guide:create` |
| 编辑 | `grassland-guide:edit` |
| 发布 | `grassland-guide:publish` |
| 下线 | `grassland-guide:offline` |
| 设置主推 | `grassland-guide:featured` |
| 上传视频 | `grassland-guide:video:upload` |
默认授权角色为 `SUPER_ADMIN`、`OPERATOR`。
### 2.2 素材库分类
- 显示名称:`草原指南`
- 固定编码:`grassland_guide`
- 分类性质:系统内置顶级分类
- 用途:视频、视频封面、正文图片
- 约束:不能删除、停用或修改固定编码;后台错误码为 `210405`
前端应隐藏该顶级分类的删除、停用和修改编码入口。视频素材、封面图片都上传到这个分类;不需要创建“小程序专用”子分类。
## 3. 视频上传流程
### 3.1 前端先读取本地视频时长
选择 MP4 后,在请求上传凭证前读取浏览器本地元数据:
```ts
export async function readVideoDurationSeconds(file: File): Promise<number> {
const url = URL.createObjectURL(file)
try {
const video = document.createElement('video')
video.preload = 'metadata'
video.src = url
await new Promise<void>((resolve, reject) => {
video.onloadedmetadata = () => resolve()
video.onerror = () => reject(new Error('无法读取视频元数据'))
})
return Math.ceil(video.duration)
} finally {
URL.revokeObjectURL(url)
}
}
```
草原指南视频的 `durationSeconds` 必填且必须大于 0。后端不会下载完整视频计算时长。
### 3.2 获取素材上传凭证
`POST /admin/material/upload/token`
请求示例:
```json
{
"categoryCode": "grassland_guide",
"materialName": "呼伦贝尔草原上的风为什么特别大?",
"filename": "hulunbuir-wind.mp4",
"fileSize": 12925261,
"md5": "32位文件MD5",
"contentType": "video/mp4",
"durationSeconds": 61
}
```
草原指南视频的强制校验:
- `categoryCode` 必须是 `grassland_guide`。
- 文件扩展名必须是 `.mp4`。
- `contentType` 必须以 `video/mp4` 开头。
- `durationSeconds` 必须大于 0。
响应中的 `uploadMode` 有两种:
- `PRESIGNED_URL`:使用响应中的 `uploadUrl` 执行 `PUT`。
- `STS_MULTIPART`:使用 `stsToken`、`bucket`、`region`、`ossKey` 分片上传。
无论哪种模式,上传时的 `Content-Type` 都必须使用接口返回的 `contentType`。STS 临时密钥只能保存在内存中,不能写入日志或本地存储。
若 `instantUpload=true`,直接使用响应中的 `material`,不再上传文件,也不调用确认接口。
### 3.3 上传完成后确认素材
`POST /admin/material/upload/confirm`
```json
{
"materialId": "2076195939797688322",
"description": "草原指南视频",
"tagIds": []
}
```
确认成功后保存返回的 `materialId`,创建草原指南内容时作为 `videoMaterialId` 提交。
### 3.4 文件夹上传
如素材库继续支持文件夹批量上传,`POST /admin/material/upload/folder` 的每个 `files[]` 项也必须为草原指南视频提交 `durationSeconds`。
## 4. 视频封面
封面图片同样通过素材库通用上传接口上传到 `grassland_guide`,图片不传 `durationSeconds`。
- 指定 `customCoverMaterialId`:详情返回 `coverSource=CUSTOM`,使用管理员上传的图片。
- 不指定或清空 `customCoverMaterialId`:详情返回 `coverSource=AUTO`,后端使用视频素材的 OSS 自动截帧地址。
OSS 自动封面格式:
```text
{videoUrl}?x-oss-process=video/snapshot,t_0,f_jpg,w_800,m_fast
```
前端只使用响应中的 `effectiveCoverUrl`/`coverUrl`,不要自行拼接处理参数。
## 5. 草原指南管理接口
基础路径:`/admin/grassland-guide/videos`
| 方法 | 路径 | 说明 |
|---|---|---|
| `GET` | `/admin/grassland-guide/videos` | 分页列表 |
| `GET` | `/admin/grassland-guide/videos/{videoId}` | 详情 |
| `POST` | `/admin/grassland-guide/videos` | 创建草稿 |
| `PUT` | `/admin/grassland-guide/videos/{videoId}` | 全量更新,必须传当前 `version` |
| `PUT` | `/admin/grassland-guide/videos/{videoId}/publish` | 发布/重新发布 |
| `PUT` | `/admin/grassland-guide/videos/{videoId}/offline` | 下线 |
创建或全量更新的主体:
```json
{
"title": "呼伦贝尔草原上的风为什么特别大?",
"summary": "视频摘要",
"videoMaterialId": "2076195939797688322",
"customCoverMaterialId": "2076196060279070722",
"contentHtml": "<p>正文内容</p>",
"contentImageMaterialIds": [],
"featured": true,
"showProducedBadge": true,
"sortWeight": 300,
"publishTime": null,
"linkedProductId": null,
"relatedVideoIds": ["2076196084920610817", "2076196086652858370"],
"version": 2
}
```
`version` 只在更新时必填。创建草稿允许内容暂时不完整,发布时必须满足:
- 标题、视频素材和有效封面完整。
- 视频素材为正常状态的 `grassland_guide` OSS MP4。
- 视频存在 `ossUrl`、`thumbnailUrl` 和正数时长。
- 正文包含有效文字或正文图片。
- 配置 2~3 条不重复、非自身的相关推荐。
后台详情返回新增字段:
```json
{
"videoMaterialId": "2076195939797688322",
"videoUrl": "https://.../the-daily-dweebs-1080.mp4",
"customCoverMaterialId": "2076196060279070722",
"coverSource": "CUSTOM",
"effectiveCoverUrl": "https://.../cover.jpg",
"durationSeconds": 61
}
```
所有雪花 ID 在 JavaScript 中都按字符串处理,避免超过安全整数范围。
## 6. 小程序接口
小程序只保留三个只读接口:
| 方法 | 路径 | 说明 |
|---|---|---|
| `GET` | `/mp/grassland-guide/home?page=1&pageSize=20` | 精选区与视频分页 |
| `GET` | `/mp/grassland-guide/videos?page=1&pageSize=20` | 视频分页列表 |
| `GET` | `/mp/grassland-guide/videos/{videoId}` | 视频详情 |
分页规则:
- `page` 默认 `1`,小于 `1` 时按 `1` 处理。
- `pageSize` 默认 `20`,最大 `100`。
- 只返回已发布内容,按发布时间倒序。
- 标准分页字段为 `records`、`total`、`page`、`pageSize`;当前响应不返回 `totalPages`,需要时由前端计算。
详情直接返回 OSS 原视频地址:
```json
{
"videoId": "2076196082752155649",
"title": "草原指南测试|The Daily Dweebs",
"videoUrl": "https://.../the-daily-dweebs-1080.mp4",
"durationSeconds": 61,
"coverSource": "CUSTOM",
"coverUrl": "https://.../cover.jpg",
"contentHtml": "<p>正文内容</p>",
"relatedVideos": []
}
```
小程序直接使用原生组件:
```html
<video src="{{detail.videoUrl}}" controls></video>
```
OSS MP4 支持 Range 请求,原生 `<video>` 能从 MP4 元数据读取并显示当前播放时间、总时长和进度条。卡片时长使用接口的 `durationSeconds` 格式化即可,不做清晰度切换。
## 7. 测试环境验收结果
`dev-v3` 已于 2026-07-12 完成部署和真实数据验证:
- `hl-resource-service`:`8082/8182`,Nacos `2/2` 健康。
- `hl-mp-service`:`8085/8185`,Nacos `2/2` 健康。
- `hl-gateway`:`8080/8180`,Nacos `2/2` 健康。
- `hl-user-service`:`8081/8181`,Nacos `2/2` 健康。
- User Flyway `20260712.005` 执行成功;Resource Flyway `20260712.001` 执行成功。
- Knife4j 已展示草原指南管理接口、素材上传接口及 `durationSeconds`;小程序文档只剩上述 3 个接口。
真实数据:
| 内容 | 素材 ID | 内容 ID | 时长 | 封面 |
|---|---|---|---:|---|
| The Daily Dweebs | `2076195939797688322` | `2076196082752155649` | 61 秒 | `CUSTOM` |
| Caminandes 3: Llamigos | `2076195998660550657` | `2076196084920610817` | 151 秒 | `AUTO` |
| Glass Half | `2076196033146118145` | `2076196086652858370` | 194 秒 | `AUTO` |
验收结果:
- 三个文件都是不同的 Blender 官方真实作品,不是同一视频的多档清晰度。
- 三个视频均通过 `STS_MULTIPART` 上传,分类为 `grassland_guide`,存储方为 `OSS`。
- 自定义封面通过 `PRESIGNED_URL` 上传。
- MP4 Range 请求均返回 `206 video/mp4`。
- 自定义封面和 OSS 自动封面均返回 `200 image/*`。
- 小程序 `pageSize=2` 实测分页为第 1 页 2 条、第 2 页 1 条,无重复。
- 三个详情均直接返回 `videoUrl`、正确时长和 2 条相关推荐。
- 旧 `/play-auth` 接口已不可用。
## 8. 前端改造检查单
- [ ] 新增“草原指南管理”列表、编辑和发布页面。
- [ ] 素材库展示顶级分类“草原指南”,隐藏系统分类删除/停用/改码操作。
- [ ] 选择 MP4 后通过 `loadedmetadata` 获取时长并 `Math.ceil`。
- [ ] 使用素材库通用上传接口处理 `PRESIGNED_URL` 和 `STS_MULTIPART`。
- [ ] 支持单独上传/选择图片作为自定义封面,并允许清空后恢复自动封面。
- [ ] 草原指南内容保存 `videoMaterialId`,不保存 VOD 字段。
- [ ] 小程序列表接入分页参数和分页结果。
- [ ] 小程序详情直接使用 `videoUrl` 播放。
- [ ] 删除 VOD SDK、`play-auth` 调用和清晰度切换 UI。
- [ ] 所有雪花 ID 均按字符串处理。