diff --git a/changelogs/2026-07/12_feat_grassland_guide_admin_mp_vod.md b/changelogs/2026-07/12_feat_grassland_guide_admin_mp_vod.md new file mode 100644 index 0000000..6abca40 --- /dev/null +++ b/changelogs/2026-07/12_feat_grassland_guide_admin_mp_vod.md @@ -0,0 +1,520 @@ +# 草原指南管理、VOD 素材与小程序接口 + +> **服务**: `hl-user-service`、`hl-resource-service`、`hl-mp-service`、`hl-gateway` +> **PR**: [#4916](https://git.1814.love:8443/wx/HL/pulls/4916)、[#4917](https://git.1814.love:8443/wx/HL/pulls/4917)、[#4918](https://git.1814.love:8443/wx/HL/pulls/4918) +> **Issue**: [#4914](https://git.1814.love:8443/wx/HL/issues/4914) +> **日期**: 2026-07-12 +> **影响范围**: 管理后台草原指南内容维护、素材库草原指南分类、小程序草原指南列表/详情/播放 + +--- + +## 一、前端必须先确认的结论 + +1. **草原指南是独立内容模块**,不是“小程序专用素材库列表”;页面不做内容分类筛选。 +2. 素材库新增固定顶级分类: + - 名称:`草原指南` + - 编码:`grassland_guide` + - 编码固定,不能删除、停用或修改;显示名称和排序可以调整。 +3. 视频、可选自定义封面、富文本图片都由后台管理员上传;**小程序用户没有上传接口**。 +4. 视频文件走阿里云 VOD,确认后自动注册为 `grassland_guide` 分类的视频素材。 +5. 视频封面规则: + - 传 `customCoverMaterialId`:使用管理员上传的图片,`coverSource=CUSTOM`; + - 不传或传 `null`:使用阿里云 VOD 自动截帧封面,`coverSource=AUTO`。 +6. 本次没有新增 `/mp/material/miniprogram/page`,小程序草原指南只调用本文的 `/mp/grassland-guide/**`。 +7. 所有业务失败仍可能返回 HTTP 200;前端必须判断响应体 `code` / `success`。 + +--- + +## 二、管理后台菜单与权限 + +| 配置 | 值 | +|---|---| +| 菜单名 | `草原指南管理` | +| 路由 | `/grassland-guide` | +| 组件路径 | `grassland-guide/GrasslandGuideList` | +| 图标 | `VideocamOutline` | +| 列表权限 | `grassland-guide:list` | +| 新增权限 | `grassland-guide:create` | +| 编辑权限 | `grassland-guide:edit` | +| 发布权限 | `grassland-guide:publish` | +| 下线权限 | `grassland-guide:offline` | +| 设置精选权限 | `grassland-guide:featured` | +| 上传视频权限 | `grassland-guide:vod:upload` | + +菜单及按钮已分配给 `SUPER_ADMIN`、`OPERATOR`。 + +- 所有管理端接口均要求管理员登录。 +- 列表、详情没有额外业务角色限制。 +- 创建、更新、发布、下线、VOD 上传仅允许 `SUPER_ADMIN` 或 `OPERATOR`。 +- 当前没有“删除草原指南内容”接口,只允许下线。 + +--- + +## 三、接口总表 + +### 管理端内容接口 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `GET` | `/admin/grassland-guide/videos` | 分页查询 | +| `GET` | `/admin/grassland-guide/videos/{videoId}` | 内容详情 | +| `POST` | `/admin/grassland-guide/videos` | 创建草稿 | +| `PUT` | `/admin/grassland-guide/videos/{videoId}` | 全量更新 | +| `PUT` | `/admin/grassland-guide/videos/{videoId}/publish` | 发布/重新发布 | +| `PUT` | `/admin/grassland-guide/videos/{videoId}/offline` | 下线 | + +### 管理端 VOD 接口 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `POST` | `/admin/grassland-guide/vod/upload/create` | 创建 VOD 上传会话 | +| `POST` | `/admin/grassland-guide/vod/upload/refresh` | 刷新上传凭证 | +| `POST` | `/admin/grassland-guide/vod/upload/confirm` | 确认上传并注册素材 | +| `GET` | `/admin/grassland-guide/vod/materials/{materialId}/play-auth` | 后台预览播放凭证 | + +### 小程序只读接口 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `GET` | `/mp/grassland-guide/home` | 精选区 + 视频分页 | +| `GET` | `/mp/grassland-guide/videos` | 视频分页 | +| `GET` | `/mp/grassland-guide/videos/{videoId}` | 视频详情 | +| `GET` | `/mp/grassland-guide/videos/{videoId}/play-auth` | 短效播放凭证 | + +小程序仅放行以上精确 `GET` 路径;同路径的 `POST`、`PUT`、`DELETE` 不放行。 + +--- + +## 四、管理端视频内容契约 + +### 4.1 分页查询 `GET /admin/grassland-guide/videos` + +#### Query + +| 字段 | 类型 | 必填 | 默认/约束 | 说明 | +|---|---|---:|---|---| +| `keyword` | String | 否 | - | 标题、摘要模糊匹配 | +| `status` | String | 否 | `DRAFT/PUBLISHED/OFFLINE` | 状态精确过滤 | +| `featured` | Boolean | 否 | - | 是否精选 | +| `page` | Integer | 否 | 默认 1,最小 1 | 页码 | +| `pageSize` | Integer | 否 | 默认 20,范围 1–100 | 每页条数 | + +固定排序:`publishTime DESC, videoId DESC`。管理列表不按 `sortWeight` 排序。 + +#### `PageResult` + +分页对象只有 `records`、`total`、`page`、`pageSize`,没有 `totalPages`。 + +| 列表项字段 | 类型 | 说明 | +|---|---|---| +| `videoId` | String | 内容 ID | +| `title` | String | 标题 | +| `summary` | String | 摘要 | +| `effectiveCoverUrl` | String | 实际生效的封面 | +| `coverSource` | String | `AUTO/CUSTOM` | +| `durationSeconds` | Long | 视频时长(秒) | +| `featured` | Boolean | 是否精选 | +| `showProducedBadge` | Boolean | 是否显示“呼籁出品”角标 | +| `sortWeight` | Integer | 精选排序权重 | +| `status` | String | `DRAFT/PUBLISHED/OFFLINE` | +| `publishTime` | LocalDateTime | 首次发布时间 | +| `linkedProductId` | String | 预留关联产品 ID,一期不做跳转 | + +所有雪花 ID 在响应中按 String 返回,前端不要转成 JavaScript `Number`。 + +### 4.2 详情 `GET /admin/grassland-guide/videos/{videoId}` + +详情包含列表项全部字段,并增加: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `videoMaterialId` | String | VOD 视频素材 ID | +| `customCoverMaterialId` | String | 自定义封面素材 ID,可空 | +| `coverMaterialId` | String | 一期兼容字段,值同 `customCoverMaterialId` | +| `contentHtml` | String | 白名单清洗后的正文 HTML | +| `contentImageMaterialIds` | `String[]` | 正文图片素材 ID | +| `version` | Integer | 乐观锁版本 | +| `createdBy` | String | 创建管理员 ID | +| `updatedBy` | String | 最后更新管理员 ID | +| `createdAt` | LocalDateTime | 创建时间 | +| `updatedAt` | LocalDateTime | 更新时间 | +| `relatedVideos` | `GrasslandGuideVideoListVO[]` | 按手工顺序返回的相关推荐 | + +### 4.3 创建草稿 `POST /admin/grassland-guide/videos` + +创建允许字段暂不完整,返回的初始状态固定为 `DRAFT`、`version=1`。 + +### 4.4 全量更新 `PUT /admin/grassland-guide/videos/{videoId}` + +创建和更新共用以下字段;更新时额外必传 `version`。 + +| 字段 | 类型 | DTO 必填 | 约束/语义 | +|---|---|---:|---| +| `title` | String | 否 | 最长 200;空白按 null | +| `summary` | String | 否 | 最长 1000;空白按 null | +| `videoMaterialId` | Long/String | 否 | `grassland_guide` 分类的可播放 VOD 素材 | +| `customCoverMaterialId` | Long/String | 否 | 自定义图片封面;兼容输入别名 `coverMaterialId` | +| `contentHtml` | String | 否 | 富文本正文 | +| `contentImageMaterialIds` | Long[]/String[] | 否 | 正文图片素材 ID,元素不能为 null | +| `featured` | Boolean | 否 | 未传按 false | +| `showProducedBadge` | Boolean | 否 | 未传按 false | +| `sortWeight` | Integer | 否 | 未传按 0 | +| `publishTime` | LocalDateTime | 否 | 首次发布未传时由服务端补当前时间 | +| `linkedProductId` | Long/String | 否 | 二期预留,一期不做产品跳转 | +| `relatedVideoIds` | Long[]/String[] | 否 | 最多 3 条,保持数组顺序 | +| `version` | Integer | 更新必填 | 必须等于详情返回的当前版本 | + +> **更新是全量覆盖,不是 PATCH。** 未传布尔值会写为 false,未传 `sortWeight` 会写为 0,未传正文图片/相关推荐会清空,未传自定义封面会切回 `AUTO`。 + +示例: + +```json +{ + "title": "呼伦贝尔草原上的风为什么特别大?", + "summary": "从地形和季节理解草原的风。", + "videoMaterialId": "2030000000000001", + "customCoverMaterialId": null, + "contentHtml": "

