# 草原指南视频上传校验与服务端时长

> 日期：2026-07-13
>
> 后端 Issue：[HL #4949](https://git.1814.love:8443/wx/HL/issues/4949)
>
> 后端 PR：[HL #4961](https://git.1814.love:8443/wx/HL/pulls/4961)、[HL #4962](https://git.1814.love:8443/wx/HL/pulls/4962)、[HL #4964](https://git.1814.love:8443/wx/HL/pulls/4964)
>
> 测试合并：[HL #4963](https://git.1814.love:8443/wx/HL/pulls/4963)、[HL #4965](https://git.1814.love:8443/wx/HL/pulls/4965)，均按 `dev` → `dev-v3` 进入 TEST
>
> 当前状态：TEST 已部署并通过真实 4K 上传确认；尚未合入 `main`、尚未部署正式环境

## 1. 本文件只说明视频上传链路

删除功能、登录鉴权、产品绑定等接口分别由各自 changelog 说明，不与本次上传校验混写。

本次仅影响素材库顶级分类 `grassland_guide` 下的 MP4 视频：

- 视频仍由前端直传 OSS，字节不经过 Resource/User Java 服务。
- 播放仍使用详情接口返回的 OSS MP4 地址，不接入 CDN、VOD、直播或 HLS。
- 不转码、不降低 4K 清晰度、不提供清晰度切换。
- 后端自动读取视频时长；管理端不得提供可编辑时长输入框。

## 2. 获取上传凭证

`POST /admin/material/upload/token`

```json
{
  "categoryCode": "grassland_guide",
  "materialName": "呼伦贝尔草原上的风为什么特别大？",
  "filename": "hulunbuir-wind-4k.mp4",
  "fileSize": 412400073,
  "md5": "32位文件MD5",
  "contentType": "video/mp4"
}
```

前端注意：

- 草原指南视频不提交 `durationSeconds`；即使提交，后端也会忽略。
- 几百 MB 视频按响应中的 `uploadMode=STS_MULTIPART` 分片直传 OSS。
- 不调用旧的 `/admin/material/chunk/**` 完成草原指南视频上传。
- 若响应为秒传，后端也会重新核对文件类型、大小、OSS `Content-Type` 和媒体结构。

## 3. 确认上传

`POST /admin/material/upload/confirm`

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

后端确认时只读取 OSS 对象元数据、MP4 顶层 box 和 `moov`，不下载 `mdat` 视频内容，并校验：

- OSS 对象存在，实际大小与申请一致，`Content-Type` 为 `video/mp4`。
- MP4 满足 `ftyp < moov < mdat`，可进行 faststart 渐进播放。
- 恰好包含一条 H.264 视频轨和一条 AAC-LC 音频轨。
- 从 `moov` 自动计算并保存 `durationSeconds`。

确认成功后，素材返回：

```json
{
  "durationSeconds": 312,
  "mediaStatus": "OSS_MP4_READY_V1",
  "storageProvider": "OSS",
  "ossUrl": "https://<bucket-domain>/<object-key>"
}
```

`durationSeconds`、`mediaStatus`、`storageProvider` 和 `ossUrl` 都是只读字段。`mediaStatus` 成功值只返回 `OSS_MP4_READY_V1`，前端不要增加其他成功状态分支。

## 4. 错误处理

| 业务码 | 含义 | 前端处理 |
|---|---|---|
| `250015` | OSS 元数据或 Range 探测暂时不可用，或并发状态发生变化 | 保留当前 `materialId`，允许稍后重复调用确认接口 |
| `250016` | 文件不是支持的 faststart H.264 + AAC-LC MP4 | 展示后端原因，要求管理员重新选择处理后的文件上传 |

同一上传人对已确认的草原指南视频重复调用确认接口会幂等返回；普通素材的确认行为不变。

## 5. 使用门禁与历史兼容

- 新建草原指南内容时，`videoMaterialId` 必须绑定 `OSS_MP4_READY_V1` 视频素材。
- 编辑时只有替换 `videoMaterialId` 才校验新素材；未替换的历史视频不追溯阻断。
- 单个或批量移动素材分类不执行媒体门禁，也不会自动把普通历史视频标记为 `OSS_MP4_READY_V1`。
- 图片素材、普通分类视频和其他模块的上传行为不变。

## 6. 播放验收边界

该校验只证明文件具备基础渐进播放结构，不承诺所有微信版本、所有 Android/iOS 设备都能流畅解码 4K 原片，也不把后端结构校验描述为“已解决音画同步”。

上线前必须用真实原片和无损 faststart 重排文件，在目标微信 Android/iOS 真机完成播放、拖动、暂停恢复和音画同步 A/B 验收。

## 7. 前端联调清单

- [x] 后端 OpenAPI 与真实请求确认：上传凭证不需要草原指南视频 `durationSeconds`。
- [x] 412400073 字节真实 4K 文件使用 `STS_MULTIPART` 直传 OSS。
- [ ] 管理端时长只读展示确认接口返回值。
- [ ] 原始 `moov` 位于文件尾部的视频确认返回 `250016`。
- [x] 无损 faststart 重排后的真实 4K 原片确认成功并返回 `OSS_MP4_READY_V1`、`durationSeconds=83` 和自动封面。
- [x] 首次确认因 fat JAR 类加载问题失败后，使用同一个 `materialId=2076658080942116865` 重试成功，未重复上传 412 MB 文件。
- [x] 新建草原指南已实测只接受 `OSS_MP4_READY_V1` 视频；历史原素材不变时仍可维护。
- [ ] 微信 Android/iOS 真机完成原片与重排文件的 A/B 验收。

## 8. TEST 真实验证记录

- User 部署任务：`1a2a42da`，`dev-v3`，8081/8181 双实例滚动成功；Nacos `test` 命名空间两实例均为 `healthy=true`、`enabled=true`。
- 素材：`2076658080942116865`，文件大小 `412400073`，存储方 `OSS`，媒体状态 `OSS_MP4_READY_V1`，服务端时长 `83` 秒，自动封面存在。
- fat JAR 回归：通过 Spring Boot `PropertiesLauncher` 直接加载最终候选 JAR，并解析同一真实 4K `moov`，返回 `durationSeconds=83 / OSS_MP4_READY_V1`。
- OSS Range：请求 `bytes=0-1023` 返回 HTTP `206`、1024 字节和 `Content-Range`。
- 仍未据此宣称真机音画同步通过；默认 Bucket 域名的微信 Android/iOS 真机播放、拖动、暂停恢复与音画同步必须由前端单独验收。
