Files
gongxue-base/docs/refactor/object-storage.md
2026-06-29 18:25:47 +08:00

245 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 对象存储接入说明
更新时间2026-06-29
## 目标
题库里的图片、PDF、视频、音频、资料包等媒体资源统一走 `content_assets` 台账和后端签名接口。前端不直接保存或读取云厂商密钥,也不直接拼接私有资源 URL。题目视频播放还需要经过 `POST /api/videos/play` 校验 SVIP 或视频次数权益后下发短期签名 URL。
已接入的 provider
- `local_dev`:本地开发占位签名,便于前后端联调。
- `aliyun_oss`:阿里云 OSS 官方 Node.js SDK `signatureUrl`
- `tencent_cos`:腾讯云 COS XML API V5 签名 URL服务端用 Node `crypto` 实现,避免引入当前 COS Node SDK 的高危依赖。
- `supabase_storage`Supabase Storage 官方 `createSignedUploadUrl` / `createSignedUrl` 语义。
- `external_url`:外部公开或厂商托管 URL只允许作为已管理资源的下载地址不支持后端直传签名。
## API
后台申请上传签名:
```text
POST /api/tenant-content/assets/sign-upload
```
后台登记资源草稿:
```text
PUT /api/tenant-content/assets
```
后台确认上传并发布:
```text
POST /api/tenant-content/assets/confirm-upload
```
学生端下载:
```text
GET /api/catalog/assets/download?assetId=...
```
学生端预览:
```text
GET /api/catalog/assets/preview?assetId=...
```
学生端视频播放:
```text
POST /api/videos/play
```
后台查看资源访问事件:
```text
GET /api/tenant-content/assets/access-events?assetId=...
```
后台管理员下载:
```text
POST /api/tenant-content/assets/sign-download
```
后台管理员预览:
```text
POST /api/tenant-content/assets/sign-preview
```
后台资源复检 worker
```bash
npm --workspace @tiku-saas/worker run assets:once
```
题库 PDF/Word/每日一练 ZIP 导出 worker
```bash
npm --workspace @tiku-saas/worker run exports:once
```
## 标准上传流程
后台前端上传 PDF、图片、视频或资料包时必须走下面流程
1. 调用 `POST /api/tenant-content/assets/sign-upload` 申请短期上传 URL。
2. 前端使用返回的 `upload.url``upload.headers` 直传对象存储。
3. 调用 `PUT /api/tenant-content/assets` 登记资源台账。托管对象默认进入 `status=draft``uploadStatus=pending`
4. 调用 `POST /api/tenant-content/assets/confirm-upload`由后端读取对象元数据并比对大小、MIME、SHA-256。
5. 校验通过且 `publish=true` 时,后端将资源置为 `status=active``uploadStatus=verified`
6. 学生端只能下载或预览 `active + verified` 的托管对象资源。
7. 学生端和后台管理员下载/预览都会写入 `content_asset_access_events`,包含 `assetId/userId/accessType/result/expiresInSec/signatureMode/ip/userAgent` 等审计字段。
8. 生产环境定时运行 assets worker复检 `pending/verified` 托管对象的大小、MIME、SHA-256 等元数据。
9. 如果复检发现对象丢失、跨租户 objectKey、大小/MIME/checksum 不一致worker 会把资源置为 `uploadStatus=failed`,并将 `active` 资源退回 `draft`,同时写入 `security_flags.assetRecheckFailed=true``audit_logs`
## 题库导出生成资源
题库导出 PDF/Word/每日一练 ZIP 不走前端上传,而是由后端和 worker 自动进入资源台账:
1. 租户后台调用 `POST /api/tenant-content/exports/questions``format=pdf``format=docx``format=daily_practice_zip`
2. API 校验租户内容权限、导出范围和答案/解析开关,创建 `content_export_jobs.status=pending`
3. `apps/worker --job exports` 抢占 pending job复用后端导出 payload 规则渲染 PDF/Word或为每日一练生成 ZIP 素材包PDF/Word 会应用 `options.watermarkText` 水印ZIP 包会包含 PNG/SVG 卡片、拼图、manifest 和脱敏 payload。
4. worker 将二进制文件写入配置的对象存储。`local_dev` 会写入 `EXPORT_LOCAL_STORAGE_ROOT`,生产建议使用 `supabase_storage``aliyun_oss``tencent_cos`
5. worker 创建 `content_assets`,设置 `uploadStatus=verified`,并把 `assetId`、文件名、大小和 SHA-256 回填到 `content_export_jobs.output_metadata`
6. 前端轮询 `GET /api/tenant-content/exports/jobs`,拿到 `assetId` 后通过 `POST /api/tenant-content/assets/sign-download``sign-preview` 下载/预览。
注意PDF/Word/ZIP 文件本身不直接存入数据库,数据库只保存 job、资源台账和校验 metadata。`daily_practice_zip` 资源类型为 `package`,通常只做签名下载,不做 inline 预览。
确认上传示例:
```json
{
"assetId": "00000000-0000-0000-0000-000000000000",
"fileSizeBytes": 4096,
"mimeType": "application/pdf",
"checksumSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"publish": true
}
```
注意:
- `local_dev` 用声明的元数据模拟对象 HEAD方便本地和集成测试。
- `aliyun_oss` 使用 OSS 对象 HEAD 元数据。
- `tencent_cos` 使用 COS HEAD Object 预签名请求读取元数据。
- `supabase_storage` 使用 Storage list metadata 做最小存在性/大小校验。
- 如果 provider 无法返回 SHA-256后端会记录 `checksumUnavailable=true`,生产建议上传时写入对象自定义元数据,例如 `x-oss-meta-sha256``x-cos-meta-sha256`
- worker 的复检证据写入 `verification_details.assetWorker`,包含 `lastCheckedAt``lastResult``expected``observed``issues`,便于租户后台定位资源异常。
## 安全规则
- 只有租户内容维护权限用户可以申请上传签名。
- `objectKey` 默认必须以当前 `tenantId/` 开头,防止跨租户覆盖或读取。
- 禁止 `..`、反斜杠、编码斜杠等危险 object key。
- 上传会校验 MIME 类型和文件大小。
- 托管对象资源未确认前不能发布为 `active`,学生端不可下载。
- PDF/图片预览使用 `inline` 签名,不等同于长期公开 URL。
- 下载、预览和视频播放必须先经过 API 权限判断,再下发短期签名 URL。
- 学生端 `preview`、锁定资料下载、视频和资料包默认使用更短 TTL。当前策略是学生 inline 预览、`members/svip/private` 资料、`video/package` 资源最多 300 秒;租户后台预览最多 3600 秒,后台下载最多 86400 秒。
- `visibility in ('members','svip','private')` 的外部 CDN/直链资源默认会被拒绝,除非资源 `metadata.providerManagedAccess=true``metadata.cdnAccessMode='signed_by_provider'`。商用建议这类资源优先登记为 `objectKey`,由 API 生成 OSS/COS/Supabase Storage 私有签名 URL。
- 每次上传签名、上传确认、学生下载/预览、后台下载/预览都会写入 `content_asset_access_events`。授权失败也会记录 `result=denied``denyCode`,用于租户后台排查资源访问问题。
- 云厂商 AccessKey、SecretKey、Service Role Key 只存在服务端环境变量,不返回前端。
- `content_assets` 是资源唯一台账,前端不得绕过台账直接访问私有 bucket。
- 前端不能把 `uploadStatus=failed``status=draft` 的资源继续展示为可下载;列表仍返回时应展示“资料处理中”或“资源异常已下架”,真正下载/预览会被后端拒绝。
## 访问事件字段
资源访问事件表:
```text
public.content_asset_access_events
```
关键字段:
| 字段 | 说明 |
| --- | --- |
| `tenant_id` | 租户隔离维度 |
| `asset_id` | 资源 ID上传签名阶段可能为空 |
| `user_id` | 学生或后台操作者 |
| `actor_role` | `anonymous/student/tenant_content_editor/tenant_admin/system` |
| `access_type` | `download/preview/admin_download/admin_preview/upload_sign/upload_confirm` |
| `result` | `granted``denied` |
| `deny_code` | 拒绝原因,例如 `ASSET_CDN_ACCESS_NOT_ALLOWED` |
| `expires_in_sec` | 下发签名有效期 |
| `signature_mode` | `local-placeholder``supabase-storage-signed-url``aliyun-oss-signature-url-v1``tencent-cos-signature-url-v5``public-or-provider-managed` |
| `metadata` | 签名摘要、对象位置、文件名等排查信息;不保存云厂商密钥 |
后台接口:
```text
GET /api/tenant-content/assets/access-events?assetId=<assetId>&limit=100
```
前端只用于后台审计和排查,不要把 `content_asset_access_events` 当成学生端下载列表来源。
## 环境变量
通用:
```text
STORAGE_DEFAULT_PROVIDER=local_dev
STORAGE_DEFAULT_BUCKET=tenant-assets
STORAGE_PUBLIC_BASE_URL=
STORAGE_MAX_UPLOAD_BYTES=524288000
STORAGE_ALLOWED_MIME_PREFIXES=image/,video/,audio/
STORAGE_ALLOWED_MIME_TYPES=application/pdf,application/json,application/zip,application/x-zip-compressed,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/octet-stream,text/plain,text/markdown,text/csv
STORAGE_REQUIRE_TENANT_PREFIX=true
```
阿里云 OSS
```text
ALIYUN_OSS_REGION=oss-cn-hangzhou
ALIYUN_OSS_ENDPOINT=
ALIYUN_OSS_ACCESS_KEY_ID=
ALIYUN_OSS_ACCESS_KEY_SECRET=
ALIYUN_OSS_STS_TOKEN=
ALIYUN_OSS_INTERNAL=false
```
腾讯云 COS
```text
TENCENT_COS_REGION=ap-shanghai
TENCENT_COS_APP_ID=
TENCENT_COS_SECRET_ID=
TENCENT_COS_SECRET_KEY=
TENCENT_COS_SECURITY_TOKEN=
```
Supabase Storage
```text
SUPABASE_STORAGE_URL=https://your-project.supabase.co/storage/v1
SUPABASE_STORAGE_SERVICE_KEY=
```
assets worker
```text
WORKER_ASSET_BATCH_SIZE=50
WORKER_ASSET_MIN_AGE_SECONDS=300
WORKER_ASSET_RECHECK_INTERVAL_SECONDS=86400
WORKER_ASSET_REQUEST_TIMEOUT_MS=10000
```
## 生产建议
- 阿里云和腾讯云生产环境优先用 STS/临时密钥或 RAM/CAM 最小权限账号。
- bucket 默认私有,公开资源也建议先经过 CDN/防盗链策略,不让前端直接持有写权限。
- 图片、PDF、视频分别设置合理的 CORS只允许前端域名和小程序业务域名访问。
- 开启对象版本控制、生命周期、跨区域复制或定时备份,满足后续容灾要求。
- 视频资源已接入 SVIP/播放次数校验、短期签名和播放日志生产阶段继续补转码、动态水印、CDN 防盗链和播放统计。
- 大文件上传已经支持 API 即时确认和 worker 元数据复检;后续继续补杀毒、转码、水印和 CDN 刷新。
## 官方依据
- Supabase Storage JavaScript `createSignedUrl` 用于为文件创建固定有效期的签名 URL并要求对象具备 `select` 权限;本项目由服务端集中处理权限与签名。
- 阿里云 OSS 官方文档建议服务端生成 signed URL 后让客户端直传对象,也说明私有对象可通过 presigned URL 在有效期内授权下载或预览。
- 腾讯云 COS 文档给出的签名 URL 格式包含 `q-sign-algorithm``q-ak``q-sign-time``q-key-time``q-header-list``q-url-param-list``q-signature` 等字段;本项目服务端生成签名,不把 SecretKey 下发前端。