Files
gongxue-base/docs/refactor/object-storage-production-runbook.md
2026-07-01 06:09:23 +08:00

216 lines
8.8 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.

# 对象存储生产验收 Runbook
更新时间2026-06-30
本系统的题图、PDF、视频、音频、资料包、题库导出文件都必须进入 `content_assets` 台账,并由 `apps/api` 做权限判断、短期签名、水印和审计。前端不得直接持有云厂商密钥、service role key、私有 bucket 路径或长期私有资源 URL。
## 官方能力依据
- 阿里云 OSS 官方文档说明可由服务端生成预签名 URL客户端用该 URL 上传或下载对象,并通过有效期限制访问时间。阿里云也建议在更复杂的大文件场景中评估 STS 授权直传。参考:[OSS 预签名 URL 上传](https://help.aliyun.com/zh/oss/user-guide/upload-files-using-presigned-urls)、[OSS 预签名 URL 下载/预览](https://www.alibabacloud.com/help/zh/oss/user-guide/how-to-obtain-the-url-of-a-single-object-or-the-urls-of-multiple-objects)。
- 腾讯云 COS 官方文档说明预签名 URL 可用于临时上传/下载私有对象URL 中携带签名和有效期;官方也建议签名有效期设置为完成本次操作所需的最短期限。参考:[COS 预签名 URL 访问](https://cloud.tencent.com/document/product/436/68284)、[COS 预签名授权上传](https://cloud.tencent.com/document/product/436/14114)。
- Supabase Storage 官方文档说明 `createSignedUrl` 可以按秒设置下载 URL 有效期,访问私有对象需要相应 `select` 权限;本项目由后端 service key 集中签名,前端只拿短期 URL。参考[Supabase Storage signed URL](https://supabase.com/docs/reference/javascript/storage-from-createsignedurl)、[Supabase Storage downloads](https://supabase.com/docs/guides/storage/serving/downloads)。
## 生产阻断项
上线前必须满足:
- `NODE_ENV=production` 下 API 和 worker 都不能使用 `STORAGE_DEFAULT_PROVIDER=local_dev`
- `STORAGE_DEFAULT_BUCKET` 必须配置。
- `STORAGE_REQUIRE_TENANT_PREFIX=true` 必须保持开启。
- 阿里云 OSS 必须配置 `ALIYUN_OSS_REGION`、官方 HTTPS `aliyuncs.com` `ALIYUN_OSS_ENDPOINT`,以及 AccessKey生产禁止 `ALIYUN_OSS_INTERNAL=true` 用于面向用户的签名 URL。
- 腾讯云 COS 必须配置 `TENCENT_COS_REGION``TENCENT_COS_APP_ID``TENCENT_COS_SECRET_ID``TENCENT_COS_SECRET_KEY`
- Supabase Storage 必须配置 `SUPABASE_STORAGE_URL``SUPABASE_STORAGE_SERVICE_KEY`
- `WORKER_ASSET_SECURITY_SCANNER` 生产必须包含 `http`,例如 `metadata_rules,http`
- `WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT` 必须是 HTTPS 生产地址。
- `WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN` 必须是强随机密钥。
- `WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false`,外部扫描服务不可用时必须 fail-closed。
对应自动化命令:
```bash
npm run test:readiness
npm run readiness:production
npm run readiness:production:db
```
其中 `test:readiness` 会同时验证 production readiness 脚本和 API/worker 的生产配置 fail-fast。
## 推荐环境变量模板
阿里云 OSS
```text
NODE_ENV=production
STORAGE_DEFAULT_PROVIDER=aliyun_oss
STORAGE_DEFAULT_BUCKET=tiku-assets-prod
STORAGE_REQUIRE_TENANT_PREFIX=true
ALIYUN_OSS_REGION=cn-hangzhou
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
ALIYUN_OSS_ACCESS_KEY_ID=...
ALIYUN_OSS_ACCESS_KEY_SECRET=...
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.example.com/api/scan
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=...
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
```
腾讯云 COS
```text
NODE_ENV=production
STORAGE_DEFAULT_PROVIDER=tencent_cos
STORAGE_DEFAULT_BUCKET=tiku-assets-prod
STORAGE_REQUIRE_TENANT_PREFIX=true
TENCENT_COS_REGION=ap-shanghai
TENCENT_COS_APP_ID=...
TENCENT_COS_SECRET_ID=...
TENCENT_COS_SECRET_KEY=...
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.example.com/api/scan
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=...
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
```
Supabase Storage
```text
NODE_ENV=production
STORAGE_DEFAULT_PROVIDER=supabase_storage
STORAGE_DEFAULT_BUCKET=tiku-assets-prod
STORAGE_REQUIRE_TENANT_PREFIX=true
SUPABASE_STORAGE_URL=https://<project-ref>.supabase.co/storage/v1
SUPABASE_STORAGE_SERVICE_KEY=...
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.example.com/api/scan
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=...
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
```
## 云侧配置验收
以下配置无法仅靠仓库代码证明,必须在云控制台或云 API 上确认:
| 项目 | 验收标准 |
| --- | --- |
| Bucket 权限 | 默认私有;公开 bucket 只允许存放确认为公开的品牌素材 |
| CORS | 只允许学生端 H5、租户后台 H5、平台后台 H5 域名和微信小程序合法域名;允许必要方法 `GET/HEAD/PUT` 和必要 headers |
| RAM/CAM 权限 | 服务端密钥最小权限,仅允许目标 bucket 的指定前缀读写和 HEAD不要使用主账号密钥 |
| 租户前缀 | 对象 key 必须以 `{tenantId}/` 开头bucket policy 不应允许跨前缀写入 |
| CDN/防盗链 | 私有资料、SVIP 资料、视频、资料包禁止普通长效 CDN URL如使用 CDN必须由 provider 侧签名或回源鉴权 |
| 生命周期 | 临时上传、导出中间文件、失败扫描资源设置清理规则;正式资料按业务保留周期设置归档或低频 |
| 版本控制/备份 | 生产 bucket 开启版本控制、跨区域复制或定时备份,满足误删和容灾要求 |
| 日志审计 | 开启对象访问日志或云审计,至少保留一个完整售后周期 |
| 内容安全 | 外部 scanner 真实接入,并覆盖 PDF、图片、压缩包、视频封面或转码前文件 |
## 联调步骤
1. 启动 API 和 worker确保不是 `local_dev` provider。
```bash
npm run build:api
npm run build:worker
npm run readiness:production
npm run readiness:production:db
```
2. 租户后台申请上传签名:
```text
POST /api/tenant-content/assets/sign-upload
```
验收点:
- 返回 URL 为目标云厂商域名或 Supabase Storage 域名。
- `headers` 包含上传所需 `content-type`
- `objectKey` 带当前 `tenantId/` 前缀。
- `content_asset_access_events` 写入 `upload_sign`
3. 前端直传云存储后调用确认上传:
```text
POST /api/tenant-content/assets/confirm-upload
```
验收点:
- 后端读取云对象元数据。
- 大小、MIME、checksum 能对上。
- 状态变为 `uploadStatus=verified``securityScanStatus=pending`
- 即使传 `publish=true`,也不能绕过扫描直接发布。
4. 运行 assets worker
```bash
npm --workspace @tiku-saas/worker run assets:once
```
验收点:
- 外部 scanner 收到请求。
- scanner token 未进入前端响应。
- 扫描通过后 `securityScanStatus=passed`
- 扫描失败、超时或服务不可用时资源退回 `draft`,并记录 `content_asset_security_scan_events`
5. 后台发布资源:
```text
PUT /api/tenant-content/assets
```
验收点:
- 只有 `verified + passed` 的托管对象能发布为 `active`
- `failed/skipped/pending` 资源发布被拒绝。
6. 学生端预览/下载:
```text
GET /api/catalog/assets/preview?assetId=...
GET /api/catalog/assets/download?assetId=...
```
验收点:
- 未登录、无权益、跨租户、扫描未通过都被拒绝。
- 私有/SVIP/视频/资料包 TTL 不超过 300 秒。
- 响应包含 `watermark.mode=visible_overlay``traceId`
- `content_asset_access_events` 记录 granted/denied、TTL、signatureMode、watermark traceId。
7. 题库导出 worker
```bash
npm --workspace @tiku-saas/worker run exports:once
```
验收点:
- PDF/Word/每日一练 ZIP 写入对象存储。
- 自动创建 `content_assets`
- `securityScanProvider=trusted_export_worker`
- 后台通过 `sign-download``sign-preview` 取短签名 URL。
## 抽样清单
每次生产联调至少抽样:
- PDF 10 个,含大文件、中文文件名、水印预览。
- 图片 20 张,含题图、手册图、品牌图。
- 视频 10 个,含有会员限制和播放次数限制的题目视频。
- ZIP/资料包 5 个,含每日一练导出包。
- 扫描失败样本 3 个,确认不能发布、不能下载。
- 跨租户访问 5 组,确认无法签名下载或预览。
- 私有 CDN URL 样本 3 个,未标记 provider-managed 时必须拒绝。
## 准出标准
满足以下条件后,才允许把资料、题图、视频迁到生产对象存储:
- `npm run test:readiness` 通过。
- `npm run readiness:production` 无 blocker。
- `npm run readiness:production:db` 无 blocker。
- `npm run test:worker:assets` 通过。
- API/worker 在 `NODE_ENV=production` 下无法用 `local_dev` 启动。
- 真实云存储上传、确认、扫描、发布、预览、下载、导出全链路通过。
- 云侧 CORS、防盗链、生命周期、备份/版本控制、访问日志有截图或变更记录。