feat: protect video playback access

This commit is contained in:
Codex
2026-06-28 23:00:05 +08:00
parent db65295ddc
commit 3647c2bc2d
12 changed files with 581 additions and 24 deletions

View File

@@ -20,6 +20,8 @@ apps/api/src/
learning/ 组卷 session、答题、错题、收藏、练习进度
commerce/ 订单、支付确认、激活码、权益
referral/ 销售/代理客资追踪、首绑保护、团队关系、CRM 队列
storage/ 对象存储签名 provider
video/ 题目视频列表、搜索、SVIP/次数校验和签名播放
platform-admin/ 平台方 SaaS 租户、订阅、账单、使用量
tenant-admin/ 租户品牌、域名、公开设置、登录/商户配置、成员权限、活动/兑换码运营
tenant-content/ 租户后台内容维护:入口、分类树、集合、练习蓝图、题目、视频、分数线、单词、知识手册、资料资源、批量导入

View File

@@ -69,8 +69,8 @@
| 知识手册目录/内容 | 可联调 | `/api/catalog/handbook-*` |
| 知识手册 JSON 导入 | 可联调 | `/api/tenant-content/imports/*/handbook` |
| 分数线字段/院校/专业/记录/趋势 | 可联调 | `/api/scoreline/*` |
| 题目视频/批量预加载/搜索 | 可联调 | `/api/questions/*/videos``/api/videos/search` |
| 视频会员播放次数 | 待补齐 | 缺播放次数扣减、播放日志、防盗链、水印 |
| 题目视频/批量预加载/搜索 | 可联调 | `/api/questions/*/videos``/api/videos/search`;付费视频列表不返回可播放 URL |
| 视频会员播放次数 | 可联调 | `POST /api/videos/play` 支持 SVIP/视频次数校验、签名播放次数扣减、播放日志;深度防盗链和动态水印继续补 |
## 资料与对象存储

View File

@@ -76,6 +76,7 @@ GET /api/scoreline/years
GET /api/questions/{questionId}/videos
POST /api/questions/videos/batch
GET /api/videos/search
POST /api/videos/play
GET /api/tenant-content/content-entries
PUT /api/tenant-content/content-entries
GET /api/tenant-content/content-nodes

View File

@@ -25,7 +25,7 @@
| 全真模拟 | `components/AdminMockexam``MockExamConfigModal.tsx` | 部分覆盖 | 后端有 blueprint 基础;缺完整交卷报告、排名、复盘 |
| 错题本 | 用户 stats/错题逻辑 | 已覆盖 | 后续补错题复习计划 |
| 收藏夹 | `WordFavoritesPage.tsx`、题目收藏 | 已覆盖 | 题目和单词收藏已有 |
| 题目视频 | `VideoPlayer.tsx` | 部分覆盖 | 题目视频查询已有;缺播放签名、次数扣减、水印、防下载 |
| 题目视频 | `VideoPlayer.tsx` | 部分覆盖 | 题目视频查询播放签名、SVIP/次数扣减、播放日志已有;缺深度防盗链、动态水印、播放统计报表 |
| 背单词 | `VocabularyPage.tsx``VocabularyQuiz.tsx` | 部分覆盖 | 单词列表/进度/收藏/统计已有;缺完整艾宾浩斯算法、每日计划、收藏练习细节 |
| 知识手册 | `Handbook*.tsx` | 已覆盖 | 前端需做好 Markdown/公式/图片渲染和搜索体验 |
| 分数线 | `ScorelinePage.tsx` | 已覆盖 | 动态字段/趋势已有;缺批量导入和复杂筛选优化 |
@@ -97,7 +97,7 @@
### P1商用主链路
1. 微信/支付宝支付和 webhook 幂等。
2. 对象存储 PDF 预览、视频播放签名、防盗链、水印。
2. 对象存储 PDF 预览、视频深度防盗链、动态水印。
3. Excel/CSV、分数线、视频批量导入。
4. 数据看板和销售/代理分佣结算。
5. 公共题库授权、租户采纳和版本同步。
@@ -108,4 +108,3 @@
2. 班级、教师、学生分组和学习督导。
3. AI 择校推荐和 PDF 报告。
4. 题库导出、试卷生成、每日一练运营工具。

View File

@@ -8,7 +8,7 @@
- Supabase/PostgreSQL 多租户 schema、RLS、索引、触发器。
- Node.js API 分层:`core/features`
- 学生端核心 API题库、练习、答题、错题、收藏、背单词、知识手册、分数线、视频、资料、订单、权益、个人中心。
- 学生端核心 API题库、练习、答题、错题、收藏、背单词、知识手册、分数线、视频播放签名、资料、订单、权益、个人中心。
- 租户后台 API品牌、域名、设置、支付账户、登录 provider、私密密钥、活动、激活码、优惠券、成员权限、审计、内容管理。
- 平台后台 API租户、SaaS 套餐、订阅、账单、服务费收款、用量。
- 销售/代理/CRM 增长链路邀请码、扫码事件、首绑保护、团队、统计、CRM 队列。
@@ -33,7 +33,7 @@
2. 对象存储
- 已接阿里云 OSS、腾讯云 COS、Supabase Storage 的上传/下载签名 provider。
- 继续补上传后对象存在性校验、PDF 预览地址、视频播放签名、防盗链、水印和 worker 校验。
- 继续补上传后对象存在性校验、PDF 预览地址、视频深度防盗链、动态水印和 worker 校验。
- `content_assets` 继续作为资源台账,不允许前端绕过台账直接访问私有资源。
3. 真实导入 dry-run
@@ -71,8 +71,8 @@
- 租户采纳、复制、授权、版本同步策略。
5. 视频会员控制
- 视频 SVIP 权限、播放次数扣减。
- 防盗链、水印、播放日志、播放统计。
- 已完成视频 SVIP 权限、播放次数扣减、签名播放和播放日志
- 继续补深度防盗链、动态水印、播放统计。
- 单题视频和通用知识视频混合推荐。
6. 学习统计

