14 KiB
对象存储接入说明
更新时间:2026-06-29
目标
题库里的图片、PDF、视频、音频、资料包等媒体资源统一走 content_assets 台账和后端签名接口。前端不直接保存或读取云厂商密钥,也不直接拼接私有资源 URL。题目视频播放还需要经过 POST /api/videos/play 校验 SVIP 或视频次数权益后下发短期签名 URL。
已接入的 provider:
local_dev:本地开发占位签名,便于前后端联调。aliyun_oss:阿里云 OSS 官方 Node.js SDKsignatureUrl。tencent_cos:腾讯云 COS XML API V5 签名 URL,服务端用 Nodecrypto实现,避免引入当前 COS Node SDK 的高危依赖。supabase_storage:Supabase Storage 官方createSignedUploadUrl/createSignedUrl语义。external_url:外部公开或厂商托管 URL,只允许作为已管理资源的下载地址,不支持后端直传签名。
API
后台申请上传签名:
POST /api/tenant-content/assets/sign-upload
后台登记资源草稿:
PUT /api/tenant-content/assets
后台确认上传:
POST /api/tenant-content/assets/confirm-upload
学生端下载:
GET /api/catalog/assets/download?assetId=...
学生端预览:
GET /api/catalog/assets/preview?assetId=...
学生端视频播放:
POST /api/videos/play
后台查看资源访问事件:
GET /api/tenant-content/assets/access-events?assetId=...
后台查看资源安全扫描事件:
GET /api/tenant-content/assets/security-scan-events?assetId=...
后台管理员下载:
POST /api/tenant-content/assets/sign-download
后台管理员预览:
POST /api/tenant-content/assets/sign-preview
后台资源复检 worker:
npm --workspace @tiku-saas/worker run assets:once
题库 PDF/Word/每日一练 ZIP 导出 worker:
npm --workspace @tiku-saas/worker run exports:once
标准上传流程
后台前端上传 PDF、图片、视频或资料包时必须走下面流程:
- 调用
POST /api/tenant-content/assets/sign-upload申请短期上传 URL。 - 前端使用返回的
upload.url和upload.headers直传对象存储。 - 调用
PUT /api/tenant-content/assets登记资源台账。托管对象默认进入status=draft、uploadStatus=pending。 - 调用
POST /api/tenant-content/assets/confirm-upload,由后端读取对象元数据并比对大小、MIME、SHA-256。 - 校验通过后,后端只会置为
uploadStatus=verified、securityScanStatus=pending,并继续保持status=draft。即使请求里传publish=true,也不会绕过安全扫描直接发布。 - 运行 assets worker。worker 会先复检对象元数据,再执行内置
metadata_rules安全扫描,检查 object key、MIME allowlist、文件大小、扩展名/MIME 是否匹配等规则。 - 扫描通过后,资源变为
securityScanStatus=passed,后台再调用PUT /api/tenant-content/assets将status=active发布。 - 学生端只能下载或预览
active + uploadStatus=verified + securityScanStatus=passed的托管对象资源;后台管理员下载/预览也执行同一安全扫描门禁。 - 学生端和后台管理员下载/预览都会写入
content_asset_access_events,包含assetId/userId/accessType/result/expiresInSec/signatureMode/ip/userAgent等审计字段。 - 生产环境定时运行 assets worker,复检
pending/verified托管对象的大小、MIME、SHA-256 等元数据。 - 如果复检发现对象丢失、跨租户 objectKey、大小/MIME/checksum 不一致,worker 会把资源置为
uploadStatus=failed、securityScanStatus=skipped,并将active资源退回draft,同时写入security_flags.assetRecheckFailed=true、content_asset_security_scan_events和audit_logs。
题库导出生成资源
题库导出 PDF/Word/每日一练 ZIP 不走前端上传,而是由后端和 worker 自动进入资源台账:
- 租户后台调用
POST /api/tenant-content/exports/questions,format=pdf、format=docx或format=daily_practice_zip。 - API 校验租户内容权限、导出范围和答案/解析开关,创建
content_export_jobs.status=pending。 apps/worker --job exports抢占 pending job,复用后端导出 payload 规则渲染 PDF/Word,或为每日一练生成 ZIP 素材包;PDF/Word 会应用options.watermarkText水印,ZIP 包会包含 PNG/SVG 卡片、拼图、manifest 和脱敏 payload。- worker 将二进制文件写入配置的对象存储。
local_dev会写入EXPORT_LOCAL_STORAGE_ROOT,生产建议使用supabase_storage、aliyun_oss或tencent_cos。 - worker 创建
content_assets,设置uploadStatus=verified、securityScanStatus=passed、securityScanProvider=trusted_export_worker,并把assetId、文件名、大小和 SHA-256 回填到content_export_jobs.output_metadata。 - 前端轮询
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 预览。
确认上传示例:
{
"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,便于租户后台定位资源异常。 publish=true只作为迁移期兼容参数保留。确认上传成功后仍然会保持草稿,必须等securityScanStatus=passed后再由后台显式发布。- 新上传托管对象常见状态流为:
draft/pending/pending->draft/verified/pending->draft/verified/passed->active/verified/passed。
安全规则
- 只有租户内容维护权限用户可以申请上传签名。
objectKey默认必须以当前tenantId/开头,防止跨租户覆盖或读取。- 禁止
..、反斜杠、编码斜杠等危险 object key。 - 上传会校验 MIME 类型和文件大小。
- 托管对象资源未确认或安全扫描未通过前不能发布为
active,学生端不可下载。 - 托管对象安全扫描失败会返回
ASSET_SECURITY_SCAN_FAILED;仍在等待扫描或扫描被跳过会返回ASSET_SECURITY_SCAN_REQUIRED。 - 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、securityScanStatus!=passed或status=draft的资源继续展示为可下载;列表仍返回时应展示“资料处理中”“安全扫描中”或“资源异常已下架”,真正下载/预览会被后端拒绝。
安全扫描字段
资源台账 content_assets 新增安全扫描状态:
| 字段 | 说明 |
|---|---|
security_scan_status |
not_required/pending/scanning/passed/failed/skipped |
security_scanned_at |
最近一次扫描完成时间 |
security_scan_provider |
扫描来源,例如 metadata_rules、trusted_export_worker |
security_scan_summary |
风险等级、问题码、扫描证据和失败原因 |
当前内置 provider 是 metadata_rules。它不是完整杀毒引擎,但能阻断明显危险或不合规对象:跨租户 key、非法 object key、超限文件、MIME allowlist 外文件、扩展名/MIME 不匹配,以及测试/运营标记的强制失败。商用生产仍需继续接入真实 AV/内容安全 provider,并将结果写入同一张事件表。
扫描事件表:
public.content_asset_security_scan_events
后台接口:
GET /api/tenant-content/assets/security-scan-events?assetId=<assetId>&limit=100
事件字段:
| 字段 | 说明 |
|---|---|
asset_id |
资源 ID |
provider |
扫描 provider |
scan_status |
pending/scanning/passed/failed/skipped |
risk_level |
none/low/medium/high/critical |
issue_codes |
问题码数组,例如 file_extension_mime_mismatch |
details |
observed/declared metadata、扩展名、错误信息等证据 |
访问事件字段
资源访问事件表:
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 |
签名摘要、对象位置、文件名等排查信息;不保存云厂商密钥 |
后台接口:
GET /api/tenant-content/assets/access-events?assetId=<assetId>&limit=100
前端只用于后台审计和排查,不要把 content_asset_access_events 当成学生端下载列表来源。
环境变量
通用:
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:
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:
TENCENT_COS_REGION=ap-shanghai
TENCENT_COS_APP_ID=
TENCENT_COS_SECRET_ID=
TENCENT_COS_SECRET_KEY=
TENCENT_COS_SECURITY_TOKEN=
Supabase Storage:
SUPABASE_STORAGE_URL=https://your-project.supabase.co/storage/v1
SUPABASE_STORAGE_SERVICE_KEY=
assets worker:
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 下发前端。