diff --git a/README.md b/README.md index 905efab5c..d2189ef3e 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,68 @@ -# 小虾 SaaS - 自动化剪辑 SaaS 平台 +# 小虾 SaaS - 自动化视频剪辑平台 -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) [![FastAPI](https://img.shields.io/badge/FastAPI-0.115.0-009688.svg)](https://fastapi.tiangolo.com) [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-336791.svg)](https://www.postgresql.org/) -一个功能完整、生产就绪的多租户 SaaS 平台,专为自动化视频剪辑服务设计。 +自动化视频剪辑 SaaS 平台,支持素材上传、AI 分类、智能剪辑计划生成、自动化视频合成与成片管理。 --- -## ✨ 核心特性 +## ✨ 核心功能 -### 🎬 视频剪辑主链路(Phase 7 已完成) -- 素材上传与管理 -- AI 智能分类 -- 自动化视频生成 -- 成片下载与管理 +### 🎬 视频剪辑主链路 +- 素材上传(直传 OSS + 分片上传大文件,最大 2GB) +- AI 智能分类与质量评分 +- 4 种剪辑模式:one_take / pip(画中画)/ voice_over(口播+B-roll)/ voice_pip +- 剪辑计划模板 + 智能生成 +- 自动化视频合成任务(Celery 异步) +- 成片下载与审核管理 +- 资产诊断(素材就绪度评估、缺口分析) -### 🔐 完整的认证系统 -- JWT 认证(access + refresh token) -- 邮箱验证和密码重置 -- Session 管理 +### 🔐 认证系统 +- JWT Bearer Token 认证 +- 邮箱注册 + 邮箱验证 +- 密码重置(邮箱找回) - bcrypt 密码加密 -### 🏢 多租户架构 -- 工作空间隔离 -- 团队成员管理 -- 基于角色的权限控制(Owner/Admin/Member/Viewer) -- 邀请和审批流程 +### 📋 项目管理 +- 项目 CRUD + 共享 +- 任务管理(创建/更新/状态流转/进度追踪) +- 里程碑管理 +- 任务问题追踪 -### 💸 订阅管理 -- 3 级订阅计划(Free/Pro/Enterprise) -- 配额管理(项目数/存储空间) -- 升级和取消订阅 - -### ⚡ 高性能 -- 数据库连接池(5-6x 性能提升) -- 请求日志和监控 -- 慢查询检测 -- 健康检查(Kubernetes 就绪) +### 📊 素材库管理 +- 素材库创建与管理 +- 素材上传、审核状态流转(pending_review → approved/rejected) +- 素材诊断(就绪度评分、缺口分析、智能视图) --- ## 🚀 快速开始 -### 方式 1: Docker(推荐) +### 方式 1: Docker Compose(推荐) ```bash # 1. 克隆仓库 -git clone https://github.com/your-org/xiaoxia-saas.git +git clone https://git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas.git cd xiaoxia-saas -# 2. 启动所有服务 +# 2. 配置环境变量 +cp .env.example .env +# 编辑 .env 填写数据库、Redis、OSS 等配置 + +# 3. 启动所有服务 docker-compose up -d -# 3. 访问 API 文档 +# 4. 访问 API 文档 open http://localhost:8000/docs ``` -就这么简单!🎉 - ### 方式 2: 本地开发 ```bash # 1. 克隆仓库 -git clone https://github.com/your-org/xiaoxia-saas.git +git clone https://git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas.git cd xiaoxia-saas # 2. 创建虚拟环境 @@ -74,36 +72,90 @@ source venv/bin/activate # Windows: venv\Scripts\activate # 3. 安装依赖 pip install -r requirements.txt -# 4. 启动开发服务器 +# 4. 配置环境变量 +cp .env.example .env + +# 5. 启动 API 服务 uvicorn apps.api.main:app --reload -# 5. 访问 API 文档 +# 6. 访问 API 文档 open http://localhost:8000/docs ``` --- -## 📚 主要文档 +## 📚 API 文档 -### 快速入口 -- **[API 主线清单](docs/API-MAINLINE.md)** - 所有 API endpoints 总览 -- **[代码状态标注](docs/CODE-STATUS.md)** - 代码库导航指南 -- **[API 使用指南](docs/API-GUIDE.md)** - API 详细用法 +### 交互式文档 +- **Swagger UI**: https://saas-api.xiaoxiajianji.com/docs +- **OpenAPI Schema**: https://saas-api.xiaoxiajianji.com/openapi.json -### Phase 7 交付 -- **[Phase 7 进度](docs/PHASE7-PROGRESS.md)** - 核心视频剪辑业务已完成 -- **[Phase 7 设计](docs/PHASE7-DESIGN.md)** - 设计文档 +### 核心 API 路径 -### 基础设施 -- **[Docker 部署指南](docs/DOCKER-DEPLOYMENT.md)** -- **[CI/CD 文档](docs/CI-CD.md)** -- **[性能监控指南](docs/PERFORMANCE-MONITORING.md)** -- **[健康检查指南](docs/HEALTH-CHECKS.md)** +**认证** (`/api/v1/auth`) -### 开发规范 -- **[Git 工作流](docs/GIT-WORKFLOW.md)** -- **[环境配置指南](docs/ENVIRONMENT-CONFIG.md)** -- **[贡献指南](CONTRIBUTING.md)** +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/register` | 用户注册 | +| POST | `/login` | 用户登录 | +| GET | `/me` | 获取当前用户信息 | +| POST | `/password/forgot` | 忘记密码 | +| POST | `/password/reset` | 重置密码 | + +**视频剪辑主链路** + +``` +上传素材 → POST /api/v1/upload(直传)或 /api/v1/upload/chunk/init(分片) + ↓ +创建素材 → POST /api/v1/assets + ↓ +AI 分类 → POST /api/v1/classification-jobs + ↓ +生成剪辑计划 → POST /api/v1/projects/{id}/edit-plans/auto-generate + ↓ +创建生成任务 → POST /api/v1/generation/tasks/ + ↓ +查询结果 → GET /api/v1/generation/tasks/{task_id}/results/ + ↓ +获取成片 → GET /api/v1/generated-videos/{video_id}/download-url +``` + +**项目管理** (`/api/v1/project-management`) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET/POST | `/tasks` | 任务列表/创建 | +| PATCH | `/tasks/{id}` | 更新任务信息 | +| PATCH | `/tasks/{id}/status` | 更新任务状态 | +| PATCH | `/tasks/{id}/progress` | 更新任务进度 | +| GET/POST | `/milestones` | 里程碑列表/创建 | +| GET/POST | `/issues` | 问题列表/创建 | +| PATCH | `/issues/{id}/resolve` | 解决问题 | + +**素材与上传** + +| 方法 | 路径 | 说明 | +|------|------|------| +| POST | `/api/v1/upload` | 直传素材(multipart/form-data) | +| POST | `/api/v1/upload/direct/prepare` | 准备 OSS 直传签名 | +| POST | `/api/v1/upload/direct/complete` | 确认直传完成 | +| POST | `/api/v1/upload/chunk/init` | 初始化分片上传 | +| POST | `/api/v1/upload/chunk/{id}/{index}` | 上传分片 | +| POST | `/api/v1/upload/chunk/{id}/complete` | 完成分片上传 | +| GET | `/api/v1/assets` | 素材列表 | +| PATCH | `/api/v1/assets/{id}/review` | 更新素材审核状态 | +| GET | `/api/v1/projects/{id}/asset-diagnosis` | 资产诊断 | + +**成片管理** (`/api/v1/generated-videos`) + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/` | 成片列表 | +| GET | `/{video_id}` | 成片详情 | +| GET | `/{video_id}/download-url` | 下载链接 | +| PATCH | `/{video_id}/review` | 审核状态 | + +完整 API 列表请查看 [API 主线清单](docs/API-MAINLINE.md) --- @@ -111,61 +163,49 @@ open http://localhost:8000/docs ``` 小虾 SaaS -├── packages/ # 核心业务逻辑 -│ ├── domain/ # 领域模型 -│ ├── application/ # 用例 -│ ├── ports/ # 接口定义 -│ └── adapters/ # 适配器实现 +├── packages/ # 核心业务逻辑(Clean Architecture) +│ ├── domain/ # 领域模型(dataclass) +│ ├── application/ # 用例(Use Cases) +│ ├── ports/ # 接口定义(抽象端口) +│ └── adapters/ # 适配器实现(SQLAlchemy、Redis、SMTP 等) ├── apps/ # 应用层 -│ ├── api/ # FastAPI 应用 -│ ├── web/ # React 前端 -│ └── worker/ # Celery worker -├── migrations/ # 数据库迁移 +│ ├── api/ # FastAPI 应用 + 路由 + Pydantic schemas +│ ├── web/ # React + Vite 前端 +│ └── worker/ # Celery 异步任务(视频处理、分类等) +├── migrations/ # Alembic 数据库迁移 ├── tests/ # 测试 │ ├── unit/ # 单元测试 -│ └── integration/ # 集成测试 +│ ├── integration/ # 集成测试 +│ └── e2e/ # 端到端测试 └── docs/ # 文档 ``` **设计模式:** -- Clean Architecture -- 依赖注入 -- Repository 模式 +- Clean Architecture(依赖方向:外层 → 内层) +- 依赖注入(FastAPI Depends) +- Repository 模式(通过 ports 抽象) - Domain-Driven Design --- -## 🔑 核心 API 路径 +## 🛠️ 技术栈 -### 视频剪辑主链路(Phase 7) +**后端:** +- Python 3.12 + FastAPI 0.115.0 +- PostgreSQL 16(生产) +- Redis 7(缓存 + Celery Broker) +- Celery(异步任务:视频处理、素材导入、分类) +- 阿里云 OSS(文件存储) -``` -上传素材 - ↓ -POST /api/v1/upload +**前端:** +- React 18 + TypeScript +- Vite(构建工具) +- Ant Design(UI 组件) -创建 Asset - ↓ -POST /api/v1/assets - -发起分类 - ↓ -POST /api/v1/classification-jobs - -创建生成任务 - ↓ -POST /api/v1/generation/tasks - -查询生成结果 - ↓ -GET /api/v1/generation/tasks/{task_id}/results - -获取下载链接 - ↓ -GET /api/v1/generated-videos/{video_id}/download-url -``` - -完整 API 列表请查看 [API 主线清单](docs/API-MAINLINE.md) +**部署:** +- Docker + Docker Compose +- Gitea + Gitea Actions(CI/CD) +- Nginx(反向代理) --- @@ -185,51 +225,32 @@ pytest tests/integration -v pytest --cov=packages --cov-report=html ``` -**测试统计:** -- 单元测试: 17+ 个 ✅ -- 集成测试: 6+ 个 ✅ -- 测试覆盖率: 持续提升中 - ---- - -## 🛠️ 技术栈 - -**后端:** -- Python 3.12 -- FastAPI 0.115.0 -- PostgreSQL 16 -- Redis 7 -- Celery -- MinIO - -**前端:** -- React 18 -- TypeScript -- Vite -- Ant Design - -**测试:** -- pytest -- pytest-asyncio -- pytest-cov - -**部署:** -- Docker -- Docker Compose -- Gitea Actions - --- ## 📊 当前状态 | 模块 | 状态 | |------|------| -| Phase 7(视频剪辑主链) | ✅ 已完成 | -| CI/CD 稳定性 | ✅ 已修复 | -| 认证系统 | ✅ 完整 | -| 多租户架构 | ✅ 完整 | -| 订阅系统 | ✅ 完整 | -| 前端联调 | ✅ 完成 | +| 视频剪辑主链路(Phase 7) | ✅ 已完成 | +| 分片上传(最大 2GB) | ✅ 已完成 | +| 4 种剪辑模式 | ✅ 已完成 | +| 项目管理 + 任务追踪 | ✅ 已完成 | +| 资产诊断 | ✅ 已完成 | +| 认证系统(JWT) | ✅ 已完成 | +| CI/CD 流水线 | ✅ 运行中 | +| 前端界面(Vite) | ✅ 已完成 | + +--- + +## 📄 更多文档 + +- [API 主线清单](docs/API-MAINLINE.md) - 全部端点总览 +- [API 使用指南](docs/API-GUIDE.md) - 详细用法 +- [代码状态标注](docs/CODE-STATUS.md) - 代码库导航 +- [Docker 部署指南](docs/DOCKER-DEPLOYMENT.md) +- [CI/CD 文档](docs/CI-CD.md) +- [Git 工作流](docs/GIT-WORKFLOW.md) +- [环境配置指南](docs/ENVIRONMENT-CONFIG.md) --- @@ -237,43 +258,8 @@ pytest --cov=packages --cov-report=html 欢迎贡献!请查看 [贡献指南](CONTRIBUTING.md) -1. Fork 项目 -2. 创建分支 (`git checkout -b feature/AmazingFeature`) -3. 提交更改 (`git commit -m 'feat: Add some AmazingFeature'`) -4. 推送到分支 (`git push origin feature/AmazingFeature`) -5. 创建 Pull Request +**仓库地址**: https://git.xiaoxiajianji.com/xiaoxia/xiaoxia-saas --- -## 📄 许可证 - -本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情 - ---- - -## 📞 联系方式 - -- **问题反馈:** GitHub Issues -- **文档:** 参考 `docs/` 目录 - ---- - -## 🎉 致谢 - -感谢所有贡献者和使用者! - -**开发团队:** 小虾 🦐 - ---- - -**⭐ 如果这个项目对你有帮助,请给一个 Star!** - ---- - -**最后更新**: 2026-06-19 -**当前版本**: Phase 7 完成 -ci test Fri Jun 26 06:59:37 PM CST 2026 -debug test Fri Jun 26 07:05:38 PM CST 2026 -docker daemon restart test -19:08:39 - final test -19:10:17 - new runner test +**License**: MIT diff --git a/alembic/versions/008_add_video_dedup_fields.py b/alembic/versions/008_add_video_dedup_fields.py index bb0f9abd3..e304cff44 100644 --- a/alembic/versions/008_add_video_dedup_fields.py +++ b/alembic/versions/008_add_video_dedup_fields.py @@ -1,7 +1,7 @@ """Add video fingerprint and duplicate detection fields to generated_videos table. -Revision ID: 007 -Revises: 006 +Revision ID: 008 +Revises: 007 Create Date: 2024-06-26 """ import sqlalchemy as sa @@ -9,7 +9,7 @@ import sqlalchemy as sa from alembic import op revision = "008" -down_revision = "006" +down_revision = "007" branch_labels = None depends_on = None diff --git a/alembic/versions/009_remove_workspace_concept.py b/alembic/versions/009_remove_workspace_concept.py index a8b3da09a..d4b9b4ce7 100644 --- a/alembic/versions/009_remove_workspace_concept.py +++ b/alembic/versions/009_remove_workspace_concept.py @@ -1,6 +1,6 @@ """Remove workspace concept - Projects now directly under User -Revision ID: 007_remove_workspace_concept +Revision ID: 009 Revises: 008 Create Date: 2026-06-26 @@ -16,7 +16,7 @@ import sqlalchemy as sa from sqlalchemy import text # revision identifiers -revision = "007_remove_workspace_concept" +revision = "009" down_revision = "008" branch_labels = None depends_on = None diff --git a/apps/api/app/api/routes/auth.py b/apps/api/app/api/routes/auth.py index c3bf6c956..9a9d83ab3 100755 --- a/apps/api/app/api/routes/auth.py +++ b/apps/api/app/api/routes/auth.py @@ -166,7 +166,6 @@ async def verify_email_post( @router.post("/password/forgot", response_model=MessageResponse, status_code=status.HTTP_202_ACCEPTED) -@router.post("/forgot-password", response_model=MessageResponse, status_code=status.HTTP_202_ACCEPTED) async def forgot_password( request: PasswordResetRequestModel, user_repository: UserRepository = Depends(get_user_repository), @@ -184,7 +183,6 @@ async def forgot_password( @router.post("/password/reset", response_model=MessageResponse) -@router.post("/reset-password", response_model=MessageResponse) async def reset_password( request: ResetPasswordModel, user_repository: UserRepository = Depends(get_user_repository), diff --git a/apps/api/app/api/routes/project_management.py b/apps/api/app/api/routes/project_management.py index ee488b905..24a3765fc 100644 --- a/apps/api/app/api/routes/project_management.py +++ b/apps/api/app/api/routes/project_management.py @@ -51,7 +51,7 @@ def get_issue_repo(): class CreateTaskRequest(BaseModel): project_id: str - workspace_id: str + workspace_id: str = "" name: str description: str = "" priority: TaskPriority = TaskPriority.MEDIUM @@ -62,7 +62,7 @@ class CreateTaskRequest(BaseModel): class TaskResponse(BaseModel): id: str project_id: str - workspace_id: str + workspace_id: str = "" name: str description: str status: TaskStatus @@ -96,7 +96,7 @@ class UpdateTaskProgressRequest(BaseModel): class CreateMilestoneRequest(BaseModel): project_id: str - workspace_id: str + workspace_id: str = "" name: str description: str = "" @@ -104,7 +104,7 @@ class CreateMilestoneRequest(BaseModel): class MilestoneResponse(BaseModel): id: str project_id: str - workspace_id: str + workspace_id: str = "" name: str description: str target_date: datetime | None @@ -117,7 +117,7 @@ class MilestoneResponse(BaseModel): class CreateTaskIssueRequest(BaseModel): task_id: str project_id: str - workspace_id: str + workspace_id: str = "" title: str description: str = "" created_by_user_id: str = "" @@ -127,7 +127,7 @@ class TaskIssueResponse(BaseModel): id: str task_id: str project_id: str - workspace_id: str + workspace_id: str = "" title: str description: str resolved: bool diff --git a/apps/api/app/api/routes/projects.py b/apps/api/app/api/routes/projects.py index e77c9ab36..225b49716 100644 --- a/apps/api/app/api/routes/projects.py +++ b/apps/api/app/api/routes/projects.py @@ -47,7 +47,7 @@ def get_project( @router.get("", response_model=ListProjectsResponse) def list_projects( - workspace_id: str, + workspace_id: str = "", authenticated_user: AuthenticatedUser = Depends(get_current_user), project_repository: Any = Depends(get_project_repository), workspace_member_repository: WorkspaceMemberRepository = Depends(get_workspace_member_repository), diff --git a/apps/api/app/api/routes/upload.py b/apps/api/app/api/routes/upload.py index e542acccf..23ae253da 100644 --- a/apps/api/app/api/routes/upload.py +++ b/apps/api/app/api/routes/upload.py @@ -17,8 +17,11 @@ from app.schemas.upload import ( DirectUploadCompleteResponse, DirectUploadPrepareRequest, DirectUploadPrepareResponse, + UploadAssetRequest, UploadAssetResponse, ) +from typing import Annotated + from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile, status from packages.application import GetProjectUseCase, SubmitIngestJobCommand, SubmitIngestJobUseCase @@ -184,12 +187,15 @@ async def complete_direct_upload( return DirectUploadCompleteResponse(storage_key=normalized_key, ingest_job_id=job.id) -@router.post("", response_model=UploadAssetResponse) +@router.post( + "", + response_model=UploadAssetResponse, + summary="Upload Asset", + description="上传素材文件(multipart/form-data),支持视频、音频、图片。触发导入流水线自动处理。", +) async def upload_asset( file: UploadFile = File(..., description="要上传的文件(视频、音频、图片等)"), - workspace_id: str = Form(..., description="工作空间 ID"), - project_id: str = Form(..., description="项目 ID"), - library_id: str = Form(..., description="素材库 ID"), + form_data: Annotated[UploadAssetRequest, Form()], authenticated_user: AuthenticatedUser = Depends(get_current_user), ingest_job_repository: Any = Depends(get_ingest_job_repository), project_repository: Any = Depends(get_project_repository), @@ -198,6 +204,9 @@ async def upload_asset( storage_service: OSSStorageService = Depends(get_storage_service), ) -> UploadAssetResponse: """上传素材文件并触发导入流水线。""" + workspace_id = form_data.workspace_id + project_id = form_data.project_id + library_id = form_data.library_id require_workspace_member(workspace_id, authenticated_user, workspace_member_repository) _require_project_and_library(workspace_id, project_id, library_id, project_repository, asset_library_repository) diff --git a/apps/api/app/schemas/asset_diagnosis.py b/apps/api/app/schemas/asset_diagnosis.py index ef6647e27..0510c5ba4 100644 --- a/apps/api/app/schemas/asset_diagnosis.py +++ b/apps/api/app/schemas/asset_diagnosis.py @@ -16,7 +16,6 @@ class AssetGapItem(BaseModel): class ProjectAssetDiagnosisResponse(BaseModel): - workspace_id: str = "" project_id: str readiness_score: int readiness_label: str diff --git a/apps/api/app/schemas/edit_plan.py b/apps/api/app/schemas/edit_plan.py index 993c43bcb..b1059e7fa 100644 --- a/apps/api/app/schemas/edit_plan.py +++ b/apps/api/app/schemas/edit_plan.py @@ -5,7 +5,6 @@ from pydantic import BaseModel, Field class EditTemplateResponse(BaseModel): id: str - workspace_id: str = "" project_id: str name: str description: str @@ -28,7 +27,6 @@ class EditPlanClipResponse(BaseModel): class EditPlanResponse(BaseModel): id: str - workspace_id: str = "" project_id: str template_id: str asset_library_id: str @@ -42,7 +40,6 @@ class EditPlanResponse(BaseModel): class CreateEditPlanRequest(BaseModel): - workspace_id: str = "" asset_library_id: str template_id: str = "" title_id: str = "" @@ -50,7 +47,6 @@ class CreateEditPlanRequest(BaseModel): class AutoGenerateEditPlanRequest(BaseModel): """智能生成剪辑计划请求""" - workspace_id: str = "" asset_library_id: str editing_mode: str = Field( default="one_take", diff --git a/apps/api/app/schemas/project_title.py b/apps/api/app/schemas/project_title.py index 9311af9bf..59862cd98 100644 --- a/apps/api/app/schemas/project_title.py +++ b/apps/api/app/schemas/project_title.py @@ -8,7 +8,6 @@ TitleCategory = Literal["default", "marketing", "tutorial", "story", "promo"] class ProjectTitleResponse(BaseModel): id: str - workspace_id: str = "" project_id: str text: str category: str @@ -24,7 +23,6 @@ class ListProjectTitlesResponse(BaseModel): class CreateProjectTitleRequest(BaseModel): - workspace_id: str = "" text: str = Field(min_length=1, max_length=200) category: TitleCategory = "default" favorite: bool = False diff --git a/apps/api/app/schemas/task_center.py b/apps/api/app/schemas/task_center.py index 08fa63579..a7c5f2f9a 100644 --- a/apps/api/app/schemas/task_center.py +++ b/apps/api/app/schemas/task_center.py @@ -6,7 +6,6 @@ from pydantic import BaseModel, Field class ProjectTaskResponse(BaseModel): id: str task_type: str - workspace_id: str = "" project_id: str status: str progress: float diff --git a/apps/api/app/schemas/upload.py b/apps/api/app/schemas/upload.py index 8e36febfd..86cb25599 100644 --- a/apps/api/app/schemas/upload.py +++ b/apps/api/app/schemas/upload.py @@ -1,6 +1,13 @@ from pydantic import BaseModel, Field + +class UploadAssetRequest(BaseModel): + """素材上传请求(multipart form)""" + project_id: str = Field(..., min_length=1, description="项目 ID") + library_id: str = Field(..., min_length=1, description="素材库 ID") + workspace_id: str = Field(default="", description="工作空间 ID(已废弃,可传空)") + class UploadAssetResponse(BaseModel): storage_key: str ingest_job_id: str