hl-api-changelog/changelogs/2026-07/12_feat_grassland_guide_admin_mp_oss.md
2026-07-12 18:36:19 +08:00

16 KiB

草原指南管理、OSS 素材上传与小程序接口(一期)

日期2026-07-12

后端 IssueHL #4919HL #4922HL #4927

后端 PRHL #4921HL #4924HL #4929HL #4930

测试分支: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
  • vodFileIdplayAuthexpiresInSeconds
  • 阿里云 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_ADMINOPERATOR

2.2 素材库分类

  • 显示名称:草原指南
  • 固定编码:grassland_guide
  • 分类性质:系统内置顶级分类
  • 用途:视频、视频封面、正文图片
  • 约束:不能删除、停用或修改固定编码;后台错误码为 210405

前端应隐藏该顶级分类的删除、停用和修改编码入口。视频素材、封面图片都上传到这个分类;不需要创建“小程序专用”子分类。

3. 视频上传流程

3.1 前端先读取本地视频时长

选择 MP4 后,在请求上传凭证前读取浏览器本地元数据:

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

请求示例:

{
  "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。
  • 单个视频最大 2147483648 字节2 GiB,大于 10485760 字节10 MiB时使用 OSS 分片上传。

响应中的 uploadMode 有两种:

  • PRESIGNED_URL:使用响应中的 uploadUrl 执行 PUT
  • STS_MULTIPART:使用 stsTokenbucketregionossKey 分片上传。

无论哪种模式,上传时的 Content-Type 都必须使用接口返回的 contentType。STS 临时密钥只能保存在内存中,不能写入日志或本地存储。

instantUpload=true,直接使用响应中的 material,不再上传文件,也不调用确认接口。

3.3 上传完成后确认素材

POST /admin/material/upload/confirm

{
  "materialId": "2076195939797688322",
  "description": "草原指南视频",
  "tagIds": []
}

确认接口会通过 OSS HeadObject 读取实际对象元数据,并校验:

  • OSS 对象必须真实存在。
  • 实际 Content-Length 必须等于申请上传凭证时声明的 fileSize
  • MP4 的实际 Content-Type 必须是 video/mp4(允许携带参数)。

任一校验失败时素材不会进入 ACTIVE。确认成功后保存返回的 materialId,创建草原指南内容时作为 videoMaterialId 提交。

3.4 文件夹上传

如素材库继续支持文件夹批量上传,POST /admin/material/upload/folder 的每个 files[] 项也必须为草原指南视频提交 durationSeconds

4. 视频封面

封面图片同样通过素材库通用上传接口上传到 grassland_guide,图片不传 durationSeconds

  • 指定 customCoverMaterialId:详情返回 coverSource=CUSTOM,使用管理员上传的图片。
  • 不指定或清空 customCoverMaterialId:详情返回 coverSource=AUTO,后端使用视频素材的 OSS 自动截帧地址。

OSS 自动封面格式:

{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 下线

创建或全量更新的主体:

{
  "title": "呼伦贝尔草原上的风为什么特别大?",
  "summary": "视频摘要",
  "videoMaterialId": "2076195939797688322",
  "customCoverMaterialId": "2076196060279070722",
  "contentHtml": "<p>正文内容</p>",
  "contentImageMaterialIds": [],
  "featured": true,
  "showProducedBadge": true,
  "sortWeight": 3,
  "linkedProductId": null,
  "version": 2
}

version 只在更新时必填。创建草稿允许内容暂时不完整,发布时必须满足:

  • 标题、视频素材和有效封面完整。
  • 视频素材为正常状态的 grassland_guide OSS MP4。
  • 视频存在 ossUrlthumbnailUrl 和正数时长。
  • 正文包含有效文字或正文图片。

管理端不再提交 publishTimedurationSecondsrelatedVideoIds

  • durationSeconds 从当前 videoMaterialId 对应的视频素材自动派生。
  • publishTime 在首次点击发布时由后端写入;后续编辑、下架和重上架都保留首次值。
  • 相关推荐在小程序查询时自动生成,没有其他候选也不阻止当前内容发布。
  • sortWeight 数值越小越靠前,影响精选 Hero 位及相关内容同分时的弱排序。

后台详情返回新增字段:

{
  "videoMaterialId": "2076195939797688322",
  "videoUrl": "https://.../the-daily-dweebs-1080.mp4",
  "customCoverMaterialId": "2076196060279070722",
  "coverSource": "CUSTOM",
  "effectiveCoverUrl": "https://.../cover.jpg",
  "durationSeconds": 61,
  "publishTime": "2026-07-12 14:44:56"
}

durationSecondspublishTime 在 Knife4j 中均标记为只读。

所有雪花 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} 视频详情
GET /mp/grassland-guide/videos/{videoId}/recommendations?page=1&pageSize=20 自动推荐分页,供滑动加载

分页规则:

  • page 默认 1,小于 1 时按 1 处理。
  • pageSize 默认 20,最大 100
  • 只返回已发布内容,按发布时间倒序。
  • 标准分页字段为 recordstotalpagepageSize;当前响应不返回 totalPages,需要时由前端计算。

详情直接返回 OSS 原视频地址:

{
  "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。进入相关推荐区域时,请求独立分页接口:

{
  "records": [
    {
      "videoId": "2076196084920610817",
      "title": "草原指南测试Caminandes 3Llamigos",
      "summary": "Blender 官方真实短片",
      "coverUrl": "https://.../auto-cover.jpg",
      "coverSource": "AUTO",
      "durationSeconds": 151
    }
  ],
  "total": 2,
  "page": 1,
  "pageSize": 1
}

推荐排序规则:

  1. 仅使用 PUBLISHED 内容,排除当前视频、已删除内容和重复项。
  2. 同一 linkedProductId(关联行程/产品)优先。
  3. 再按标题、摘要内容相似度排序。
  4. featuredsortWeightpublishTime 只做弱排序。
  5. 其余已上架内容使用稳定随机顺序兜底。

候选集合不变时各页顺序稳定且不重不漏。若管理员恰好在用户翻页期间发布或下架内容,标准 offset 分页边界可能变化,前端应按 videoId 去重后追加。

小程序直接使用原生组件:

<video src="{{detail.videoUrl}}" controls></video>

OSS MP4 支持 Range 请求,原生 <video> 能从 MP4 元数据读取并显示当前播放时间、总时长和进度条。卡片时长使用接口的 durationSeconds 格式化即可,不做清晰度切换。

7. 测试环境验收结果

dev-v3 已于 2026-07-12 完成部署和真实数据验证:

  • hl-resource-service8082/8182,Nacos 2/2 健康。
  • hl-mp-service8085/8185,Nacos 2/2 健康。
  • hl-gateway8080/8180,Nacos 2/2 健康。
  • hl-user-service8081/8181,Nacos 2/2 健康。
  • User Flyway 20260712.005 执行成功;Resource Flyway 20260712.001 执行成功。
  • Knife4j 已展示草原指南管理接口、素材上传接口及只读字段;小程序文档为上述 4 个 GET。
  • POST /admin/material/upload/tokenPOST /admin/material/upload/confirm 的请求体,以及详情/推荐路径中的 videoId,均在 Knife4j 中明确标记为必填。

真实数据:

内容 素材 ID 内容 ID 时长 封面
The Daily Dweebs 2076195939797688322 2076196082752155649 61 秒 CUSTOM
Caminandes 3: Llamigos 2076195998660550657 2076196084920610817 151 秒 AUTO
Glass Half 2076196033146118145 2076196086652858370 194 秒 AUTO
呼伦贝尔草原有多大?3812×2160 原片) 2076248042813480962 2076248969276551170 83 秒 CUSTOM

验收结果:

  • 三个文件都是不同的 Blender 官方真实作品,不是同一视频的多档清晰度。
  • 三个视频均通过 STS_MULTIPART 上传,分类为 grassland_guide,存储方为 OSS
  • 自定义封面通过 PRESIGNED_URL 上传。
  • MP4 Range 请求均返回 206 video/mp4
  • 自定义封面和 OSS 自动封面均返回 200 image/*
  • 小程序 pageSize=2 实测分页为第 1 页 2 条、第 2 页 1 条,无重复。
  • 三个详情均直接返回 videoUrl 和正确时长,且不再包含 relatedVideos
  • 三个推荐分页均为 total=2;按 pageSize=1 加载两页,无自身、无重复,重复请求顺序一致。
  • 实测下架其中一条后推荐 total 从 2 变为 1,候选中不含下架内容;重上架后恢复为 2。
  • 故意提交伪造 publishTime=2000年durationSeconds=9999 和旧 relatedVideoIds,后端均未采纳;下架重上架后首次发布时间保持不变。
  • /play-auth 接口已不可用。
  • 真实 4K 原片为 412360919 字节(约 393 MiB,通过 STS_MULTIPART 上传;OSS HEAD 返回 200Content-Type: video/mp4Content-Length: 412360919
  • 对 4K 原片请求 Range: bytes=0-1 返回 206Content-Range: bytes 0-1/412360919,证明 OSS 支持原生 <video> 所需的分段读取,流量不经过业务服务器。
  • 4K 内容使用独立上传的 JPEG 封面,详情返回 coverSource=CUSTOM;推荐分页返回 3 条上架内容且排除了当前视频。
  • 当前测试桶默认域名同时返回 Content-Disposition: attachmentx-oss-force-download: true。HTTP Range 能正常工作,但小程序仍须做真机播放验收;若原生 <video> 因该响应头拒绝播放,优先给同一 OSS Bucket 绑定自定义域名直出,不需要先接 CDN。

8. 2026-07-14 正式环境 Nacos 部署清单

草原指南一期暂不使用 CDN、VOD、HLS 或转码,正式环境仅同步 OSS 原片上传上限。部署 hl-user-service 前执行:

  1. 在正式 Nacos 读取并备份完整 hl-user-service-prod.yml,确认生产实例实际加载的 namespace、group 和 dataId。
  2. 只做最小配置修改:
file:
  max-size:
    video: 2147483648
  resumable-threshold: 10485760
  1. 不复制仓库内测试配置中的其他值,不覆盖正式环境已有的 OSS、数据库、Redis 或鉴权配置。
  2. 串行滚动部署 hl-user-service,确认两实例均注册健康且配置已生效。
  3. 用正式 OSS 做一轮大于 10 MiB 的 MP4 分片上传、确认、HEADRange 206 验收;不得在日志或交付文档中记录 STS、AccessKey、Token 等凭证。

9. 前端改造检查单

  • 新增“草原指南管理”列表、编辑和发布页面。
  • 素材库展示顶级分类“草原指南”,隐藏系统分类删除/停用/改码操作。
  • 选择 MP4 后通过 loadedmetadata 获取时长并 Math.ceil,界面只读展示,不允许手填。
  • 使用素材库通用上传接口处理 PRESIGNED_URLSTS_MULTIPART
  • 支持单独上传/选择图片作为自定义封面,并允许清空后恢复自动封面。
  • 草原指南内容保存 videoMaterialId,不保存 VOD 字段。
  • 发布日期只读:草稿显示未发布,首次发布成功后刷新详情回显后端时间。
  • 删除管理员手工选择相关推荐的控件,不提交 relatedVideoIds
  • 小程序列表接入分页参数和分页结果。
  • 小程序详情直接使用 videoUrl 播放。
  • 相关推荐通过独立分页接口滑动加载,并按 videoId 去重追加。
  • 删除 VOD SDK、play-auth 调用和清晰度切换 UI。
  • 所有雪花 ID 均按字符串处理。