diff --git a/docs/API-MAINLINE.md b/docs/API-MAINLINE.md index edd46a1b8..9ae399396 100644 --- a/docs/API-MAINLINE.md +++ b/docs/API-MAINLINE.md @@ -117,8 +117,9 @@ |--------|------|------|------| | POST | `/` | 上传文件 | ✅ ACTIVE | -**Use Cases**: -- 直接调用 `MinIOService` +**Use Cases / Services**: +- `SubmitIngestJobUseCase` +- `OSSStorageService`(经 `get_storage_service` 注入) --- diff --git a/docs/CODE-STATUS.md b/docs/CODE-STATUS.md index 73fea1c82..fd2b69d12 100644 --- a/docs/CODE-STATUS.md +++ b/docs/CODE-STATUS.md @@ -259,7 +259,7 @@ apps/web/src/ ``` apps/api/app/api/routes/upload.py (API) ↓ -apps/api/app/core/storage.py (MinIO) +apps/api/app/core/storage.py (OSS / generated-files fallback) ↓ packages/domain/entities.py::Asset (领域模型) ↓ diff --git a/docs/DATABASE-SWITCH.md b/docs/DATABASE-SWITCH.md index 7bebe45e9..17e23eb1d 100644 --- a/docs/DATABASE-SWITCH.md +++ b/docs/DATABASE-SWITCH.md @@ -99,10 +99,9 @@ uvicorn apps.api.main:app --reload \q ``` -3. **运行迁移** +3. **运行 Alembic 迁移** ```bash - psql postgresql://xiaoxia_user:password@localhost:5432/xiaoxia_saas \ - -f migrations/001_initial_schema.sql + alembic upgrade head ``` 4. **配置环境变量** @@ -120,17 +119,17 @@ uvicorn apps.api.main:app --reload ## 🐳 Docker 环境 -### docker-compose(自动配置 PostgreSQL) +### Canonical Docker Compose(自动配置 PostgreSQL) ```bash # 自动启动 PostgreSQL 和应用 -docker-compose up -d +WEB_PORT=3001 docker compose -f infra/docker/compose.yml up -d --build # 查看日志 -docker-compose logs -f api +docker compose -f infra/docker/compose.yml logs -f api ``` -`docker-compose.yml` 默认使用 PostgreSQL。 +`infra/docker/compose.yml` 是当前唯一有效 compose 入口;根目录 `docker-compose.yml` 已退役。 --- diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index b5a958852..ec95a4041 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -78,6 +78,16 @@ Staging 当前可以保持 no-op;Production 开启前必须先验证 SMTP/Redi --- +## 生成文件存储与保留 + +- Staging 未配置 OSS 凭证时,生成视频落盘到 `/var/lib/xiaoxia-saas-staging/generated`,并通过 Nginx `/generated-files/` 公开访问。 +- Production 优先使用 OSS;若临时启用本地 fallback,必须配置独立持久化目录、Nginx 只读公开路径和磁盘告警。 +- 保留策略建议:staging 生成文件保留 7 天或保留最近 20GB;production 按业务套餐/订单状态定义,禁止无上限增长。 +- 清理脚本上线前必须先 dry-run 输出待删列表,再按 workspace/project 维度删除,避免误删仍被 GeneratedVideo 记录引用的文件。 +- 当前脚本:`python scripts/cleanup_generated_files.py --dir /var/lib/xiaoxia-saas-staging/generated --days 7` 仅 dry-run;确认后再加 `--apply`。 + +--- + ## 生产发布清单 生产发布前必须按 `docs/PRODUCTION-RELEASE-CHECKLIST.md` 执行:先备份,后 Alembic,最后 smoke;禁止靠临场记忆操作数据库。 diff --git a/docs/DOCKER-DEPLOYMENT.md b/docs/DOCKER-DEPLOYMENT.md index dfb6e6878..47bc0192c 100644 --- a/docs/DOCKER-DEPLOYMENT.md +++ b/docs/DOCKER-DEPLOYMENT.md @@ -1,5 +1,7 @@ # 小虾 SaaS Docker 部署指南 +> ⚠️ 历史文档,仅供归档参考。当前 canonical 部署入口是 `infra/docker/compose.yml` 和 `infra/docker/deploy-staging.sh`;生产发布必须按 `docs/PRODUCTION-RELEASE-CHECKLIST.md` 执行。 + ## 🐳 快速启动 ### 1. 准备环境变量 diff --git a/docs/PRODUCTION-CHECKLIST.md b/docs/PRODUCTION-CHECKLIST.md index 55e4caf08..7cf67bedc 100644 --- a/docs/PRODUCTION-CHECKLIST.md +++ b/docs/PRODUCTION-CHECKLIST.md @@ -211,14 +211,13 @@ redis-cli -u $REDIS_URL ping python scripts/test_email.py # 6. 检查 Docker 镜像 -docker build -t xiaoxia-saas:latest . -docker run --rm xiaoxia-saas:latest python -c "import apps.api.main" +WEB_PORT=3001 docker compose -f infra/docker/compose.yml build api worker web # 7. 检查数据库迁移 -psql $DATABASE_URL -f migrations/001_initial_schema.sql +alembic upgrade head # 8. 启动服务测试 -docker-compose up -d +WEB_PORT=3001 docker compose -f infra/docker/compose.yml up -d curl http://localhost:8000/health ``` diff --git a/scripts/cleanup_generated_files.py b/scripts/cleanup_generated_files.py new file mode 100644 index 000000000..fe102b79f --- /dev/null +++ b/scripts/cleanup_generated_files.py @@ -0,0 +1,75 @@ +"""Clean local generated-files storage with a dry-run-first workflow.""" + +from __future__ import annotations + +import argparse +import os +import time +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--dir", + default=os.getenv("GENERATED_FILES_DIR", "/app/generated"), + help="Generated files root directory. Defaults to GENERATED_FILES_DIR or /app/generated.", + ) + parser.add_argument( + "--days", + type=int, + default=7, + help="Delete files older than this many days. Defaults to 7.", + ) + parser.add_argument( + "--apply", + action="store_true", + help="Actually delete files. Without this flag the script only prints a dry run.", + ) + return parser.parse_args() + + +def iter_expired_files(root: Path, cutoff_epoch: float): + for path in root.rglob("*"): + if path.is_file() and path.stat().st_mtime < cutoff_epoch: + yield path + + +def remove_empty_dirs(root: Path) -> None: + for path in sorted((p for p in root.rglob("*") if p.is_dir()), key=lambda p: len(p.parts), reverse=True): + try: + path.rmdir() + except OSError: + pass + + +def main() -> int: + args = parse_args() + root = Path(args.dir).resolve() + if not root.exists(): + print(f"generated files directory does not exist: {root}") + return 0 + if not root.is_dir(): + raise SystemExit(f"not a directory: {root}") + if args.days < 1: + raise SystemExit("--days must be >= 1") + + cutoff_epoch = time.time() - args.days * 24 * 60 * 60 + expired_files = list(iter_expired_files(root, cutoff_epoch)) + total_bytes = sum(path.stat().st_size for path in expired_files) + + mode = "APPLY" if args.apply else "DRY-RUN" + print(f"mode={mode} root={root} days={args.days} files={len(expired_files)} bytes={total_bytes}") + for path in expired_files: + print(path) + if args.apply: + path.unlink() + + if args.apply: + remove_empty_dirs(root) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())