正文内容

", + "contentImageMaterialIds": [], + "featured": true, + "showProducedBadge": true, + "sortWeight": 100, + "publishTime": null, + "linkedProductId": null, + "relatedVideoIds": ["2030000000000002", "2030000000000003"], + "version": 1 +} +``` + +### 4.5 发布/下线 + +请求体一致: + +```json +{ "version": 2 } +``` + +允许的状态流转只有: + +```text +DRAFT -> PUBLISHED -> OFFLINE -> PUBLISHED +``` + +- 不允许退回 `DRAFT`。 +- 全量更新、发布、下线成功后都会 `version + 1`。 +- 首次从草稿发布且 `publishTime` 为空时,服务端自动填当前时间。 +- 下线后重新发布保留首次发布时间。 + +### 4.6 发布门禁 + +发布前必须同时满足: + +- 标题非空; +- 有效 VOD 视频素材,媒体状态为 `Normal`,时长大于 0; +- 有生效封面; +- 正文至少有有效文字或一张图片; +- 有发布时间; +- 有 2–3 条相关推荐。 + +相关推荐不能为 null、不能重复、不能指向自身,且目标内容必须存在。管理端按请求数组顺序保存;小程序详情只返回其中仍为 `PUBLISHED` 的内容,不自动补位。 + +富文本会做白名单清洗;`img.src` 只允许 HTTP/HTTPS,并且必须与 `contentImageMaterialIds` 对应素材 URL 集合完全一致。 + +--- + +## 五、VOD 上传与素材注册 + +### 5.1 创建上传会话 + +`POST /admin/grassland-guide/vod/upload/create` + +```json +{ + "title": "草原指南测试视频", + "fileName": "grassland-guide.mp4" +} +``` + +| 入参字段 | 类型 | 必填 | 约束 | +|---|---|---:|---| +| `title` | String | 是 | 1–128 字符 | +| `fileName` | String | 是 | 1–255 字符,保留真实扩展名;支持 mp4/mov/avi/mkv/wmv/flv/webm | + +返回 `data`: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `uploadSessionId` | String | 后续 refresh/confirm 使用 | +| `vodFileId` | String | 阿里云 VideoId | +| `uploadAddress` | String | 阿里云上传地址,按不透明字符串处理 | +| `uploadAuth` | String | 阿里云上传凭证,按不透明敏感字符串处理 | + +`uploadAddress`、`uploadAuth` 禁止写日志、LocalStorage 或业务数据库。 + +### 5.2 刷新上传凭证 + +`POST /admin/grassland-guide/vod/upload/refresh` + +```json +{ "uploadSessionId": "会话ID" } +``` + +返回结构同创建接口。阿里云浏览器上传 SDK 触发凭证过期回调时调用此接口,并把新凭证交回 SDK。 + +### 5.3 确认上传并注册素材 + +`POST /admin/grassland-guide/vod/upload/confirm` + +```json +{ + "uploadSessionId": "会话ID", + "materialName": "草原指南测试视频" +} +``` + +| 字段 | 类型 | 必填 | 约束 | +|---|---|---:|---| +| `uploadSessionId` | String | 是 | 非空 | +| `materialName` | String | 否 | 最长 200 | + +只有阿里云媒体状态为 `Normal` 时才会确认成功。若视频仍在转码/截图处理中,前端保留会话 ID,稍后重试 confirm,不要重新创建上传会话。重复 confirm 是幂等的,会返回同一个 `fileId/materialId`,并刷新异步生成的时长和封面。 + +成功响应: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "file": { + "fileId": "...", + "storageProvider": "VOD", + "providerFileId": "阿里云VideoId", + "durationSeconds": 125, + "mediaStatus": "Normal", + "thumbnailUrl": "阿里云自动封面URL" + }, + "material": { + "materialId": "...", + "categoryCode": "grassland_guide", + "fileType": "VIDEO", + "storageProvider": "VOD", + "providerFileId": "阿里云VideoId", + "durationSeconds": 125, + "mediaStatus": "Normal", + "thumbnailUrl": "阿里云自动封面URL" + } + }, + "success": true +} +``` + +前端保存 `data.material.materialId`,作为内容表单的 `videoMaterialId`。 + +### 5.4 推荐的浏览器上传顺序 + +```text +选择本地视频 + -> create 获取 uploadAddress/uploadAuth/uploadSessionId + -> 使用阿里云 VOD 浏览器上传 SDK 直传 + -> 凭证过期时调用 refresh + -> SDK 报告上传完成后调用 confirm + -> confirm 返回 materialId + -> 创建/更新草原指南草稿 +``` + +不要把视频二进制提交给 HL 后端;后端只签发上传凭证、确认媒体状态并登记素材。 + +### 5.5 后台预览播放凭证 + +`GET /admin/grassland-guide/vod/materials/{materialId}/play-auth` + +返回: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `vodFileId` | String | 阿里云 VideoId | +| `playAuth` | String | 短效播放凭证 | +| `expiresInSeconds` | Long | 本次凭证有效期,以返回值为准 | +| `durationSeconds` | Long | 时长(秒) | +| `coverUrl` | String | 播放器封面 | + +每次打开播放器或凭证即将过期时重新请求;不要缓存、持久化 `playAuth`。 + +--- + +## 六、自定义封面与正文图片上传 + +图片继续复用现有素材库 OSS 上传流程: + +1. `POST /admin/material/upload/token` +2. 浏览器按返回凭证直传 OSS +3. `POST /admin/material/upload/confirm` + +请求上传凭证时固定传: + +```json +{ + "categoryCode": "grassland_guide", + "materialName": "视频自定义封面", + "filename": "cover.jpg", + "fileSize": 123456, + "md5": "32位十六进制MD5", + "contentType": "image/jpeg" +} +``` + +- 自定义封面、正文图片必须是 `grassland_guide` 分类的有效图片素材。 +- 有自定义封面时传 `customCoverMaterialId`;不传/null 时后端使用 VOD 自动封面。 +- 自动封面读取顺序为阿里云 `coverURL`,为空时取 snapshots 中第一张有效图片;后端当前不主动发起截图任务。 +- 如果阿里云自动封面仍未生成,草稿可以保存,但发布会因缺少生效封面返回 `371004`;稍后重试发布会先刷新 VOD 元数据。 +- 删除、批量删除或移出分类时,如果素材正在被草原指南引用,返回 `371009`。 +- 固定顶级分类本身不能删除、停用或修改编码,保护错误码为 `210405`。 + +--- + +## 七、小程序接口详情 + +### 7.1 首页 `GET /mp/grassland-guide/home` + +Query:`page` 默认 1;`pageSize` 默认 20、最大 100。 + +返回: + +```json +{ + "code": 200, + "message": "成功", + "data": { + "featuredVideos": [], + "videos": { + "records": [], + "total": 0, + "page": 1, + "pageSize": 20 + } + }, + "success": true +} +``` + +- `featuredVideos` 最多 5 条,只取已发布且 `featured=true` 的内容。 +- 精选排序:`sortWeight DESC -> publishTime DESC -> videoId DESC`。 +- 没有精选时返回空数组,不使用普通视频兜底。 +- `videos` 返回全部已发布内容,排序为 `publishTime DESC -> videoId DESC`。 + +### 7.2 视频分页 `GET /mp/grassland-guide/videos` + +Query 同首页:`page`、`pageSize`。 + +卡片字段: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `videoId` | String | 内容 ID | +| `title` | String | 标题 | +| `summary` | String | 摘要 | +| `coverUrl` | String | 生效封面 | +| `coverSource` | String | `AUTO/CUSTOM` | +| `durationSeconds` | Long | 时长(秒) | + +精选项在卡片字段基础上增加 `showProducedBadge`。 + +### 7.3 视频详情 `GET /mp/grassland-guide/videos/{videoId}` + +在卡片字段基础上增加: + +| 字段 | 类型 | 说明 | +|---|---|---| +| `publishTime` | LocalDateTime | 发布时间 | +| `contentHtml` | String | 已清洗正文 | +| `relatedVideos` | 视频卡片数组 | 只包含仍在线的推荐内容,保持手工顺序 | +| `showProducedBadge` | Boolean | 是否显示“呼籁出品”角标 | + +### 7.4 播放凭证 `GET /mp/grassland-guide/videos/{videoId}/play-auth` + +返回字段与后台预览一致:`vodFileId`、`playAuth`、`expiresInSeconds`、`durationSeconds`、`coverUrl`。 + +- 仅已发布内容可获取。 +- 接口不缓存,避免返回过期凭证。 +- 前端不得把 `playAuth` 当成长期播放 URL。 + +--- + +## 八、主要错误码 + +| code | 说明 | 前端动作 | +|---:|---|---| +| `400` | DTO 校验、枚举或 JSON 格式错误 | 展示具体 message | +| `401` | 未登录/小程序写请求被拦截 | 走登录或停止请求 | +| `403` | 非 `SUPER_ADMIN/OPERATOR` 执行写操作 | 隐藏操作并提示无权限 | +| `371001` | 草原指南视频不存在或未发布 | 返回列表/显示已下线 | +| `371002` | version 冲突 | 重新拉详情后再编辑 | +| `371003` | 非法状态流转 | 刷新状态 | +| `371004` | 发布条件不完整 | 按 message 补齐字段 | +| `371005` | 视频/封面/正文图片素材无效 | 重新选择素材 | +| `371006` | 相关推荐无效 | 检查数量、重复、自身和目标 ID | +| `371007` | 正文图片 URL 与素材声明不一致 | 同步 `contentHtml` 和图片 ID | +| `371009` | 素材正被草原指南引用 | 先解除内容引用 | +| `210405` | 固定素材分类受保护 | 不提供删除/停用/改编码入口 | +| `250507` | VOD 媒体尚未处理完成 | 稍后重试 confirm | +| `250511` | VOD 确认处理中 | 防重复点击,稍后重试 | +| `503` | user/resource 下游暂不可用 | 允许重试,不要当业务空数据 | + +--- + +## 九、前端实施检查单 + +- [ ] 新建 `grassland-guide/GrasslandGuideList` 并接入动态菜单。 +- [ ] 内容列表不增加“内容分类”筛选,只保留关键词、状态、精选筛选。 +- [ ] ID 全程按 String 保存和回传,避免精度丢失。 +- [ ] 编辑前先拉详情,更新/发布/下线始终提交最新 `version`。 +- [ ] 更新接口按全量表单提交,不使用局部 PATCH 心智。 +- [ ] VOD 采用 create -> SDK 直传 -> refresh(按需)-> confirm 流程。 +- [ ] 上传凭证只保存在当前上传任务内,不落 LocalStorage、不打印。 +- [ ] 图片上传固定使用 `categoryCode=grassland_guide`。 +- [ ] 自定义封面可清空;清空后展示后端返回的 VOD 自动封面。 +- [ ] 小程序播放器按需调用 play-auth,并按 `expiresInSeconds` 管理凭证。 +- [ ] 小程序实现列表骨架屏、空态、加载失败重试;接口空态为 200 + 空数组/空分页。 +- [ ] 不给小程序增加任何视频、封面或正文图片上传入口。 + +--- + +## 十、测试环境已验证 + +测试分支:`dev-v3`,合并提交:`4d4f090dcc47527e247177277313eec636ec0ada`。 + +```text +hl-user-service 8081 / 8181 Nacos healthy=true ✓ +hl-resource-service 8082 / 8182 Nacos healthy=true ✓ +hl-mp-service 8085 / 8185 Nacos healthy=true ✓ +hl-gateway 8080 / 8180 Nacos healthy=true ✓ + +GET /mp/grassland-guide/home -> 200,精选空数组 + 空分页 ✓ +GET /mp/grassland-guide/videos -> 200,PageResult total=0 ✓ +GET /mp/grassland-guide/videos/{不存在ID} -> code=371001 ✓ +POST /mp/grassland-guide/home -> code=401 ✓ +GET /admin/grassland-guide/videos(未登录) -> code=401 ✓ +GET /admin/grassland-guide/videos(管理员) -> 200,total=0 ✓ +GET /admin/menu/my -> 草原指南菜单及组件路径存在 ✓ +GET /admin/dict/data/material_category -> grassland_guide=ACTIVE ✓ +POST /admin/grassland-guide/vod/upload/create(空请求) -> code=400,路由与校验生效 ✓ +阿里云 VOD GetCategories -> HTTP 200,当前 AK 具备 VOD 访问权限 ✓ +``` + +Flyway: + +- User `V20260712_002`、`V20260712_004` 成功; +- Resource `V20260712_001` 成功; +- 菜单 7 条、固定分类 1 条、VOD 会话表、3 张草原指南内容表均已落库。 + +> 本轮没有创建真实云端测试视频,以免产生孤儿 VideoId/上传会话;真实大文件上传仍需前端联调时用测试视频执行一次 create -> 直传 -> confirm。 + +--- + +## 十一、不影响范围 + +- 不修改现有通用素材列表接口。 +- 不增加小程序素材库上传或分页接口。 +- 不修改 `hl-ui` 现有代码;本文为前端实施契约。 +- `linkedProductId` 仅持久化,产品跳转留到二期。