View File

@@ -4,7 +4,7 @@
## 目标
题库里的图片、PDF、视频、音频、资料包等媒体资源统一走 `content_assets` 台账和后端签名接口。前端不直接保存或读取云厂商密钥,也不直接拼接私有资源 URL。
题库里的图片、PDF、视频、音频、资料包等媒体资源统一走 `content_assets` 台账和后端签名接口。前端不直接保存或读取云厂商密钥,也不直接拼接私有资源 URL。题目视频播放还需要经过 `POST /api/videos/play` 校验 SVIP 或视频次数权益后下发短期签名 URL。
已接入的 provider
@@ -34,6 +34,12 @@ PUT /api/tenant-content/assets
GET /api/catalog/assets/download?assetId=...
```
学生端视频播放:
```text
POST /api/videos/play
```
后台管理员下载:
```text
@@ -46,7 +52,7 @@ POST /api/tenant-content/assets/sign-download
- `objectKey` 默认必须以当前 `tenantId/` 开头,防止跨租户覆盖或读取。
- 禁止 `..`、反斜杠、编码斜杠等危险 object key。
- 上传会校验 MIME 类型和文件大小。
- 下载必须先经过 API 权限判断,再下发短期签名 URL。
- 下载和视频播放必须先经过 API 权限判断,再下发短期签名 URL。
- 云厂商 AccessKey、SecretKey、Service Role Key 只存在服务端环境变量,不返回前端。
- `content_assets` 是资源唯一台账,前端不得绕过台账直接访问私有 bucket。
@@ -98,7 +104,7 @@ SUPABASE_STORAGE_SERVICE_KEY=
- bucket 默认私有,公开资源也建议先经过 CDN/防盗链策略,不让前端直接持有写权限。
- 图片、PDF、视频分别设置合理的 CORS只允许前端域名和小程序业务域名访问。
- 开启对象版本控制、生命周期、跨区域复制或定时备份,满足后续容灾要求。
- 视频资源建议后续接入转码、水印、防盗链、播放日志和播放次数扣减
- 视频资源已接入 SVIP/播放次数校验、短期签名和播放日志;生产阶段继续补转码、动态水印、CDN 防盗链和播放统计
- 大文件上传后应由 worker 校验对象是否真实存在、大小/hash 是否匹配,再把资源状态从 `draft` 发布为 `active`
## 官方依据

View File

@@ -139,7 +139,7 @@ tenant:<tenantId>:theme
| 提交答案 | `POST /api/learning/answers` |
| 错题本 | `GET /api/learning/wrong-questions``POST /api/learning/wrong-questions/resolve` |
| 收藏夹 | `GET/POST /api/learning/favorites/questions` |
| 题目视频 | `GET /api/questions/{questionId}/videos``POST /api/questions/videos/batch` |
| 题目视频 | `GET /api/questions/{questionId}/videos``POST /api/questions/videos/batch``POST /api/videos/play` |
| 背单词 | `/api/catalog/vocabulary-units``/api/catalog/vocabulary-words` |
| 单词进度 | `/api/learning/vocabulary/progress``/api/learning/vocabulary/stats` |
| 单词收藏 | `/api/learning/vocabulary/favorites` |
@@ -152,6 +152,60 @@ tenant:<tenantId>:theme
| 个人中心 | `GET/PATCH /api/profile/me` |
| 销售分享 | `/api/referral/resolve``track-event``bind` |
## 视频播放契约
题目视频分为 `free``svip``video_quota` 三种访问模式。列表接口只用于展示标题、封面、时长、访问模式和试看秒数;除免费公开视频外,列表和搜索接口不会返回可播放 URL。
播放步骤:
1. 进入题目页后调用 `GET /api/questions/{questionId}/videos` 或批量预加载 `POST /api/questions/videos/batch`
2. 用户点击播放时调用 `POST /api/videos/play`
3. 后端校验当前 session 用户、租户、题目绑定关系、SVIP 权益或视频次数权益。
4. 后端返回短期签名 URL、播放 token、权益来源和过期时间。
5. 前端播放器只使用本次返回的 `playback.url`,不要缓存为长期资源地址。
请求示例:
```json
{
"videoId": "00000000-0000-0000-0000-000000000821",
"questionId": "00000000-0000-0000-0000-000000000401"
}
```
响应关键字段:
```json
{
"item": {
"id": "...",
"title": "...",
"accessMode": "svip",
"freePreviewSeconds": 15
},
"playToken": "vp_...",
"playback": {
"url": "https://...",
"expiresAt": "2026-06-28T12:00:00.000Z",
"signatureMode": "signed"
},
"access": {
"mode": "svip",
"entitlementId": "...",
"quotaAccountId": null,
"consumedQuota": 0
}
}
```
前端处理规则:
- `VIDEO_SVIP_REQUIRED`:弹出开通或升级会员。
- `VIDEO_QUOTA_REQUIRED`:提示购买视频次数包或套餐。
- `VIDEO_ASSET_REQUIRED`:展示“视频暂不可播放”,同时上报前端日志。
- 签名 URL 过期后必须重新调用 `/api/videos/play`,不要重试旧 URL。
- 小程序/H5 不保存对象存储真实 key不把播放 URL 写入本地持久缓存。
## 题库新模型接入方式
旧项目常按“地区 -> 科目 -> 章节/试卷”固定层级处理。新项目不要写死层级,按下面模型渲染: