feat: initial SaaS scaffold
- domain: User, Workspace, Project, AssetLibrary, Asset, IngestJob - ports: repository interfaces - application: use cases - adapters: in-memory - API: FastAPI 5 routes - worker: Celery ingest_asset - tests: 4 passing
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# Environment example
|
||||
APP_ENV=development
|
||||
APP_NAME=xiaoxia-saas
|
||||
API_HOST=0.0.0.0
|
||||
API_PORT=8000
|
||||
WEB_PORT=3000
|
||||
POSTGRES_URL=postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
OBJECT_STORAGE_PROVIDER=minio
|
||||
OBJECT_STORAGE_ENDPOINT=http://localhost:9000
|
||||
OBJECT_STORAGE_BUCKET=xiaoxia-saas
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
# Node / frontend
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
dist/
|
||||
coverage/
|
||||
|
||||
# Python / backend
|
||||
.venv/
|
||||
venv/
|
||||
__pycache__/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.pytype/
|
||||
ruff_cache/
|
||||
*.pyc
|
||||
|
||||
# Env / secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Logs / temp
|
||||
*.log
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
# Build / runtime artifacts
|
||||
build/
|
||||
.runtime/
|
||||
@@ -0,0 +1,27 @@
|
||||
# xiaoxia-saas
|
||||
|
||||
小虾自动剪辑 SaaS 新主线仓库。
|
||||
|
||||
## 目标
|
||||
|
||||
- 与旧桌面版彻底隔离
|
||||
- 按正规 SaaS 工程方式重建
|
||||
- 先按文档、架构、制度启动,再进入开发
|
||||
|
||||
## 目录
|
||||
|
||||
- `docs/`:产品、架构、流程、盘点和启动文档
|
||||
- `apps/web/`:前端 Web 应用
|
||||
- `apps/api/`:后端 API 应用
|
||||
- `apps/worker/`:后台任务执行应用
|
||||
- `packages/domain/`:核心业务对象与规则
|
||||
- `packages/application/`:用例层
|
||||
- `packages/ports/`:接口定义
|
||||
- `packages/adapters/`:外部实现
|
||||
- `packages/shared/`:共享基础模块
|
||||
- `infra/`:部署与运行基础设施
|
||||
- `tests/`:集成测试与端到端测试
|
||||
|
||||
## 当前状态
|
||||
|
||||
当前仓库已完成 SaaS 启动期文档骨架和目录骨架,下一步进入工程初始化。
|
||||
@@ -0,0 +1,15 @@
|
||||
# xiaoxia-saas API
|
||||
|
||||
## 结构
|
||||
|
||||
- `app/core/`:配置与基础能力
|
||||
- `app/api/`:路由组织
|
||||
- `app/schemas/`:请求响应模型
|
||||
- `app/dependencies.py`:依赖注入入口
|
||||
- `main.py`:FastAPI 启动入口
|
||||
|
||||
## 当前可用接口
|
||||
|
||||
- `GET /api/health`
|
||||
- `GET /api/projects?workspace_id=...`
|
||||
- `POST /api/projects`
|
||||
@@ -0,0 +1,3 @@
|
||||
from .main import app, create_app
|
||||
|
||||
__all__ = ["app", "create_app"]
|
||||
@@ -0,0 +1 @@
|
||||
"""API application package."""
|
||||
@@ -0,0 +1 @@
|
||||
"""API package."""
|
||||
@@ -0,0 +1,14 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.routes.asset_libraries import router as asset_libraries_router
|
||||
from app.api.routes.assets import router as assets_router
|
||||
from app.api.routes.health import router as health_router
|
||||
from app.api.routes.ingest_jobs import router as ingest_jobs_router
|
||||
from app.api.routes.projects import router as projects_router
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(health_router, prefix="/health", tags=["health"])
|
||||
api_router.include_router(projects_router, prefix="/projects", tags=["projects"])
|
||||
api_router.include_router(asset_libraries_router, prefix="/asset-libraries", tags=["asset-libraries"])
|
||||
api_router.include_router(assets_router, prefix="/assets", tags=["assets"])
|
||||
api_router.include_router(ingest_jobs_router, prefix="/ingest-jobs", tags=["ingest-jobs"])
|
||||
@@ -0,0 +1 @@
|
||||
"""Route modules."""
|
||||
@@ -0,0 +1,55 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.dependencies import get_asset_library_repository
|
||||
from app.schemas.asset_library import AssetLibraryResponse, CreateAssetLibraryRequest, ListAssetLibrariesResponse
|
||||
from packages.adapters.in_memory import InMemoryAssetLibraryRepository
|
||||
from packages.application import CreateAssetLibraryCommand, CreateAssetLibraryUseCase, ListAssetLibrariesUseCase
|
||||
from packages.domain import AssetLibraryKind
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=ListAssetLibrariesResponse)
|
||||
def list_asset_libraries(
|
||||
project_id: str,
|
||||
kind: str | None = None,
|
||||
asset_library_repository: InMemoryAssetLibraryRepository = Depends(get_asset_library_repository),
|
||||
) -> ListAssetLibrariesResponse:
|
||||
use_case = ListAssetLibrariesUseCase(asset_library_repository)
|
||||
parsed_kind = AssetLibraryKind(kind) if kind else None
|
||||
items = use_case.execute(project_id, kind=parsed_kind)
|
||||
return ListAssetLibrariesResponse(
|
||||
items=[
|
||||
AssetLibraryResponse(
|
||||
id=item.id,
|
||||
workspace_id=item.workspace_id,
|
||||
project_id=item.project_id,
|
||||
name=item.name,
|
||||
kind=item.kind.value,
|
||||
)
|
||||
for item in items
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=AssetLibraryResponse)
|
||||
def create_asset_library(
|
||||
request: CreateAssetLibraryRequest,
|
||||
asset_library_repository: InMemoryAssetLibraryRepository = Depends(get_asset_library_repository),
|
||||
) -> AssetLibraryResponse:
|
||||
use_case = CreateAssetLibraryUseCase(asset_library_repository)
|
||||
item = use_case.execute(
|
||||
CreateAssetLibraryCommand(
|
||||
workspace_id=request.workspace_id,
|
||||
project_id=request.project_id,
|
||||
name=request.name,
|
||||
kind=AssetLibraryKind(request.kind),
|
||||
)
|
||||
)
|
||||
return AssetLibraryResponse(
|
||||
id=item.id,
|
||||
workspace_id=item.workspace_id,
|
||||
project_id=item.project_id,
|
||||
name=item.name,
|
||||
kind=item.kind.value,
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.dependencies import get_asset_repository
|
||||
from app.schemas.asset import AssetResponse, CreateAssetRequest, ListAssetsResponse
|
||||
from packages.adapters.in_memory import InMemoryAssetRepository
|
||||
from packages.application import CreateAssetCommand, CreateAssetUseCase, ListAssetsUseCase
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=ListAssetsResponse)
|
||||
def list_assets(
|
||||
library_id: str,
|
||||
asset_repository: InMemoryAssetRepository = Depends(get_asset_repository),
|
||||
) -> ListAssetsResponse:
|
||||
use_case = ListAssetsUseCase(asset_repository)
|
||||
items = use_case.execute(library_id)
|
||||
return ListAssetsResponse(
|
||||
items=[
|
||||
AssetResponse(
|
||||
id=item.id,
|
||||
workspace_id=item.workspace_id,
|
||||
project_id=item.project_id,
|
||||
library_id=item.library_id,
|
||||
name=item.name,
|
||||
storage_key=item.storage_key,
|
||||
mime_type=item.mime_type,
|
||||
metadata=item.metadata,
|
||||
)
|
||||
for item in items
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=AssetResponse)
|
||||
def create_asset(
|
||||
request: CreateAssetRequest,
|
||||
asset_repository: InMemoryAssetRepository = Depends(get_asset_repository),
|
||||
) -> AssetResponse:
|
||||
use_case = CreateAssetUseCase(asset_repository)
|
||||
item = use_case.execute(
|
||||
CreateAssetCommand(
|
||||
workspace_id=request.workspace_id,
|
||||
project_id=request.project_id,
|
||||
library_id=request.library_id,
|
||||
name=request.name,
|
||||
storage_key=request.storage_key,
|
||||
mime_type=request.mime_type,
|
||||
metadata=request.metadata,
|
||||
)
|
||||
)
|
||||
return AssetResponse(
|
||||
id=item.id,
|
||||
workspace_id=item.workspace_id,
|
||||
project_id=item.project_id,
|
||||
library_id=item.library_id,
|
||||
name=item.name,
|
||||
storage_key=item.storage_key,
|
||||
mime_type=item.mime_type,
|
||||
metadata=item.metadata,
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.schemas.health import HealthResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=HealthResponse)
|
||||
def get_health() -> HealthResponse:
|
||||
return HealthResponse(ok=True, service="api")
|
||||
@@ -0,0 +1,35 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.dependencies import get_ingest_job_repository
|
||||
from app.schemas.ingest_job import IngestJobResponse, SubmitIngestJobRequest
|
||||
from packages.adapters.in_memory import InMemoryIngestJobRepository
|
||||
from packages.application import SubmitIngestJobCommand, SubmitIngestJobUseCase
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("", response_model=IngestJobResponse)
|
||||
def submit_ingest_job(
|
||||
request: SubmitIngestJobRequest,
|
||||
ingest_job_repository: InMemoryIngestJobRepository = Depends(get_ingest_job_repository),
|
||||
) -> IngestJobResponse:
|
||||
use_case = SubmitIngestJobUseCase(ingest_job_repository)
|
||||
job = use_case.execute(
|
||||
SubmitIngestJobCommand(
|
||||
workspace_id=request.workspace_id,
|
||||
project_id=request.project_id,
|
||||
library_id=request.library_id,
|
||||
storage_key=request.storage_key,
|
||||
)
|
||||
)
|
||||
# TODO: enqueue async worker task here
|
||||
return IngestJobResponse(
|
||||
id=job.id,
|
||||
workspace_id=job.workspace_id,
|
||||
project_id=job.project_id,
|
||||
library_id=job.library_id,
|
||||
storage_key=job.storage_key,
|
||||
status=job.status.value,
|
||||
error_message=job.error_message,
|
||||
result_asset_id=job.result_asset_id,
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from app.dependencies import get_project_repository
|
||||
from app.schemas.project import CreateProjectRequest, ListProjectsResponse, ProjectResponse
|
||||
from packages.application import CreateProjectCommand, CreateProjectUseCase, ListProjectsUseCase
|
||||
from packages.adapters.in_memory import InMemoryProjectRepository
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("", response_model=ListProjectsResponse)
|
||||
def list_projects(
|
||||
workspace_id: str,
|
||||
project_repository: InMemoryProjectRepository = Depends(get_project_repository),
|
||||
) -> ListProjectsResponse:
|
||||
use_case = ListProjectsUseCase(project_repository)
|
||||
projects = use_case.execute(workspace_id)
|
||||
return ListProjectsResponse(
|
||||
items=[
|
||||
ProjectResponse(
|
||||
id=item.id,
|
||||
workspace_id=item.workspace_id,
|
||||
name=item.name,
|
||||
description=item.description,
|
||||
)
|
||||
for item in projects
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=ProjectResponse)
|
||||
def create_project(
|
||||
request: CreateProjectRequest,
|
||||
project_repository: InMemoryProjectRepository = Depends(get_project_repository),
|
||||
) -> ProjectResponse:
|
||||
use_case = CreateProjectUseCase(project_repository)
|
||||
project = use_case.execute(
|
||||
CreateProjectCommand(
|
||||
workspace_id=request.workspace_id,
|
||||
name=request.name,
|
||||
description=request.description,
|
||||
)
|
||||
)
|
||||
return ProjectResponse(
|
||||
id=project.id,
|
||||
workspace_id=project.workspace_id,
|
||||
name=project.name,
|
||||
description=project.description,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Core configuration package."""
|
||||
@@ -0,0 +1,11 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class AppSettings(BaseModel):
|
||||
app_name: str = "xiaoxia-saas-api"
|
||||
app_env: str = "development"
|
||||
api_prefix: str = "/api"
|
||||
|
||||
|
||||
def get_settings() -> AppSettings:
|
||||
return AppSettings()
|
||||
@@ -0,0 +1,28 @@
|
||||
from functools import lru_cache
|
||||
|
||||
from packages.adapters.in_memory import (
|
||||
InMemoryAssetLibraryRepository,
|
||||
InMemoryAssetRepository,
|
||||
InMemoryIngestJobRepository,
|
||||
InMemoryProjectRepository,
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_project_repository() -> InMemoryProjectRepository:
|
||||
return InMemoryProjectRepository()
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_asset_library_repository() -> InMemoryAssetLibraryRepository:
|
||||
return InMemoryAssetLibraryRepository()
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_asset_repository() -> InMemoryAssetRepository:
|
||||
return InMemoryAssetRepository()
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def get_ingest_job_repository() -> InMemoryIngestJobRepository:
|
||||
return InMemoryIngestJobRepository()
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Schema package."""
|
||||
|
||||
from .asset import AssetResponse, CreateAssetRequest, ListAssetsResponse
|
||||
from .asset_library import AssetLibraryResponse, CreateAssetLibraryRequest, ListAssetLibrariesResponse
|
||||
from .health import HealthResponse
|
||||
from .ingest_job import IngestJobResponse, SubmitIngestJobRequest
|
||||
from .project import CreateProjectRequest, ListProjectsResponse, ProjectResponse
|
||||
|
||||
__all__ = [
|
||||
"AssetResponse",
|
||||
"AssetLibraryResponse",
|
||||
"CreateAssetLibraryRequest",
|
||||
"CreateAssetRequest",
|
||||
"CreateProjectRequest",
|
||||
"HealthResponse",
|
||||
"IngestJobResponse",
|
||||
"ListAssetLibrariesResponse",
|
||||
"ListAssetsResponse",
|
||||
"ListProjectsResponse",
|
||||
"ProjectResponse",
|
||||
"SubmitIngestJobRequest",
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CreateAssetRequest(BaseModel):
|
||||
workspace_id: str = Field(..., min_length=1)
|
||||
project_id: str = Field(..., min_length=1)
|
||||
library_id: str = Field(..., min_length=1)
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
storage_key: str = Field(..., min_length=1, max_length=255)
|
||||
mime_type: str = Field(..., min_length=1, max_length=100)
|
||||
metadata: dict[str, object] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class AssetResponse(BaseModel):
|
||||
id: str
|
||||
workspace_id: str
|
||||
project_id: str
|
||||
library_id: str
|
||||
name: str
|
||||
storage_key: str
|
||||
mime_type: str
|
||||
metadata: dict[str, object]
|
||||
|
||||
|
||||
class ListAssetsResponse(BaseModel):
|
||||
items: list[AssetResponse]
|
||||
@@ -0,0 +1,20 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CreateAssetLibraryRequest(BaseModel):
|
||||
workspace_id: str = Field(..., min_length=1)
|
||||
project_id: str = Field(..., min_length=1)
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
kind: str = Field(..., pattern="^(video|voice)$")
|
||||
|
||||
|
||||
class AssetLibraryResponse(BaseModel):
|
||||
id: str
|
||||
workspace_id: str
|
||||
project_id: str
|
||||
name: str
|
||||
kind: str
|
||||
|
||||
|
||||
class ListAssetLibrariesResponse(BaseModel):
|
||||
items: list[AssetLibraryResponse]
|
||||
@@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class HealthResponse(BaseModel):
|
||||
ok: bool
|
||||
service: str
|
||||
@@ -0,0 +1,19 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class SubmitIngestJobRequest(BaseModel):
|
||||
workspace_id: str = Field(..., min_length=1)
|
||||
project_id: str = Field(..., min_length=1)
|
||||
library_id: str = Field(..., min_length=1)
|
||||
storage_key: str = Field(..., min_length=1, max_length=255)
|
||||
|
||||
|
||||
class IngestJobResponse(BaseModel):
|
||||
id: str
|
||||
workspace_id: str
|
||||
project_id: str
|
||||
library_id: str
|
||||
storage_key: str
|
||||
status: str
|
||||
error_message: str
|
||||
result_asset_id: str
|
||||
@@ -0,0 +1,18 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class CreateProjectRequest(BaseModel):
|
||||
workspace_id: str = Field(..., min_length=1)
|
||||
name: str = Field(..., min_length=1, max_length=100)
|
||||
description: str = Field(default="", max_length=500)
|
||||
|
||||
|
||||
class ProjectResponse(BaseModel):
|
||||
id: str
|
||||
workspace_id: str
|
||||
name: str
|
||||
description: str
|
||||
|
||||
|
||||
class ListProjectsResponse(BaseModel):
|
||||
items: list[ProjectResponse]
|
||||
@@ -0,0 +1,14 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.api.router import api_router
|
||||
from app.core.config import get_settings
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
settings = get_settings()
|
||||
app = FastAPI(title=settings.app_name)
|
||||
app.include_router(api_router, prefix=settings.api_prefix)
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
def health():
|
||||
return {"ok": True, "service": "api"}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Workspace settings
|
||||
|
||||
This file keeps editor/workspace-specific settings if needed later.
|
||||
@@ -0,0 +1,14 @@
|
||||
# xiaoxia-saas Web
|
||||
|
||||
Next.js frontend app placeholder.
|
||||
|
||||
## Planned stack
|
||||
|
||||
- Next.js
|
||||
- TypeScript
|
||||
- Tailwind CSS
|
||||
- shadcn/ui
|
||||
|
||||
## Current status
|
||||
|
||||
Scaffold phase only. Runtime initialization will be added after final stack bootstrap.
|
||||
@@ -0,0 +1,12 @@
|
||||
export const metadata = {
|
||||
title: 'xiaoxia-saas',
|
||||
description: 'Xiaoxia SaaS scaffold',
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<main style={{ padding: 32, fontFamily: 'sans-serif' }}>
|
||||
<h1>xiaoxia-saas</h1>
|
||||
<p>SaaS 新主线前端骨架已创建。</p>
|
||||
<ul>
|
||||
<li>项目管理</li>
|
||||
<li>素材管理</li>
|
||||
<li>配音管理</li>
|
||||
<li>任务与成片</li>
|
||||
</ul>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// This file is auto-used by Next.js TypeScript projects.
|
||||
@@ -0,0 +1,6 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@xiaoxia/web",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000",
|
||||
"build": "next build",
|
||||
"start": "next start -p 3000",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "14.2.5",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "5.5.4",
|
||||
"@types/node": "22.7.4",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-next": "14.2.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["dom", "dom.iterable", "es2020"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [{ "name": "next" }]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# xiaoxia-saas Worker
|
||||
|
||||
## 结构
|
||||
|
||||
- `worker_app/core/`:worker 配置
|
||||
- `worker_app/celery_app.py`:Celery 应用入口
|
||||
- `worker_app/tasks/`:任务模块
|
||||
- `main.py`:本地调试入口
|
||||
|
||||
## 当前任务
|
||||
|
||||
- `worker.healthcheck`
|
||||
@@ -0,0 +1,11 @@
|
||||
from celery import Celery
|
||||
|
||||
|
||||
def create_celery_app() -> Celery:
|
||||
app = Celery("xiaoxia_saas_worker")
|
||||
app.conf.broker_url = "redis://redis:6379/0"
|
||||
app.conf.result_backend = "redis://redis:6379/1"
|
||||
return app
|
||||
|
||||
|
||||
celery_app = create_celery_app()
|
||||
@@ -0,0 +1,10 @@
|
||||
from worker_app.tasks.health import healthcheck
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("xiaoxia-saas worker scaffold")
|
||||
print(healthcheck.name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,6 @@
|
||||
from .celery_app import celery_app
|
||||
|
||||
|
||||
@celery_app.task(name="worker.healthcheck")
|
||||
def healthcheck() -> dict:
|
||||
return {"ok": True, "service": "worker"}
|
||||
@@ -0,0 +1 @@
|
||||
"""Worker application package."""
|
||||
@@ -0,0 +1,9 @@
|
||||
from celery import Celery
|
||||
|
||||
from worker_app.core.config import get_settings
|
||||
|
||||
|
||||
settings = get_settings()
|
||||
celery_app = Celery(settings.worker_name)
|
||||
celery_app.conf.broker_url = settings.broker_url
|
||||
celery_app.conf.result_backend = settings.result_backend
|
||||
@@ -0,0 +1 @@
|
||||
"""Worker core package."""
|
||||
@@ -0,0 +1,11 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class WorkerSettings(BaseModel):
|
||||
worker_name: str = "xiaoxia-saas-worker"
|
||||
broker_url: str = "redis://redis:6379/0"
|
||||
result_backend: str = "redis://redis:6379/1"
|
||||
|
||||
|
||||
def get_settings() -> WorkerSettings:
|
||||
return WorkerSettings()
|
||||
@@ -0,0 +1 @@
|
||||
"""Task modules."""
|
||||
@@ -0,0 +1,23 @@
|
||||
from worker_app.celery_app import celery_app
|
||||
|
||||
|
||||
@celery_app.task(name="worker.healthcheck")
|
||||
def healthcheck() -> dict:
|
||||
return {"ok": True, "service": "worker"}
|
||||
|
||||
|
||||
@celery_app.task(name="worker.ingest_asset")
|
||||
def ingest_asset(job_id: str, workspace_id: str, project_id: str, library_id: str, storage_key: str) -> dict:
|
||||
"""
|
||||
Ingest asset task placeholder.
|
||||
Real implementation would:
|
||||
1. Fetch file from storage_key
|
||||
2. Extract metadata (duration, resolution, etc.)
|
||||
3. Create Asset entity
|
||||
4. Update IngestJob status
|
||||
"""
|
||||
return {
|
||||
"job_id": job_id,
|
||||
"status": "completed",
|
||||
"asset_id": "placeholder-asset-id",
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# 新仓库启动说明
|
||||
|
||||
本仓库是 `xiaoxia-saas` 的新主线。
|
||||
|
||||
## 启动约束
|
||||
|
||||
- 新仓库与旧桌面版仓库物理隔离
|
||||
- 旧仓库只作为业务参考和历史样本
|
||||
- 新仓库所有开发都遵循 SaaS 启动文档
|
||||
- 先文档,后架构,后骨架,最后开发
|
||||
|
||||
## 入口文档
|
||||
|
||||
- `docs/saas-index.md`
|
||||
- `docs/saas-rebuild-kickoff-plan.md`
|
||||
- `docs/saas-rebuild-inventory.md`
|
||||
- `docs/saas-project-bootstrap-location.md`
|
||||
- `docs/saas-project-structure-spec.md`
|
||||
- `docs/saas-development-workflow.md`
|
||||
|
||||
## 当前下一步
|
||||
|
||||
1. 选择是否立即初始化前后端基础工程
|
||||
2. 确认数据库/队列/存储的本地开发方式
|
||||
3. 建立第一版 CI/CD
|
||||
@@ -0,0 +1,25 @@
|
||||
# SaaS 目录索引
|
||||
|
||||
```text
|
||||
F:\openclaw-saas
|
||||
├─ docs
|
||||
├─ apps
|
||||
│ ├─ web
|
||||
│ ├─ api
|
||||
│ └─ worker
|
||||
├─ packages
|
||||
│ ├─ domain
|
||||
│ ├─ application
|
||||
│ ├─ ports
|
||||
│ ├─ adapters
|
||||
│ └─ shared
|
||||
├─ infra
|
||||
│ ├─ docker
|
||||
│ ├─ nginx
|
||||
│ └─ scripts
|
||||
├─ tests
|
||||
│ ├─ integration
|
||||
│ └─ e2e
|
||||
├─ README.md
|
||||
└─ .gitignore
|
||||
```
|
||||
@@ -0,0 +1,281 @@
|
||||
# AI 团队工具清单(第一版)
|
||||
|
||||
本文档定义新 SaaS 项目启动阶段需要的 AI 工具、用途、角色归属、优先级、获取方式和安全要求。
|
||||
|
||||
---
|
||||
|
||||
## 1. 总原则
|
||||
|
||||
我们不自研 AI 平台,前期采用:
|
||||
|
||||
- 现成模型服务
|
||||
- 现成 AI IDE / AI 设计工具
|
||||
- 明确角色分工
|
||||
- 统一输入输出规范
|
||||
|
||||
重点不是“工具越多越好”,而是:
|
||||
|
||||
- 谁负责什么
|
||||
- 谁来拍板
|
||||
- 谁来 review
|
||||
- 谁来回归
|
||||
- 所有 AI 是否围绕统一文档工作
|
||||
|
||||
---
|
||||
|
||||
## 2. 工具分类
|
||||
|
||||
AI 工具分为 5 类:
|
||||
|
||||
1. 通用大模型
|
||||
2. 编码工具
|
||||
3. UI / 原型工具
|
||||
4. Review / QA 工具
|
||||
5. 协作与知识工具
|
||||
|
||||
---
|
||||
|
||||
## 3. 工具清单
|
||||
|
||||
| 类别 | 工具 | 主要用途 | 主要使用者 | 优先级 | 获取方式 |
|
||||
|------|------|----------|------------|--------|----------|
|
||||
| 通用大模型 | ChatGPT | 需求整理、方案讨论、文档草稿、总结 | 老大 / 小虾 | P0 | 直接订阅 |
|
||||
| 通用大模型 | Claude | 长文分析、架构讨论、文档、审查建议 | 老大 / 小虾 | P0 | 直接订阅 |
|
||||
| 编码工具 | Codex / Codex CLI | 仓库修改、重构、补测试、代码执行 | 小虾 / 编码 AI | P0 | 已可用 / 订阅 |
|
||||
| 编码工具 | Cursor | 前后端模块实现、快速编码 | 编码 AI | P0 | 直接订阅 |
|
||||
| 编码工具 | Claude Code / 同类 | 代码实现、重构、排错 | 编码 AI | P1 | 按需开通 |
|
||||
| UI / 原型 | Figma | 页面结构、流程图、组件设计 | 老大 / UI AI | P0 | 直接订阅 |
|
||||
| UI / 原型 | Figma AI | 辅助出界面和文案 | UI AI | P1 | Figma 内能力 |
|
||||
| UI / 原型 | v0 | Web 页面草图、组件原型 | UI AI / 前端 AI | P1 | 直接开通 |
|
||||
| UI / 原型 | Lovable | 页面快速原型 | UI AI | P2 | 按需开通 |
|
||||
| Review / QA | 独立大模型会话 | 代码审查、架构偏离检查 | Review AI | P0 | 基于已有模型 |
|
||||
| Review / QA | 独立大模型会话 | 测试设计、回归建议、bug 复盘 | QA AI | P0 | 基于已有模型 |
|
||||
| 协作 / 知识 | Gitea | 代码真相源、Issue、PR/提交记录 | 全员 | P0 | 已有 |
|
||||
| 协作 / 知识 | CI/CD | 自动检查、构建、测试、部署 | 全员 | P0 | 已有基础,需重建 |
|
||||
| 协作 / 知识 | 巡检机器人 | 线上状态、异常巡检、告警 | 运维 / 小虾 | P1 | 已有基础 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 按角色分配
|
||||
|
||||
### 4.1 老大
|
||||
|
||||
负责:
|
||||
|
||||
- 目标
|
||||
- 产品边界
|
||||
- 风格偏好
|
||||
- 最终拍板
|
||||
|
||||
建议直接使用:
|
||||
|
||||
- ChatGPT
|
||||
- Claude
|
||||
- Figma
|
||||
|
||||
### 4.2 小虾(技术负责人 / 总协调)
|
||||
|
||||
负责:
|
||||
|
||||
- 架构方案
|
||||
- 拆解任务
|
||||
- 规范约束
|
||||
- 合并口径
|
||||
- 收口
|
||||
|
||||
建议直接使用:
|
||||
|
||||
- ChatGPT / Claude
|
||||
- Codex / Codex CLI
|
||||
- Gitea
|
||||
- CI/CD
|
||||
|
||||
### 4.3 编码 AI
|
||||
|
||||
负责:
|
||||
|
||||
- 具体模块实现
|
||||
- 前端编码
|
||||
- 后端编码
|
||||
- 补测试
|
||||
|
||||
建议直接使用:
|
||||
|
||||
- Codex / Codex CLI
|
||||
- Cursor
|
||||
- Claude Code / 同类
|
||||
|
||||
### 4.4 UI AI
|
||||
|
||||
负责:
|
||||
|
||||
- 页面结构
|
||||
- 交互流
|
||||
- 原型草图
|
||||
- 视觉辅助
|
||||
|
||||
建议直接使用:
|
||||
|
||||
- Figma
|
||||
- Figma AI
|
||||
- v0
|
||||
- Lovable
|
||||
|
||||
### 4.5 Review AI
|
||||
|
||||
负责:
|
||||
|
||||
- 代码审查
|
||||
- 架构一致性检查
|
||||
- 风险提示
|
||||
- 边界偏移检查
|
||||
|
||||
建议直接使用:
|
||||
|
||||
- 独立 ChatGPT / Claude 会话
|
||||
- 仓库 diff / PR 内容
|
||||
|
||||
### 4.6 QA / Bug AI
|
||||
|
||||
负责:
|
||||
|
||||
- 复现问题
|
||||
- 补回归场景
|
||||
- 测试建议
|
||||
- bug 分类
|
||||
|
||||
建议直接使用:
|
||||
|
||||
- 独立 ChatGPT / Claude 会话
|
||||
- CI 报告
|
||||
- 日志与监控结果
|
||||
|
||||
---
|
||||
|
||||
## 5. 获取顺序
|
||||
|
||||
### 第一批:马上需要
|
||||
|
||||
必须先具备:
|
||||
|
||||
1. ChatGPT
|
||||
2. Claude
|
||||
3. Cursor 或同类编码 AI IDE
|
||||
4. Figma
|
||||
5. Gitea
|
||||
6. CI/CD 基础
|
||||
|
||||
### 第二批:开始进入前端原型和多人协作时补
|
||||
|
||||
1. v0
|
||||
2. Figma AI
|
||||
3. 独立 Review / QA 会话模板
|
||||
4. 巡检机器人 SaaS 化巡检能力
|
||||
|
||||
### 第三批:规模扩大后再补
|
||||
|
||||
1. 更专业的设计协同工具
|
||||
2. 更完整的测试平台
|
||||
3. 更高级的知识库 / 检索增强能力
|
||||
|
||||
---
|
||||
|
||||
## 6. 采购 / 开通制度
|
||||
|
||||
### 6.1 原则
|
||||
|
||||
- 优先采购真正高频使用的工具
|
||||
- 一个角色至少有一套稳定主工具
|
||||
- 前期不堆太多重复工具
|
||||
- 所有付费工具要登记账号归属和用途
|
||||
|
||||
### 6.2 必须登记的信息
|
||||
|
||||
每个工具都要记录:
|
||||
|
||||
- 工具名称
|
||||
- 购买渠道
|
||||
- 账号归属
|
||||
- 谁可以使用
|
||||
- 费用周期
|
||||
- 主要用途
|
||||
- 替代方案
|
||||
|
||||
---
|
||||
|
||||
## 7. 安全制度
|
||||
|
||||
### 7.1 严禁随意喂给 AI 的信息
|
||||
|
||||
- 服务器 root 密码
|
||||
- 数据库密码
|
||||
- 云存储密钥
|
||||
- 支付相关密钥
|
||||
- 生产环境敏感配置
|
||||
- 用户隐私数据
|
||||
|
||||
### 7.2 可以给 AI 的信息
|
||||
|
||||
- 脱敏代码
|
||||
- 架构方案
|
||||
- 伪造样本数据
|
||||
- 通用错误日志(脱敏后)
|
||||
- 接口设计草稿
|
||||
|
||||
### 7.3 最佳实践
|
||||
|
||||
- 敏感配置统一放环境变量和密钥管理中
|
||||
- 给外部 AI 前先脱敏
|
||||
- 不把完整生产数据库导出直接发给任何模型
|
||||
|
||||
---
|
||||
|
||||
## 8. 使用制度
|
||||
|
||||
### 制度 1:一个问题一个 owner
|
||||
|
||||
- 架构问题只由技术 owner 收口
|
||||
- UI 问题由 UI owner 收口
|
||||
- Review 不能和编码混同
|
||||
|
||||
### 制度 2:AI 输出不能直接上线
|
||||
|
||||
必须经过:
|
||||
|
||||
- 人工确认
|
||||
- 代码审查
|
||||
- 自动化测试
|
||||
- CI 通过
|
||||
|
||||
### 制度 3:统一文档源
|
||||
|
||||
所有 AI 都必须围绕同一套文档:
|
||||
|
||||
- PRD
|
||||
- 架构文档
|
||||
- 数据模型
|
||||
- API 规范
|
||||
- 任务拆解清单
|
||||
|
||||
### 制度 4:避免多 AI 同时乱改同一问题
|
||||
|
||||
- 一个任务一个主执行者
|
||||
- 其他 AI 提供辅助,不直接平行乱改
|
||||
|
||||
---
|
||||
|
||||
## 9. 当前结论
|
||||
|
||||
当前最值得立即准备的 AI 工具是:
|
||||
|
||||
- ChatGPT
|
||||
- Claude
|
||||
- Codex / Codex CLI
|
||||
- Cursor
|
||||
- Figma
|
||||
- Gitea
|
||||
- CI/CD
|
||||
|
||||
这些足够支撑新 SaaS 项目从 0 到可开发启动。
|
||||
|
||||
后续再按阶段补更细的 UI / Review / QA 工具。
|
||||
@@ -0,0 +1,263 @@
|
||||
# 基础设施保留 / 废弃 / 重建清单(第一版)
|
||||
|
||||
本文档定义新 SaaS 项目启动时,哪些基础设施继续保留,哪些只保留经验不保留实现,哪些必须全新重建。
|
||||
|
||||
---
|
||||
|
||||
## 1. 分类原则
|
||||
|
||||
基础设施分为三类:
|
||||
|
||||
1. **保留**:直接作为新项目资产继续使用
|
||||
2. **废弃实现,仅保留经验**:旧实现不进入新主线,但经验可复用
|
||||
3. **必须重建**:新 SaaS 需要重新搭建
|
||||
|
||||
---
|
||||
|
||||
## 2. 保留清单
|
||||
|
||||
### 2.1 代码仓库
|
||||
|
||||
- **保留项**:Gitea
|
||||
- **原因**:已经是稳定的代码真相源,具备提交、历史、协作、推送能力
|
||||
- **新项目做法**:新 SaaS 使用新的独立仓库或独立主目录,不与旧桌面版混线
|
||||
|
||||
### 2.2 CI/CD 思路和经验
|
||||
|
||||
- **保留项**:自动检查、测试、构建、发布的工程理念
|
||||
- **原因**:这是正规软件必须保留的工程资产
|
||||
- **新项目做法**:重建适合 Web/SaaS 的流水线
|
||||
|
||||
### 2.3 巡检机器人思路
|
||||
|
||||
- **保留项**:巡检、告警、状态监控思路
|
||||
- **原因**:线上系统比桌面工具更需要健康巡检
|
||||
- **新项目做法**:转为 SaaS 服务、任务、队列、磁盘、worker 的巡检
|
||||
|
||||
### 2.4 服务器与开发环境
|
||||
|
||||
- **保留项**:Windows 开发机、Ubuntu 服务器
|
||||
- **原因**:开发和部署基础已有
|
||||
- **新项目做法**:重新按 SaaS 需要规划开发/测试/生产用途
|
||||
|
||||
---
|
||||
|
||||
## 3. 旧实现废弃,但经验保留
|
||||
|
||||
### 3.1 Tkinter 桌面 UI 工程结构
|
||||
|
||||
- **旧实现**:Tkinter 主窗口 + 单体 GUI 文件
|
||||
- **处理方式**:废弃为新主线实现
|
||||
- **保留经验**:哪些业务交互复杂、哪些模块最容易耦合
|
||||
|
||||
### 3.2 安装版同步 / 发布脚本
|
||||
|
||||
- **旧实现**:`deploy_to_installed.py`、安装版同步链
|
||||
- **处理方式**:不带入新 SaaS 主线
|
||||
- **保留经验**:发布必须有清单、运行时文件必须明确登记
|
||||
|
||||
### 3.3 桌面版更新体系
|
||||
|
||||
- **旧实现**:本地补丁、manifest、rollback
|
||||
- **处理方式**:不直接复用
|
||||
- **保留经验**:版本状态、回滚、失败恢复、补丁安全校验思路
|
||||
|
||||
### 3.4 临时修复脚本文化
|
||||
|
||||
- **旧实现**:一批为临时问题而生的脚本
|
||||
- **处理方式**:明确不继承到新主线
|
||||
- **保留经验**:哪些问题容易高频复发,需要产品级能力解决
|
||||
|
||||
---
|
||||
|
||||
## 4. 必须重建的基础设施
|
||||
|
||||
### 4.1 新项目仓库结构
|
||||
|
||||
必须重建:
|
||||
|
||||
- 新仓库目录结构
|
||||
- 前后端分层结构
|
||||
- 文档结构
|
||||
- 基础模块布局
|
||||
|
||||
### 4.2 Web/SaaS CI/CD 流水线
|
||||
|
||||
必须重建:
|
||||
|
||||
- 前端构建
|
||||
- 后端测试
|
||||
- 镜像/部署包构建
|
||||
- 测试环境部署
|
||||
- 正式环境部署
|
||||
- 回滚流程
|
||||
|
||||
### 4.3 鉴权体系
|
||||
|
||||
必须重建:
|
||||
|
||||
- 用户认证
|
||||
- 会话管理
|
||||
- 权限控制
|
||||
- 团队/工作区边界(如果需要)
|
||||
|
||||
### 4.4 存储体系
|
||||
|
||||
必须重建:
|
||||
|
||||
- 对象存储 / 素材存储方案
|
||||
- 数据库存储方案
|
||||
- 结果文件管理方案
|
||||
- 清理和归档策略
|
||||
|
||||
### 4.5 任务体系
|
||||
|
||||
必须重建:
|
||||
|
||||
- 任务队列
|
||||
- 异步生成任务
|
||||
- 重试机制
|
||||
- 状态追踪
|
||||
- worker 管理
|
||||
|
||||
### 4.6 观测性体系
|
||||
|
||||
必须重建:
|
||||
|
||||
- 日志
|
||||
- 指标
|
||||
- 告警
|
||||
- 错误追踪
|
||||
- 健康检查
|
||||
|
||||
### 4.7 配置与密钥管理
|
||||
|
||||
必须重建:
|
||||
|
||||
- 环境变量规范
|
||||
- 密钥注入方式
|
||||
- 不同环境配置管理
|
||||
- 敏感信息隔离
|
||||
|
||||
---
|
||||
|
||||
## 5. 项目推进器要不要
|
||||
|
||||
### 结论
|
||||
|
||||
要,但先轻量,不要一开始就搞太重。
|
||||
|
||||
### 轻量方案即可支撑初期
|
||||
|
||||
- 需求清单
|
||||
- 里程碑
|
||||
- issue / 任务列表
|
||||
- 发布记录
|
||||
- 架构文档
|
||||
|
||||
### 暂时不需要的重型能力
|
||||
|
||||
- 复杂流程引擎
|
||||
- 重度项目管理系统
|
||||
- 过度自动化的工单流转
|
||||
|
||||
---
|
||||
|
||||
## 6. 巡检机器人要不要
|
||||
|
||||
### 结论
|
||||
|
||||
要保留,而且未来价值更大。
|
||||
|
||||
### 在新 SaaS 中的职责
|
||||
|
||||
巡检机器人未来可以检查:
|
||||
|
||||
- API 服务是否在线
|
||||
- 队列是否堆积
|
||||
- worker 是否离线
|
||||
- 生成失败率是否异常
|
||||
- 存储空间是否异常
|
||||
- 数据库连接是否异常
|
||||
- 定时任务是否失效
|
||||
|
||||
### 定位
|
||||
|
||||
- 它是运维和健康检查系统的一部分
|
||||
- 不是业务开发主流程的一部分
|
||||
|
||||
---
|
||||
|
||||
## 7. CI/CD 要不要
|
||||
|
||||
### 结论
|
||||
|
||||
必须保留,而且必须升级。
|
||||
|
||||
### 新 SaaS 项目里必须具备的能力
|
||||
|
||||
- 代码检查
|
||||
- 自动测试
|
||||
- 构建
|
||||
- 测试环境部署
|
||||
- 正式环境部署
|
||||
- 回滚
|
||||
|
||||
### 原则
|
||||
|
||||
- 新项目不能没有 CI/CD
|
||||
- 但 CI/CD 流程必须服务于 SaaS,不是延用桌面版套路
|
||||
|
||||
---
|
||||
|
||||
## 8. 当前建议的基础设施策略
|
||||
|
||||
### 立即保留
|
||||
|
||||
- Gitea
|
||||
- 现有 CI/CD 思路
|
||||
- 巡检机器人思路
|
||||
- 服务器资源
|
||||
|
||||
### 立即停止依赖旧实现
|
||||
|
||||
- 桌面版发布链
|
||||
- 安装同步链
|
||||
- Tkinter 工程结构
|
||||
- 临时修复脚本文化
|
||||
|
||||
### 接下来优先重建
|
||||
|
||||
1. 新项目仓库结构
|
||||
2. SaaS CI/CD
|
||||
3. 鉴权体系
|
||||
4. 存储体系
|
||||
5. 任务体系
|
||||
6. 观测性体系
|
||||
|
||||
---
|
||||
|
||||
## 9. 当前阶段结论
|
||||
|
||||
新 SaaS 项目不是从零开始,因为我们有:
|
||||
|
||||
- 仓库
|
||||
- CI/CD 经验
|
||||
- 巡检经验
|
||||
- 服务器
|
||||
- 业务规则
|
||||
- 稳定性治理经验
|
||||
|
||||
但新 SaaS 也不能“沿用旧系统实现”,因为:
|
||||
|
||||
- 桌面架构不适合作为未来主线
|
||||
- 发布方式完全不同
|
||||
- 任务模型完全不同
|
||||
- 存储模型完全不同
|
||||
- 用户和权限模型未来也会不同
|
||||
|
||||
所以正确策略是:
|
||||
|
||||
- **保留资产**
|
||||
- **废弃旧实现**
|
||||
- **重建 SaaS 所需基础设施**
|
||||
@@ -0,0 +1,268 @@
|
||||
# SaaS 核心流程清单(第一版)
|
||||
|
||||
本文档定义新 SaaS 系统的一期核心业务流程。目的不是画所有细节,而是先明确系统必须跑通哪些主链路。
|
||||
|
||||
---
|
||||
|
||||
## 1. 流程设计原则
|
||||
|
||||
核心流程必须满足:
|
||||
|
||||
- 覆盖现有桌面版的主要功能价值
|
||||
- 适合 SaaS 架构而不是桌面软件思维
|
||||
- 支持异步任务、多人协作、后续扩展
|
||||
- 允许阶段性交付,不要求一开始全量上线
|
||||
|
||||
---
|
||||
|
||||
## 2. 一期核心主流程
|
||||
|
||||
建议一期至少跑通 6 条主流程:
|
||||
|
||||
1. 登录与进入工作空间
|
||||
2. 创建项目
|
||||
3. 上传并管理素材
|
||||
4. 素材分类与诊断
|
||||
5. 发起生成 / 批量生成
|
||||
6. 查看结果与下载
|
||||
|
||||
---
|
||||
|
||||
## 3. 流程一:登录与进入工作空间
|
||||
|
||||
### 目标
|
||||
|
||||
让用户能够进入自己的工作环境。
|
||||
|
||||
### 基本步骤
|
||||
|
||||
1. 用户登录
|
||||
2. 进入默认工作空间
|
||||
3. 查看项目列表
|
||||
4. 选择已有项目或创建新项目
|
||||
|
||||
### 一期建议
|
||||
|
||||
- 可以先简化为单用户单工作空间
|
||||
- 但模型上要保留工作空间概念
|
||||
|
||||
---
|
||||
|
||||
## 4. 流程二:创建项目
|
||||
|
||||
### 目标
|
||||
|
||||
为一轮视频生成工作建立上下文容器。
|
||||
|
||||
### 基本步骤
|
||||
|
||||
1. 用户点击创建项目
|
||||
2. 输入项目名称
|
||||
3. 系统创建项目
|
||||
4. 跳转到项目工作台
|
||||
|
||||
### 项目工作台未来应承载
|
||||
|
||||
- 素材库
|
||||
- 配音库
|
||||
- 策略
|
||||
- 任务
|
||||
- 结果
|
||||
- 诊断
|
||||
|
||||
---
|
||||
|
||||
## 5. 流程三:上传并管理素材
|
||||
|
||||
### 目标
|
||||
|
||||
把桌面版“本地素材库”的能力转化成 SaaS 中的项目级素材管理。
|
||||
|
||||
### 子流程
|
||||
|
||||
#### 5.1 上传视频/图片素材
|
||||
|
||||
1. 选择项目
|
||||
2. 上传文件
|
||||
3. 系统保存到素材库
|
||||
4. 返回素材列表
|
||||
|
||||
#### 5.2 上传配音素材
|
||||
|
||||
1. 选择项目或配音库
|
||||
2. 上传配音文件
|
||||
3. 系统去重/校验
|
||||
4. 返回配音列表
|
||||
|
||||
#### 5.3 管理素材库
|
||||
|
||||
包括:
|
||||
|
||||
- 新建素材库
|
||||
- 删除素材库
|
||||
- 查看素材详情
|
||||
- 过滤、搜索、统计
|
||||
|
||||
### 一期建议
|
||||
|
||||
- 先做项目级素材上传和列表
|
||||
- 库管理可以保留轻量版
|
||||
|
||||
---
|
||||
|
||||
## 6. 流程四:素材分类与诊断
|
||||
|
||||
### 目标
|
||||
|
||||
复用现有桌面版已经验证过的价值:
|
||||
|
||||
- 自动分类素材
|
||||
- 给出素材准备度
|
||||
- 找出缺口和风险
|
||||
|
||||
### 基本步骤
|
||||
|
||||
1. 用户触发分类/诊断
|
||||
2. 系统创建异步任务
|
||||
3. worker 执行分类与诊断
|
||||
4. 用户看到进度
|
||||
5. 分类结果回写素材
|
||||
6. 诊断结果展示到项目界面
|
||||
|
||||
### 关键要求
|
||||
|
||||
- 长任务异步执行
|
||||
- 坏文件不能拖垮整批
|
||||
- 分类和诊断结果必须可追踪
|
||||
|
||||
---
|
||||
|
||||
## 7. 流程五:发起生成 / 批量生成
|
||||
|
||||
### 目标
|
||||
|
||||
这是核心价值流程,必须作为系统最重要主链路设计。
|
||||
|
||||
### 5.1 单次生成流程
|
||||
|
||||
1. 用户选择项目
|
||||
2. 选择/配置生成策略
|
||||
3. 选择素材库与配音库
|
||||
4. 提交生成请求
|
||||
5. 系统创建生成任务
|
||||
6. worker 执行生成
|
||||
7. 返回进度和日志
|
||||
8. 产出成片结果
|
||||
|
||||
### 5.2 批量生成流程
|
||||
|
||||
1. 用户配置批量规则
|
||||
2. 提交批量任务
|
||||
3. 系统拆分候选
|
||||
4. 每个候选独立执行
|
||||
5. 单候选失败不拖垮全批次
|
||||
6. 汇总成功/失败/跳过结果
|
||||
|
||||
### 关键要求
|
||||
|
||||
- 任务必须异步
|
||||
- 状态必须可跟踪
|
||||
- 单项失败隔离
|
||||
- 支持取消 / 停止 / 重试(后续逐步补)
|
||||
|
||||
---
|
||||
|
||||
## 8. 流程六:查看结果与下载
|
||||
|
||||
### 目标
|
||||
|
||||
让用户消费系统产出的最终价值。
|
||||
|
||||
### 基本步骤
|
||||
|
||||
1. 用户打开项目结果页
|
||||
2. 查看成片列表
|
||||
3. 预览结果
|
||||
4. 下载单个成片或结果包
|
||||
|
||||
### 后续可扩展
|
||||
|
||||
- 一键发布
|
||||
- 结果同步
|
||||
- 团队复核
|
||||
- 版本对比
|
||||
|
||||
---
|
||||
|
||||
## 9. 配套支撑流程
|
||||
|
||||
除了主流程,还需要这些支撑流程:
|
||||
|
||||
### 9.1 任务状态查看
|
||||
|
||||
- 查看执行中任务
|
||||
- 查看失败原因
|
||||
- 查看进度
|
||||
|
||||
### 9.2 日志与诊断
|
||||
|
||||
- 查看任务日志
|
||||
- 查看错误摘要
|
||||
- 查看系统健康状态
|
||||
|
||||
### 9.3 Worker 管理
|
||||
|
||||
- 节点在线状态
|
||||
- 任务分配情况
|
||||
- 队列积压情况
|
||||
|
||||
### 9.4 审计与操作记录
|
||||
|
||||
- 谁上传了什么
|
||||
- 谁删除了什么
|
||||
- 谁发起了任务
|
||||
- 失败发生在什么时候
|
||||
|
||||
---
|
||||
|
||||
## 10. 一期暂缓流程
|
||||
|
||||
这些不是现在最先必须做的:
|
||||
|
||||
- 支付 / 套餐
|
||||
- 模板市场
|
||||
- 多层级复杂权限
|
||||
- 外部平台自动发布
|
||||
- 高级协同审批流
|
||||
|
||||
这些后面可以做,但不应该阻塞 SaaS 第一版启动。
|
||||
|
||||
---
|
||||
|
||||
## 11. 一期主线 MVP 建议
|
||||
|
||||
建议先确保以下闭环跑通:
|
||||
|
||||
1. 登录
|
||||
2. 创建项目
|
||||
3. 上传素材
|
||||
4. 素材分类
|
||||
5. 发起生成
|
||||
6. 查看进度
|
||||
7. 下载结果
|
||||
|
||||
如果这条链能稳定跑通,SaaS 第一版就已经有真实价值。
|
||||
|
||||
---
|
||||
|
||||
## 12. 当前阶段结论
|
||||
|
||||
当前最重要的是:
|
||||
|
||||
- 不要试图一口气把所有桌面能力都搬完
|
||||
- 先抓住真正的主链路
|
||||
- 先让 SaaS 的关键业务闭环成立
|
||||
|
||||
下一步应继续:
|
||||
|
||||
- 根据核心对象和核心流程,开始做技术栈选型
|
||||
@@ -0,0 +1,353 @@
|
||||
# SaaS 核心对象清单(第一版)
|
||||
|
||||
本文档定义新 SaaS 系统的核心业务对象。目标不是现在就把数据库字段写死,而是先把系统世界观定清楚,避免后续边开发边返工。
|
||||
|
||||
---
|
||||
|
||||
## 1. 设计原则
|
||||
|
||||
核心对象必须满足:
|
||||
|
||||
- 能覆盖当前桌面版的主要业务能力
|
||||
- 能支撑未来多人协作和迭代
|
||||
- 能清楚区分“谁拥有、谁使用、谁产生、谁消费”
|
||||
- 不因为桌面版历史实现而被绑死
|
||||
|
||||
---
|
||||
|
||||
## 2. 顶层对象分组
|
||||
|
||||
新 SaaS 的核心对象建议分为 6 组:
|
||||
|
||||
1. 用户与组织
|
||||
2. 工作空间与项目
|
||||
3. 素材与素材库
|
||||
4. 生成与任务
|
||||
5. 结果与发布
|
||||
6. 系统与运维
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户与组织对象
|
||||
|
||||
### 3.1 User(用户)
|
||||
|
||||
代表系统中的登录使用者。
|
||||
|
||||
职责:
|
||||
|
||||
- 登录系统
|
||||
- 创建项目
|
||||
- 上传素材
|
||||
- 发起生成
|
||||
- 查看结果
|
||||
- 管理自己的资源
|
||||
|
||||
后续待确认:
|
||||
|
||||
- 是否支持多个角色
|
||||
- 是否支持子账号
|
||||
- 是否支持团队成员
|
||||
|
||||
### 3.2 Team / Workspace(团队 / 工作空间)
|
||||
|
||||
如果未来不是纯个人 SaaS,就必须有这个对象。
|
||||
|
||||
职责:
|
||||
|
||||
- 隔离不同用户组的数据
|
||||
- 承载成员、项目、素材库、任务
|
||||
- 作为权限边界
|
||||
|
||||
当前建议:
|
||||
|
||||
- 先按“工作空间”概念设计
|
||||
- 一期即使只支持单用户,也保留扩展位
|
||||
|
||||
### 3.3 Membership(成员关系)
|
||||
|
||||
表示用户属于哪个工作空间、具备什么权限。
|
||||
|
||||
职责:
|
||||
|
||||
- 权限控制
|
||||
- 团队协作
|
||||
- 后续审计与操作归属
|
||||
|
||||
---
|
||||
|
||||
## 4. 工作空间与项目对象
|
||||
|
||||
### 4.1 Project(项目)
|
||||
|
||||
新 SaaS 里最核心的业务容器之一。
|
||||
|
||||
职责:
|
||||
|
||||
- 承载一组素材、配音、策略、任务、生成结果
|
||||
- 作为用户操作的主要上下文
|
||||
|
||||
为什么必须有:
|
||||
|
||||
桌面版很多逻辑都默认“当前工作台上下文”,SaaS 里必须把这个上下文对象化。
|
||||
|
||||
### 4.2 Folder / Collection(可选)
|
||||
|
||||
如果素材量很大,后续可能需要集合/文件夹逻辑。
|
||||
|
||||
一期建议:
|
||||
|
||||
- 不作为强制核心对象
|
||||
- 预留二级组织能力即可
|
||||
|
||||
---
|
||||
|
||||
## 5. 素材与素材库对象
|
||||
|
||||
### 5.1 AssetLibrary(素材库)
|
||||
|
||||
素材库的统一抽象。
|
||||
|
||||
建议未来不要把“视频库”和“配音库”写成完全不同的两套模型,而是:
|
||||
|
||||
- 统一为 `AssetLibrary`
|
||||
- 通过 `kind` 区分:
|
||||
- `video`
|
||||
- `voice`
|
||||
- 后续可扩展 `image` / `template` / `subtitle`
|
||||
|
||||
职责:
|
||||
|
||||
- 组织素材
|
||||
- 隔离不同项目/工作空间资源
|
||||
- 提供筛选、统计、权限、归属能力
|
||||
|
||||
### 5.2 Asset(素材)
|
||||
|
||||
素材的统一抽象。
|
||||
|
||||
可能包括:
|
||||
|
||||
- 视频素材
|
||||
- 图片素材
|
||||
- 配音素材
|
||||
- 背景音频
|
||||
- 模板文件(未来)
|
||||
|
||||
关键属性层面未来要支持:
|
||||
|
||||
- 所属库
|
||||
- 所属项目 / 工作空间
|
||||
- 文件地址
|
||||
- 文件元数据
|
||||
- 分类结果
|
||||
- 质量状态
|
||||
- 风险标记
|
||||
- 使用统计
|
||||
|
||||
### 5.3 AssetClassification(素材分类结果)
|
||||
|
||||
当前桌面版已经有成熟经验,这个对象未来应该独立表达。
|
||||
|
||||
职责:
|
||||
|
||||
- 记录素材分类结果
|
||||
- 记录口播/场景/待复核等业务标签
|
||||
- 作为生成策略选择依据
|
||||
|
||||
### 5.4 AssetDiagnosis(素材诊断结果)
|
||||
|
||||
用于表达素材准备度、缺口、建议。
|
||||
|
||||
职责:
|
||||
|
||||
- 给项目/素材库生成健康判断
|
||||
- 给用户输出“缺什么、风险在哪里、建议怎么补”
|
||||
|
||||
---
|
||||
|
||||
## 6. 配音与策略对象
|
||||
|
||||
### 6.1 VoiceAsset(配音素材)
|
||||
|
||||
虽然可以作为 Asset 的一种,但业务上很重要,需要单独强调。
|
||||
|
||||
职责:
|
||||
|
||||
- 作为生成输入
|
||||
- 支持试听、复核、重复检测、质量检查
|
||||
|
||||
### 6.2 GenerationStrategy(生成策略)
|
||||
|
||||
桌面版里已经存在生成策略概念,SaaS 里必须保留,而且要对象化。
|
||||
|
||||
职责:
|
||||
|
||||
- 定义生成模式
|
||||
- 定义视频库/配音库选择方式
|
||||
- 定义阈值、数量、规则
|
||||
- 支持后续模板化与复用
|
||||
|
||||
### 6.3 StrategyTemplate(策略模板,可选)
|
||||
|
||||
如果未来要做更强复用,这个对象会非常有用。
|
||||
|
||||
职责:
|
||||
|
||||
- 保存可复用的生成策略
|
||||
- 支持团队共享
|
||||
|
||||
---
|
||||
|
||||
## 7. 生成与任务对象
|
||||
|
||||
### 7.1 GenerationRequest(生成请求)
|
||||
|
||||
表示用户点击“开始生成”时提交的一次业务请求。
|
||||
|
||||
职责:
|
||||
|
||||
- 记录用户发起的意图
|
||||
- 绑定项目、策略、素材范围、配置
|
||||
|
||||
### 7.2 GenerationTask(生成任务)
|
||||
|
||||
表示后台真正执行的一次任务。
|
||||
|
||||
职责:
|
||||
|
||||
- 追踪状态
|
||||
- 分配 worker
|
||||
- 记录进度
|
||||
- 支持失败重试
|
||||
- 支持日志与诊断
|
||||
|
||||
### 7.3 BatchTask(批量任务)
|
||||
|
||||
桌面版已经证明单个任务和批量任务不是一个复杂度级别,所以建议单独对象化。
|
||||
|
||||
职责:
|
||||
|
||||
- 管理一批生成候选
|
||||
- 统计成功/失败/跳过
|
||||
- 支持单候选失败隔离
|
||||
|
||||
### 7.4 TaskAttempt / TaskRun(任务执行记录)
|
||||
|
||||
如果未来要支持重试、回放、错误审计,这个对象很重要。
|
||||
|
||||
职责:
|
||||
|
||||
- 记录某个任务的第几次执行
|
||||
- 记录失败原因、耗时、日志摘要
|
||||
|
||||
---
|
||||
|
||||
## 8. 结果与发布对象
|
||||
|
||||
### 8.1 GeneratedVideo(生成结果 / 成片)
|
||||
|
||||
表示最终产出的业务结果。
|
||||
|
||||
职责:
|
||||
|
||||
- 保存成片元信息
|
||||
- 绑定来源任务
|
||||
- 供下载、预览、发布、同步
|
||||
|
||||
### 8.2 ResultPackage(结果包,可选)
|
||||
|
||||
如果后续要把视频、封面、字幕、元信息打包下载,可以引入。
|
||||
|
||||
### 8.3 PublishRecord(发布记录,可选)
|
||||
|
||||
如果未来要做发布/分发到外部平台,这个对象需要提前预留。
|
||||
|
||||
---
|
||||
|
||||
## 9. 系统与运维对象
|
||||
|
||||
### 9.1 WorkerNode(执行节点)
|
||||
|
||||
SaaS 版里视频生成大概率不应该和 Web 请求进程混在一起,所以需要执行节点概念。
|
||||
|
||||
职责:
|
||||
|
||||
- 执行长任务
|
||||
- 上报健康状态
|
||||
- 接受任务分派
|
||||
|
||||
### 9.2 SyncJob(同步任务)
|
||||
|
||||
当前桌面版有云同步经验,SaaS 里如果有多存储、多节点、多结果归档,也会需要同步任务抽象。
|
||||
|
||||
### 9.3 AuditLog(审计日志)
|
||||
|
||||
职责:
|
||||
|
||||
- 记录谁在什么时候做了什么
|
||||
- 支持排错、责任追踪、安全审计
|
||||
|
||||
### 9.4 SystemEvent / HealthCheck(系统事件 / 健康检查)
|
||||
|
||||
职责:
|
||||
|
||||
- 观测性
|
||||
- 巡检
|
||||
- 告警
|
||||
|
||||
---
|
||||
|
||||
## 10. 一期必须优先确认的对象
|
||||
|
||||
最先必须定死的是:
|
||||
|
||||
1. User
|
||||
2. Workspace
|
||||
3. Project
|
||||
4. AssetLibrary
|
||||
5. Asset
|
||||
6. GenerationStrategy
|
||||
7. GenerationTask
|
||||
8. BatchTask
|
||||
9. GeneratedVideo
|
||||
10. WorkerNode
|
||||
|
||||
这 10 个对象决定了 SaaS 的主骨架。
|
||||
|
||||
---
|
||||
|
||||
## 11. 当前建议
|
||||
|
||||
### 一期推荐采用的对象骨架
|
||||
|
||||
- 用户
|
||||
- 工作空间
|
||||
- 项目
|
||||
- 统一素材库
|
||||
- 统一素材对象
|
||||
- 生成策略
|
||||
- 单任务 / 批量任务
|
||||
- 成片结果
|
||||
- Worker 节点
|
||||
- 审计日志
|
||||
|
||||
### 暂时可后补的对象
|
||||
|
||||
- 套餐 / 支付
|
||||
- 发布记录
|
||||
- 模板市场
|
||||
- 高级权限体系
|
||||
- 更复杂的目录树/集合体系
|
||||
|
||||
---
|
||||
|
||||
## 12. 当前阶段结论
|
||||
|
||||
新 SaaS 项目最重要的不是“先建多少表”,而是先把对象世界观定清楚。
|
||||
|
||||
当前建议已经足够支持下一步:
|
||||
|
||||
- 继续定义核心流程
|
||||
- 再根据对象和流程定技术栈
|
||||
- 之后再设计数据库和 API
|
||||
@@ -0,0 +1,228 @@
|
||||
# SaaS 开发协作流程与制度(第一版)
|
||||
|
||||
本文档定义新 SaaS 项目的开发流程、提交流程、验收流程和 AI 协作制度。
|
||||
|
||||
目标:
|
||||
|
||||
- 避免边想边改
|
||||
- 避免多人同时乱改
|
||||
- 避免 AI 输出直接进主线
|
||||
- 让每一步都可回溯、可检查、可收口
|
||||
|
||||
---
|
||||
|
||||
## 1. 总流程
|
||||
|
||||
新 SaaS 项目统一按 7 步执行:
|
||||
|
||||
1. 定目标
|
||||
2. 写文档
|
||||
3. 拆任务
|
||||
4. 做设计
|
||||
5. 写代码
|
||||
6. Review / 测试
|
||||
7. 提交 / 发布
|
||||
|
||||
禁止跳步直接开写。
|
||||
|
||||
---
|
||||
|
||||
## 2. 需求到开发的流程
|
||||
|
||||
### 第 1 步:需求确认
|
||||
|
||||
由老大提出:
|
||||
|
||||
- 目标
|
||||
- 用户对象
|
||||
- 边界
|
||||
- 验收标准
|
||||
|
||||
### 第 2 步:小虾整理
|
||||
|
||||
小虾负责:
|
||||
|
||||
- 转成结构化需求
|
||||
- 明确范围
|
||||
- 识别依赖
|
||||
- 标记风险
|
||||
|
||||
### 第 3 步:文档先行
|
||||
|
||||
至少先补以下之一:
|
||||
|
||||
- PRD / 范围文档
|
||||
- 架构文档
|
||||
- 流程文档
|
||||
- 数据模型草稿
|
||||
- API 草稿
|
||||
|
||||
文档不清,不开工。
|
||||
|
||||
### 第 4 步:任务拆解
|
||||
|
||||
一个功能必须拆成明确任务,例如:
|
||||
|
||||
- 前端页面
|
||||
- API 接口
|
||||
- 用例层
|
||||
- 数据模型
|
||||
- 任务执行链
|
||||
- 测试
|
||||
|
||||
### 第 5 步:实现
|
||||
|
||||
按既定分工执行:
|
||||
|
||||
- UI AI 先出页面方案
|
||||
- 小虾定架构和边界
|
||||
- 编码 AI 实现模块
|
||||
|
||||
### 第 6 步:Review 和测试
|
||||
|
||||
必须独立执行:
|
||||
|
||||
- Review AI 看架构和风险
|
||||
- QA AI 看测试和回归
|
||||
- CI 跑自动检查
|
||||
|
||||
### 第 7 步:收口
|
||||
|
||||
小虾负责:
|
||||
|
||||
- 汇总变更
|
||||
- 检查一致性
|
||||
- 确认是否满足验收标准
|
||||
- 再执行提交/推送/发布
|
||||
|
||||
---
|
||||
|
||||
## 3. 提交流程
|
||||
|
||||
统一流程:
|
||||
|
||||
1. 开发完成
|
||||
2. 本地检查 / 单测 / 必要验证
|
||||
3. Review
|
||||
4. CI 通过
|
||||
5. `git add`
|
||||
6. `git commit`
|
||||
7. `git push`
|
||||
8. 记录发布或变更说明
|
||||
|
||||
铁律:
|
||||
|
||||
- 不能停在 commit
|
||||
- 没 review / 没测试 / 没 CI 通过的代码不能进主线
|
||||
|
||||
---
|
||||
|
||||
## 4. 验收流程
|
||||
|
||||
每个功能都必须有验收标准。
|
||||
|
||||
验收至少包含:
|
||||
|
||||
- 功能是否符合目标
|
||||
- 是否超出边界
|
||||
- 是否破坏架构
|
||||
- 是否有测试
|
||||
- 是否可回归
|
||||
- 是否文档同步
|
||||
|
||||
如果只是“代码写出来了”,不算完成。
|
||||
|
||||
---
|
||||
|
||||
## 5. AI 协作制度
|
||||
|
||||
### 5.1 分工制度
|
||||
|
||||
- 老大:产品 owner
|
||||
- 小虾:技术 owner
|
||||
- UI AI:页面和交互
|
||||
- 编码 AI:具体实现
|
||||
- Review AI:代码审查
|
||||
- QA AI:测试和回归
|
||||
|
||||
### 5.2 一个任务一个 owner
|
||||
|
||||
- 一个任务只能有一个主执行者
|
||||
- 其他 AI 可以辅助,不能平行乱改
|
||||
|
||||
### 5.3 架构收口权唯一
|
||||
|
||||
- 架构方向由小虾收口
|
||||
- 老大拍板
|
||||
- 其他 AI 不能随意改主方向
|
||||
|
||||
### 5.4 AI 输出不能直接上线
|
||||
|
||||
AI 输出必须经过:
|
||||
|
||||
- 人工确认
|
||||
- Code Review
|
||||
- 自动化测试
|
||||
- CI 验证
|
||||
|
||||
---
|
||||
|
||||
## 6. 分支与变更管理建议
|
||||
|
||||
如果新项目正式启动,建议采用:
|
||||
|
||||
- `master` / `main`:稳定主线
|
||||
- 功能分支:按模块开发
|
||||
- 重要改动走 PR / 合并审查
|
||||
|
||||
如果前期仍是小团队轻量开发,也至少要做到:
|
||||
|
||||
- 每次改动范围明确
|
||||
- commit message 清楚
|
||||
- 一轮改动一个主题
|
||||
|
||||
---
|
||||
|
||||
## 7. 发布流程
|
||||
|
||||
发布前必须确认:
|
||||
|
||||
- 功能验收通过
|
||||
- 回归通过
|
||||
- CI 通过
|
||||
- 文档更新
|
||||
- 日志/监控可用
|
||||
- 回滚方式明确
|
||||
|
||||
发布后必须确认:
|
||||
|
||||
- 服务在线
|
||||
- 核心流程在线可用
|
||||
- 巡检和告警正常
|
||||
|
||||
---
|
||||
|
||||
## 8. 不允许再发生的情况
|
||||
|
||||
- 边想边改、越改越乱
|
||||
- 没有文档直接开工
|
||||
- 多个 AI 同时改同一块逻辑
|
||||
- AI 代码直接进主线
|
||||
- 口头规则替代正式制度
|
||||
- 旧桌面版那种临时修复脚本文化带入新项目
|
||||
|
||||
---
|
||||
|
||||
## 9. 当前阶段结论
|
||||
|
||||
新 SaaS 项目从第一天开始就要按正规软件流程走:
|
||||
|
||||
- 先定目标
|
||||
- 再写文档
|
||||
- 再拆任务
|
||||
- 再设计
|
||||
- 再实现
|
||||
- 再 review / 测试
|
||||
- 最后提交 / 发布
|
||||
|
||||
这样我们后面人再多、AI 再多,也不会重新回到混乱状态。
|
||||
@@ -0,0 +1,66 @@
|
||||
# SaaS 重建总索引
|
||||
|
||||
本文档是新 SaaS 项目的总入口。以后所有重建相关文档都从这里进入,避免分散。
|
||||
|
||||
---
|
||||
|
||||
## 1. 总目标
|
||||
|
||||
- 旧桌面版不再作为未来主线
|
||||
- 新 SaaS 项目重新开始
|
||||
- 先定边界、规矩、工具,再进入开发
|
||||
- 所有新项目工作必须新旧隔离
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心总文档
|
||||
|
||||
### 启动与总纲
|
||||
|
||||
- [SaaS 重建启动方案](saas-rebuild-kickoff-plan.md)
|
||||
- [SaaS 重建盘点总表](saas-rebuild-inventory.md)
|
||||
|
||||
### 工具与基础设施
|
||||
|
||||
- [AI 团队工具清单](ai-team-tooling-checklist.md)
|
||||
- [基础设施保留 / 废弃 / 重建清单](infrastructure-keep-rebuild-matrix.md)
|
||||
|
||||
### 业务与技术
|
||||
|
||||
- [SaaS 核心对象清单](saas-core-objects.md)
|
||||
- [SaaS 核心流程清单](saas-core-flows.md)
|
||||
- [SaaS 技术栈选型表](saas-tech-stack-options.md)
|
||||
|
||||
### 启动范围与工程制度
|
||||
|
||||
- [SaaS 一期 MVP 范围清单](saas-mvp-scope.md)
|
||||
- [SaaS 项目目录结构规范](saas-project-structure-spec.md)
|
||||
- [SaaS 开发协作流程与制度](saas-development-workflow.md)
|
||||
|
||||
---
|
||||
|
||||
## 3. 阅读顺序
|
||||
|
||||
建议按这个顺序看:
|
||||
|
||||
1. `saas-rebuild-kickoff-plan.md`
|
||||
2. `saas-rebuild-inventory.md`
|
||||
3. `ai-team-tooling-checklist.md`
|
||||
4. `infrastructure-keep-rebuild-matrix.md`
|
||||
5. `saas-core-objects.md`
|
||||
6. `saas-core-flows.md`
|
||||
7. `saas-tech-stack-options.md`
|
||||
8. `saas-mvp-scope.md`
|
||||
9. `saas-project-structure-spec.md`
|
||||
10. `saas-development-workflow.md`
|
||||
|
||||
---
|
||||
|
||||
## 4. 当前结论
|
||||
|
||||
现在已经具备启动 SaaS 重建前最重要的文档骨架。下一步应该进入:
|
||||
|
||||
- 新项目落点确认
|
||||
- 仓库/目录方案确认
|
||||
- 第一版工程骨架设计
|
||||
- 之后才进入真正开发
|
||||
@@ -0,0 +1,194 @@
|
||||
# SaaS 一期 MVP 范围清单
|
||||
|
||||
本文档定义新 SaaS 项目的第一阶段最小可用版本(MVP)范围。
|
||||
|
||||
目标不是把桌面版全部能力一口气搬完,而是先做出一条 **真实可用、能闭环、能上线、能继续扩展** 的主链路。
|
||||
|
||||
---
|
||||
|
||||
## 1. MVP 目标
|
||||
|
||||
一期 MVP 必须实现:
|
||||
|
||||
- 用户能登录进入系统
|
||||
- 用户能创建项目
|
||||
- 用户能上传素材
|
||||
- 系统能完成素材分类
|
||||
- 用户能发起生成任务
|
||||
- 用户能看到任务进度
|
||||
- 用户能拿到生成结果
|
||||
|
||||
一句话:
|
||||
|
||||
**从“进入系统”到“拿到成片”这条主链必须完整跑通。**
|
||||
|
||||
---
|
||||
|
||||
## 2. 一期必须做的能力
|
||||
|
||||
### 2.1 账号与工作空间
|
||||
|
||||
必须做:
|
||||
|
||||
- 登录
|
||||
- 基础用户系统
|
||||
- 默认工作空间
|
||||
|
||||
一期先简化:
|
||||
|
||||
- 单用户 / 单工作空间也可以
|
||||
- 但数据模型必须保留未来扩展到团队/多租户的空间
|
||||
|
||||
### 2.2 项目管理
|
||||
|
||||
必须做:
|
||||
|
||||
- 创建项目
|
||||
- 项目列表
|
||||
- 项目详情页 / 工作台入口
|
||||
|
||||
不必一开始就做:
|
||||
|
||||
- 项目归档
|
||||
- 项目模板
|
||||
- 复杂项目权限
|
||||
|
||||
### 2.3 素材管理
|
||||
|
||||
必须做:
|
||||
|
||||
- 上传视频/图片素材
|
||||
- 上传配音素材
|
||||
- 查看素材列表
|
||||
- 查看配音列表
|
||||
- 基础素材库概念
|
||||
|
||||
必须具备的质量底线:
|
||||
|
||||
- 基础去重/错误校验
|
||||
- 基础状态可见
|
||||
- 明确的归属(项目 / 工作空间)
|
||||
|
||||
### 2.4 素材分类与诊断
|
||||
|
||||
必须做:
|
||||
|
||||
- 触发素材分类任务
|
||||
- 返回分类结果
|
||||
- 能看到素材准备度 / 诊断摘要
|
||||
|
||||
一期可以简化:
|
||||
|
||||
- 先做基础分类与基础诊断
|
||||
- 高级风险评分和复杂建议后续再增强
|
||||
|
||||
### 2.5 生成任务
|
||||
|
||||
必须做:
|
||||
|
||||
- 提交单次生成任务
|
||||
- 提交批量生成任务
|
||||
- 后台异步执行
|
||||
- 查询任务状态
|
||||
- 查询任务进度
|
||||
|
||||
必须保留的关键规则:
|
||||
|
||||
- 单项失败不能拖垮整批
|
||||
- 长任务不能阻塞主请求
|
||||
- 任务必须有状态、日志和失败原因
|
||||
|
||||
### 2.6 成片结果
|
||||
|
||||
必须做:
|
||||
|
||||
- 结果列表
|
||||
- 结果详情 / 基础信息
|
||||
- 下载成片
|
||||
|
||||
一期可以不做:
|
||||
|
||||
- 一键发布外部平台
|
||||
- 高级审核流
|
||||
- 结果版本对比
|
||||
|
||||
---
|
||||
|
||||
## 3. 一期建议做,但不是强阻塞
|
||||
|
||||
这些能力建议尽量做,但如果影响整体交付,可以延后到 1.1 或 1.2:
|
||||
|
||||
- 素材筛选与搜索
|
||||
- 配音试听
|
||||
- 更丰富的诊断报告
|
||||
- 任务失败后的手动重试
|
||||
- 结果包下载
|
||||
- 更细的操作日志
|
||||
|
||||
---
|
||||
|
||||
## 4. 一期明确不做的内容
|
||||
|
||||
以下内容不应阻塞 MVP 启动:
|
||||
|
||||
- 支付 / 套餐系统
|
||||
- 模板市场
|
||||
- 复杂 RBAC 权限体系
|
||||
- 多层级审批流
|
||||
- 自动发布到外部平台
|
||||
- 超复杂的数据看板
|
||||
- 高级团队协作机制
|
||||
- 面向外部客户的多租户计费能力
|
||||
|
||||
这些都是未来功能,不属于 MVP 起步必要条件。
|
||||
|
||||
---
|
||||
|
||||
## 5. MVP 验收标准
|
||||
|
||||
一期 MVP 完成的标准不是“页面很多”,而是以下闭环成立:
|
||||
|
||||
1. 用户登录进入工作空间
|
||||
2. 创建项目
|
||||
3. 上传视频/图片/配音素材
|
||||
4. 触发素材分类
|
||||
5. 提交单次或批量生成
|
||||
6. 系统后台执行任务
|
||||
7. 前端可见任务状态和进度
|
||||
8. 用户能查看并下载结果
|
||||
|
||||
如果这 8 步稳定成立,MVP 就合格。
|
||||
|
||||
---
|
||||
|
||||
## 6. MVP 技术验收标准
|
||||
|
||||
除了功能闭环,还必须满足工程验收:
|
||||
|
||||
- 代码结构符合目标架构
|
||||
- 前后端分层清晰
|
||||
- 核心接口有文档
|
||||
- 核心任务链有自动化测试
|
||||
- CI 可跑通
|
||||
- 有基础日志和错误追踪
|
||||
- 有最小可用部署方案
|
||||
|
||||
---
|
||||
|
||||
## 7. 当前建议结论
|
||||
|
||||
一期 MVP 只抓核心价值:
|
||||
|
||||
- 项目
|
||||
- 素材
|
||||
- 分类
|
||||
- 生成
|
||||
- 结果
|
||||
|
||||
先把主链路做实,再逐步叠加:
|
||||
|
||||
- 团队协作
|
||||
- 高级权限
|
||||
- 发布能力
|
||||
- 运营能力
|
||||
- 商业化能力
|
||||
@@ -0,0 +1,123 @@
|
||||
# SaaS 新项目落点与启动方式(第一版)
|
||||
|
||||
本文档定义新 SaaS 项目的物理落点、仓库边界和启动方式。
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标
|
||||
|
||||
新 SaaS 项目必须做到:
|
||||
|
||||
- 与旧桌面版彻底隔离
|
||||
- 有单独仓库边界或至少单独根目录边界
|
||||
- 可以独立开发、测试、部署
|
||||
- 不会把旧实现混进新主线
|
||||
|
||||
---
|
||||
|
||||
## 2. 推荐落点策略
|
||||
|
||||
### 2.1 推荐方案:独立仓库 + 独立目录
|
||||
|
||||
最稳妥方案是:
|
||||
|
||||
- 新 SaaS 使用独立 Git 仓库
|
||||
- 新项目有独立顶层目录
|
||||
- 旧桌面版继续保留在当前仓库中作为参考样本
|
||||
|
||||
### 2.2 推荐目录命名
|
||||
|
||||
如果在当前机器上先做本地启动,可以使用:
|
||||
|
||||
- `F:\openclaw-saas\`:新项目总目录
|
||||
- `F:\openclaw-saas\docs\`
|
||||
- `F:\openclaw-saas\apps\web\`
|
||||
- `F:\openclaw-saas\apps\api\`
|
||||
- `F:\openclaw-saas\apps\worker\`
|
||||
- `F:\openclaw-saas\packages\domain\`
|
||||
- `F:\openclaw-saas\packages\application\`
|
||||
- `F:\openclaw-saas\packages\ports\`
|
||||
- `F:\openclaw-saas\packages\adapters\`
|
||||
|
||||
### 2.3 推荐仓库命名
|
||||
|
||||
建议仓库名保持清楚:
|
||||
|
||||
- `xiaoxia-saas`
|
||||
- 或 `xiaoxia-autocut-saas`
|
||||
|
||||
原则:
|
||||
|
||||
- 名字要能一眼看出是新主线
|
||||
- 不要和旧桌面版仓库名字混淆
|
||||
|
||||
---
|
||||
|
||||
## 3. 为什么推荐独立仓库
|
||||
|
||||
### 好处
|
||||
|
||||
- 物理隔离最彻底
|
||||
- 不会把旧桌面版文件误加进来
|
||||
- CI/CD、依赖、部署都可以单独设计
|
||||
- 后续多人协作更清楚
|
||||
- 更符合正规 SaaS 项目的工程实践
|
||||
|
||||
### 风险
|
||||
|
||||
- 初期需要重新搭仓库和流水线
|
||||
- 需要重新整理文档和工程规范
|
||||
|
||||
这个成本是值得的,因为它换来的是长期稳定性。
|
||||
|
||||
---
|
||||
|
||||
## 4. 启动方式
|
||||
|
||||
### 4.1 启动前先做的事
|
||||
|
||||
1. 确认产品边界
|
||||
2. 确认一期 MVP
|
||||
3. 确认技术栈
|
||||
4. 确认目录结构
|
||||
5. 确认 AI 工具清单
|
||||
6. 确认协作制度
|
||||
|
||||
### 4.2 启动时要先建的东西
|
||||
|
||||
- 新仓库
|
||||
- docs 总目录
|
||||
- domain/application/ports/adapters 基础包
|
||||
- web/api/worker 三个应用入口
|
||||
- CI/CD 初版
|
||||
- 测试目录
|
||||
|
||||
### 4.3 启动时不先做的东西
|
||||
|
||||
- 不先做复杂权限
|
||||
- 不先做支付
|
||||
- 不先做多租户高级方案
|
||||
- 不先做复杂发布市场
|
||||
|
||||
---
|
||||
|
||||
## 5. 与旧桌面版的关系
|
||||
|
||||
- 旧桌面版继续保留在当前仓库中
|
||||
- 旧桌面版不再继续作为未来主线开发
|
||||
- 新 SaaS 只参考旧桌面版的业务规则和经验
|
||||
- 新 SaaS 不依赖旧桌面版的安装链路和 Tkinter 结构
|
||||
|
||||
---
|
||||
|
||||
## 6. 当前建议结论
|
||||
|
||||
### 最佳启动方案
|
||||
|
||||
- 独立仓库
|
||||
- 独立目录
|
||||
- 独立 CI/CD
|
||||
- 独立文档
|
||||
- 独立发布链路
|
||||
|
||||
这样新项目才能真正从混乱旧代码中脱离出来。
|
||||
@@ -0,0 +1,247 @@
|
||||
# SaaS 项目目录结构规范(第一版)
|
||||
|
||||
本文档定义新 SaaS 项目的目录结构和职责边界。
|
||||
|
||||
原则:
|
||||
|
||||
- 新旧隔离
|
||||
- 前后端隔离
|
||||
- 业务层和框架层隔离
|
||||
- 目录一旦定下,后续新增功能必须按结构落位
|
||||
|
||||
---
|
||||
|
||||
## 1. 顶层结构建议
|
||||
|
||||
```text
|
||||
saas/
|
||||
docs/
|
||||
apps/
|
||||
web/
|
||||
api/
|
||||
worker/
|
||||
packages/
|
||||
domain/
|
||||
application/
|
||||
ports/
|
||||
adapters/
|
||||
shared/
|
||||
infra/
|
||||
docker/
|
||||
nginx/
|
||||
scripts/
|
||||
tests/
|
||||
integration/
|
||||
e2e/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 目录职责
|
||||
|
||||
### 2.1 `docs/`
|
||||
|
||||
存放所有正式文档:
|
||||
|
||||
- PRD
|
||||
- 架构文档
|
||||
- 数据模型
|
||||
- API 规范
|
||||
- 发布说明
|
||||
- 运维说明
|
||||
- 里程碑和范围文档
|
||||
|
||||
规则:
|
||||
|
||||
- 文档必须是单一真相源
|
||||
- 先改文档,再改实现
|
||||
|
||||
### 2.2 `apps/web/`
|
||||
|
||||
前端 Web 应用。
|
||||
|
||||
职责:
|
||||
|
||||
- 页面
|
||||
- 路由
|
||||
- 组件
|
||||
- 页面状态绑定
|
||||
- 调用后端 API
|
||||
|
||||
禁止:
|
||||
|
||||
- 在这里写核心业务规则
|
||||
- 在这里写任务执行逻辑
|
||||
|
||||
### 2.3 `apps/api/`
|
||||
|
||||
后端 API 应用。
|
||||
|
||||
职责:
|
||||
|
||||
- 提供 HTTP API
|
||||
- 鉴权
|
||||
- 请求校验
|
||||
- 调用 Application Use Cases
|
||||
|
||||
禁止:
|
||||
|
||||
- 把业务规则直接写在路由层
|
||||
- 直接耦合底层外部实现
|
||||
|
||||
### 2.4 `apps/worker/`
|
||||
|
||||
后台任务执行应用。
|
||||
|
||||
职责:
|
||||
|
||||
- 执行素材分类任务
|
||||
- 执行视频生成任务
|
||||
- 执行同步和诊断任务
|
||||
|
||||
原则:
|
||||
|
||||
- Worker 只执行任务,不处理 Web 交互
|
||||
|
||||
### 2.5 `packages/domain/`
|
||||
|
||||
核心业务对象和值对象。
|
||||
|
||||
职责:
|
||||
|
||||
- User
|
||||
- Workspace
|
||||
- Project
|
||||
- Asset
|
||||
- AssetLibrary
|
||||
- GenerationTask
|
||||
- GeneratedVideo
|
||||
- 业务规则
|
||||
|
||||
禁止:
|
||||
|
||||
- import Web 框架
|
||||
- import 数据库驱动
|
||||
- import UI 工具
|
||||
|
||||
### 2.6 `packages/application/`
|
||||
|
||||
用例层。
|
||||
|
||||
职责:
|
||||
|
||||
- 创建项目
|
||||
- 上传素材
|
||||
- 分类素材
|
||||
- 发起生成
|
||||
- 查询进度
|
||||
- 获取结果
|
||||
|
||||
原则:
|
||||
|
||||
- 编排业务流程
|
||||
- 通过 ports 调外部能力
|
||||
|
||||
### 2.7 `packages/ports/`
|
||||
|
||||
接口定义层。
|
||||
|
||||
职责:
|
||||
|
||||
- 仓储接口
|
||||
- 存储接口
|
||||
- 队列接口
|
||||
- 通知接口
|
||||
- 日志接口
|
||||
|
||||
原则:
|
||||
|
||||
- 不依赖具体实现
|
||||
|
||||
### 2.8 `packages/adapters/`
|
||||
|
||||
外部实现层。
|
||||
|
||||
职责:
|
||||
|
||||
- PostgreSQL 实现
|
||||
- Redis / Celery 实现
|
||||
- OSS / S3 实现
|
||||
- 鉴权实现
|
||||
- 日志实现
|
||||
|
||||
### 2.9 `packages/shared/`
|
||||
|
||||
共享的通用工具和基础类型。
|
||||
|
||||
职责:
|
||||
|
||||
- 通用配置
|
||||
- 错误类型
|
||||
- 公共 schema
|
||||
- 公共常量
|
||||
|
||||
规则:
|
||||
|
||||
- 只放真正跨层复用的通用内容
|
||||
- 禁止把业务逻辑偷塞进 shared
|
||||
|
||||
### 2.10 `infra/`
|
||||
|
||||
基础设施目录。
|
||||
|
||||
职责:
|
||||
|
||||
- Docker
|
||||
- Nginx
|
||||
- 启动脚本
|
||||
- 本地开发环境脚本
|
||||
- 部署脚本
|
||||
|
||||
### 2.11 `tests/`
|
||||
|
||||
测试目录。
|
||||
|
||||
建议拆分:
|
||||
|
||||
- `integration/`:前后端/数据库/队列联调测试
|
||||
- `e2e/`:核心流程端到端测试
|
||||
|
||||
应用/包内部也可各自保留单元测试目录。
|
||||
|
||||
---
|
||||
|
||||
## 3. 模块新增规则
|
||||
|
||||
以后新增功能时,按这个顺序决定落点:
|
||||
|
||||
1. 是业务对象?放 `domain`
|
||||
2. 是业务用例?放 `application`
|
||||
3. 是接口定义?放 `ports`
|
||||
4. 是外部实现?放 `adapters`
|
||||
5. 是页面或交互?放 `apps/web`
|
||||
6. 是 API 路由?放 `apps/api`
|
||||
7. 是后台任务执行?放 `apps/worker`
|
||||
8. 是部署与运行?放 `infra`
|
||||
|
||||
---
|
||||
|
||||
## 4. 禁止事项
|
||||
|
||||
- 禁止把旧桌面版目录结构照搬过来
|
||||
- 禁止在 `web` / `api` 中直接写核心业务规则
|
||||
- 禁止 `domain` 依赖数据库、Redis、OSS、框架
|
||||
- 禁止 `shared` 成为大杂烩
|
||||
- 禁止把临时脚本堆进项目根目录
|
||||
|
||||
---
|
||||
|
||||
## 5. 当前阶段结论
|
||||
|
||||
新 SaaS 项目必须从第一天就按这个结构起步。
|
||||
|
||||
这样做的价值是:
|
||||
|
||||
- 不会再回到单体混乱结构
|
||||
- AI 工具更容易分工协作
|
||||
- 后面扩功能、拆模块、加 worker 都更顺
|
||||
@@ -0,0 +1,354 @@
|
||||
# 小虾 SaaS 重建盘点总表(第一版)
|
||||
|
||||
本文档用于回答两个最核心的问题:
|
||||
|
||||
1. 我们现在已经有什么?
|
||||
2. 我们为了启动新的 SaaS 项目,还缺什么?
|
||||
|
||||
目的不是罗列名词,而是为后续 **产品边界、技术选型、架构设计、AI 协作分工、开发排期** 提供真实依据。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前已有资产总览
|
||||
|
||||
当前资产分为 5 类:
|
||||
|
||||
1. 业务资产
|
||||
2. 工程资产
|
||||
3. 基础设施资产
|
||||
4. AI 协作资产
|
||||
5. 人和流程资产
|
||||
|
||||
这些是我们重建 SaaS 时真正可以利用的起点。
|
||||
|
||||
---
|
||||
|
||||
## 2. 业务资产盘点
|
||||
|
||||
### 2.1 已有功能样本
|
||||
|
||||
现有桌面版已经覆盖了未来 SaaS 需要继承的主要业务能力:
|
||||
|
||||
- 素材库管理
|
||||
- 配音库管理
|
||||
- 素材分类
|
||||
- 素材缺口诊断
|
||||
- 视频生成
|
||||
- 批量生成
|
||||
- 云同步
|
||||
- 更新治理
|
||||
- 错误治理
|
||||
- 稳定性治理
|
||||
|
||||
### 2.2 已沉淀的业务规则
|
||||
|
||||
这些规则以后不一定直接复用代码,但应该复用为 SaaS 业务规则或用例设计输入:
|
||||
|
||||
- 坏素材文件必须隔离,不能拖垮整批扫描
|
||||
- 单个候选生成失败不能拖垮整个批次
|
||||
- 长任务必须异步执行,不能阻塞 UI / 主流程
|
||||
- 更新必须可回滚、可记录状态
|
||||
- 主流程必须有错误诊断和恢复能力
|
||||
- 核心业务逻辑比表面覆盖率数字更重要
|
||||
|
||||
### 2.3 现有业务理解深度
|
||||
|
||||
我们已经搞清楚了这些关键业务对象和关系:
|
||||
|
||||
- 视频素材
|
||||
- 配音素材
|
||||
- 素材库
|
||||
- 配音库
|
||||
- 生成策略
|
||||
- 批量生成任务
|
||||
- 生成结果
|
||||
- 云端同步对象
|
||||
- 更新版本 / 补丁包
|
||||
- 错误诊断记录
|
||||
|
||||
这意味着新 SaaS 不是从“需求不清楚”开始,而是从“实现要重做,但业务已经摸透”开始。
|
||||
|
||||
---
|
||||
|
||||
## 3. 工程资产盘点
|
||||
|
||||
### 3.1 现有代码资产
|
||||
|
||||
旧桌面版代码虽然不再作为未来主线,但仍然是非常重要的参考资产:
|
||||
|
||||
- 它提供了完整功能样本
|
||||
- 它暴露了真实复杂度和真实坑点
|
||||
- 它帮助我们识别哪些模块是高风险区
|
||||
- 它为 SaaS 重建提供业务规则、异常分支、边界条件参考
|
||||
|
||||
### 3.2 已有架构资产
|
||||
|
||||
虽然旧系统的最终实现不一致,但我们已经沉淀出一些正确方向:
|
||||
|
||||
- `Domain → Repository → Service → Facade` 的分层意识
|
||||
- `Controller / View` 分离意识
|
||||
- `EventBus` 的组件解耦尝试
|
||||
- `Ports / Adapters / Application / Presentation` 新基线目录已经建立
|
||||
|
||||
### 3.3 已有测试资产
|
||||
|
||||
当前旧项目已经有一定测试基础:
|
||||
|
||||
- controller 测试
|
||||
- service 测试
|
||||
- repository 测试
|
||||
- domain 测试
|
||||
- update rollback 测试
|
||||
- startup guard 测试
|
||||
- material isolation 测试
|
||||
|
||||
这些测试未来不一定直接复用,但可以帮助我们:
|
||||
|
||||
- 提取业务规则
|
||||
- 识别边界条件
|
||||
- 设计 SaaS 版测试策略
|
||||
|
||||
---
|
||||
|
||||
## 4. 基础设施资产盘点
|
||||
|
||||
### 4.1 当前已经有的基础设施
|
||||
|
||||
- **代码仓库**:Gitea
|
||||
- **CI 基础**:已有 CI 经验和工作流雏形
|
||||
- **巡检机器人**:已有自动巡检思路和脚本基础
|
||||
- **开发环境**:Windows 开发机 + Ubuntu 服务器
|
||||
- **发布/同步经验**:已有正式版同步、回滚、状态记录经验
|
||||
|
||||
### 4.2 这些资产的保留策略
|
||||
|
||||
#### 必须保留
|
||||
|
||||
- Gitea
|
||||
- CI/CD 思路
|
||||
- 巡检机器人
|
||||
- 服务器资源
|
||||
- 版本管理流程
|
||||
|
||||
#### 只保留经验,不保留实现
|
||||
|
||||
- Tkinter 桌面版安装发布链
|
||||
- 桌面版同步脚本体系
|
||||
- 旧桌面版兼容逻辑
|
||||
- 临时修复脚本文化
|
||||
|
||||
---
|
||||
|
||||
## 5. AI 协作资产盘点
|
||||
|
||||
### 5.1 当前已经具备的 AI 协作方式
|
||||
|
||||
- 已经形成“你拍板、小虾收口”的稳定协作模式
|
||||
- 已经知道 AI 不能一个人包完所有角色
|
||||
- 已经明确未来需要分角色使用 AI:
|
||||
- 架构
|
||||
- UI
|
||||
- 编码
|
||||
- Review
|
||||
- QA
|
||||
- 文档
|
||||
|
||||
### 5.2 当前还没有真正落地的 AI 团队能力
|
||||
|
||||
还缺:
|
||||
|
||||
- 明确的 AI 工具采购清单
|
||||
- 明确的 AI 账号归属
|
||||
- 明确的角色与工具映射
|
||||
- 明确的 AI 输入输出规范
|
||||
- 明确的安全边界(密钥、服务器、数据库等)
|
||||
|
||||
---
|
||||
|
||||
## 6. 人和流程资产盘点
|
||||
|
||||
### 6.1 当前团队形态
|
||||
|
||||
本质上还是:
|
||||
|
||||
- 你:产品负责人
|
||||
- 小虾:技术负责人 / 协调者
|
||||
- 其他 AI:按角色配合
|
||||
|
||||
这是一个“小团队”模式,所以新系统必须遵守两个现实:
|
||||
|
||||
- 技术栈不能过重
|
||||
- 流程不能过重
|
||||
|
||||
### 6.2 当前已有流程经验
|
||||
|
||||
我们已经明确过的重要工程流程:
|
||||
|
||||
- 源码只在单一源目录修改
|
||||
- 修改后先验证
|
||||
- 再同步正式版
|
||||
- 再确认
|
||||
- 再 `git add / commit / push`
|
||||
- 不能停在 commit
|
||||
|
||||
这个流程意识未来可以保留,但要升级为 SaaS 项目的正式流程。
|
||||
|
||||
---
|
||||
|
||||
## 7. 当前最核心的缺口
|
||||
|
||||
下面这些是现在真正缺少的,而且不补就不能稳启动 SaaS 项目。
|
||||
|
||||
### 7.1 产品边界缺口
|
||||
|
||||
还没最终拍板:
|
||||
|
||||
- SaaS 面向谁
|
||||
- 单用户还是团队使用
|
||||
- 是否多租户
|
||||
- 是否有工作区概念
|
||||
- 是否要权限系统
|
||||
- 是否要付费/套餐系统
|
||||
|
||||
### 7.2 系统边界缺口
|
||||
|
||||
还没最终拍板:
|
||||
|
||||
- 素材文件存储在哪里
|
||||
- 视频生成在什么节点执行
|
||||
- 是否需要分布式 worker
|
||||
- 是否有对象存储 / OSS / S3
|
||||
- 是否支持多设备 / 多节点协同
|
||||
|
||||
### 7.3 技术选型缺口
|
||||
|
||||
还没确认:
|
||||
|
||||
- 前端框架
|
||||
- 后端框架
|
||||
- 数据库
|
||||
- 队列系统
|
||||
- 任务调度方式
|
||||
- 鉴权方式
|
||||
- 部署方式
|
||||
- 日志/监控方案
|
||||
|
||||
### 7.4 工程制度缺口
|
||||
|
||||
还没正式落地:
|
||||
|
||||
- SaaS 项目目录结构
|
||||
- API 规范
|
||||
- 数据模型规范
|
||||
- 提交流程
|
||||
- 分支策略
|
||||
- 测试规范
|
||||
- 发布规范
|
||||
- 回滚规范
|
||||
|
||||
### 7.5 AI 团队工具缺口
|
||||
|
||||
还没确认和落地:
|
||||
|
||||
- 编码 AI 用哪些
|
||||
- UI AI 用哪些
|
||||
- Review AI 用哪些
|
||||
- QA AI 用哪些
|
||||
- 工具怎么购买 / 开通 / 管理
|
||||
- 哪些人/角色使用哪些工具
|
||||
|
||||
---
|
||||
|
||||
## 8. 当前最应该保留的东西
|
||||
|
||||
### 必须保留
|
||||
|
||||
- 业务规则和业务理解
|
||||
- Gitea 仓库体系
|
||||
- CI/CD 思路
|
||||
- 巡检/健康检查能力
|
||||
- 稳定性治理经验
|
||||
- 测试意识
|
||||
- 版本与回滚意识
|
||||
|
||||
### 必须放弃
|
||||
|
||||
- 继续在旧桌面版上做主线重构
|
||||
- 临时脚本救火式开发
|
||||
- 架构半拉子状态
|
||||
- 一边想边改、一边改一边返工
|
||||
|
||||
---
|
||||
|
||||
## 9. 下一步盘点顺序
|
||||
|
||||
现在不应该直接进入代码开发。
|
||||
|
||||
接下来建议按这个顺序盘:
|
||||
|
||||
### 第 1 张表:我们现在有什么(当前文档)
|
||||
|
||||
已开始。
|
||||
|
||||
### 第 2 张表:我们还缺什么
|
||||
|
||||
在当前文档中已经列出核心缺口,但还需要进一步定级:
|
||||
|
||||
- 必须先有
|
||||
- 可以后补
|
||||
- 暂时不做
|
||||
|
||||
### 第 3 张表:AI 团队工具清单
|
||||
|
||||
需要补充:
|
||||
|
||||
- 工具名称
|
||||
- 用途
|
||||
- 谁用
|
||||
- 优先级
|
||||
- 获取方式
|
||||
- 是否立即需要
|
||||
|
||||
### 第 4 张表:基础设施保留 / 废弃 / 重建清单
|
||||
|
||||
需要明确:
|
||||
|
||||
- 哪些沿用
|
||||
- 哪些只保留经验
|
||||
- 哪些必须全新搭建
|
||||
|
||||
---
|
||||
|
||||
## 10. 当前阶段的结论
|
||||
|
||||
### 我们现在真正拥有的,不只是代码
|
||||
|
||||
我们拥有:
|
||||
|
||||
- 完整业务样本
|
||||
- 已知复杂度
|
||||
- 已知坑点
|
||||
- 已知异常处理经验
|
||||
- 已有工程基础设施
|
||||
- 已有协作方式
|
||||
|
||||
### 我们现在真正缺的,也不是代码
|
||||
|
||||
我们缺:
|
||||
|
||||
- 清晰边界
|
||||
- 明确规则
|
||||
- 技术选型
|
||||
- AI 工具体系
|
||||
- 正式工程制度
|
||||
|
||||
### 所以下一步的重点
|
||||
|
||||
不是“赶紧开工写 SaaS 代码”,而是:
|
||||
|
||||
- 继续把盘点做全
|
||||
- 把边界定死
|
||||
- 把工具准备好
|
||||
- 把制度落成文档
|
||||
|
||||
只有这样,新的 SaaS 项目才能真正轻装上阵,而不是把旧系统的问题带过去。
|
||||
@@ -0,0 +1,577 @@
|
||||
# 小虾 SaaS 重建启动方案
|
||||
|
||||
## 1. 背景与结论
|
||||
|
||||
当前桌面版代码已经不适合作为未来主线继续演进。
|
||||
|
||||
核心结论:
|
||||
|
||||
- 旧桌面版停止作为未来主线,不再继续做架构性重构。
|
||||
- 新系统按 **SaaS 版** 重新开始开发。
|
||||
- 目标不是“在旧系统上修补”,而是按正规软件工程方式做一个长期可维护、可扩展、可迭代的新系统。
|
||||
- 旧代码只作为 **业务参考样本**、**规则参考**、**流程参考**,不再作为主开发容器。
|
||||
- 新旧代码必须彻底隔离,避免再次混乱。
|
||||
|
||||
## 2. 总目标
|
||||
|
||||
做一个全新的 SaaS 系统,业务功能覆盖当前桌面版的核心能力:
|
||||
|
||||
- 素材库管理
|
||||
- 配音库管理
|
||||
- 素材分类与诊断
|
||||
- 视频生成
|
||||
- 批量生成
|
||||
- 云同步
|
||||
- 更新/版本机制(未来按 SaaS 方式重新设计)
|
||||
- 任务进度与结果管理
|
||||
|
||||
目标要求:
|
||||
|
||||
- 可长期迭代
|
||||
- 可多人协作开发
|
||||
- 可上线部署
|
||||
- 可观察、可回滚、可测试
|
||||
- 不再回到“哪里报错改哪里”的状态
|
||||
|
||||
## 3. 基本原则
|
||||
|
||||
### 3.1 先定规则,再开始
|
||||
|
||||
在新项目开始编码前,必须先确认:
|
||||
|
||||
- 产品边界
|
||||
- 一期范围
|
||||
- 核心对象
|
||||
- 核心流程
|
||||
- 技术栈
|
||||
- 架构分层
|
||||
- 开发规范
|
||||
- 发布规范
|
||||
- AI 团队协作方式
|
||||
|
||||
### 3.2 新旧彻底隔离
|
||||
|
||||
- 旧桌面版代码继续保留,但只作参考
|
||||
- 新 SaaS 项目必须在全新目录中开始
|
||||
- 禁止把旧桌面版代码直接混入新系统主线
|
||||
- 旧系统的安装脚本、Tkinter 逻辑、桌面发布链路不带入新主线
|
||||
|
||||
### 3.3 先盘点,再设计,再开发
|
||||
|
||||
执行顺序必须固定:
|
||||
|
||||
1. 盘点现状
|
||||
2. 确认边界
|
||||
3. 明确规则
|
||||
4. 设计架构
|
||||
5. 搭项目骨架
|
||||
6. 分模块开发
|
||||
7. Review / 测试 / 部署
|
||||
|
||||
## 4. 新系统目标架构
|
||||
|
||||
采用:
|
||||
|
||||
- **Clean Architecture**
|
||||
- **MVVM / Presenter**
|
||||
- **Ports / Adapters**
|
||||
|
||||
目标分层:
|
||||
|
||||
```text
|
||||
Domain
|
||||
↑
|
||||
Application / Use Cases
|
||||
↑
|
||||
Ports
|
||||
↑
|
||||
Adapters
|
||||
↑
|
||||
Presentation (Presenter / ViewModel)
|
||||
↑
|
||||
Web UI / API / Worker
|
||||
```
|
||||
|
||||
原则:
|
||||
|
||||
- 界面不写业务逻辑
|
||||
- 业务逻辑不依赖框架
|
||||
- 外部能力通过接口接入
|
||||
- 任务、文件、存储、云服务全部可替换
|
||||
|
||||
## 5. 我们现在已有的资产
|
||||
|
||||
### 5.1 业务资产
|
||||
|
||||
- 现有桌面版就是最完整的业务样本
|
||||
- 已经沉淀出大量业务规则和异常治理经验
|
||||
- 已经明确的功能模块:
|
||||
- 素材库
|
||||
- 配音库
|
||||
- 批量生成
|
||||
- 云同步
|
||||
- 素材分类
|
||||
- 缺口诊断
|
||||
- 更新治理
|
||||
- 稳定性治理
|
||||
|
||||
### 5.2 工程资产
|
||||
|
||||
- Gitea 代码仓库
|
||||
- CI / 基础自动化能力
|
||||
- 巡检机器人
|
||||
- 项目推进经验
|
||||
- 既有测试体系雏形
|
||||
- 架构规约基础文档
|
||||
|
||||
### 5.3 人和协作资产
|
||||
|
||||
- 你负责产品方向和最终拍板
|
||||
- 小虾负责技术架构、拆解、规范、收口
|
||||
- 已经形成连续迭代经验
|
||||
|
||||
## 6. 我们明显还缺的东西
|
||||
|
||||
### 6.1 产品边界
|
||||
|
||||
还需要明确:
|
||||
|
||||
- SaaS 面向谁
|
||||
- 是个人 SaaS 还是团队 SaaS
|
||||
- 是否多租户
|
||||
- 是否有角色权限系统
|
||||
- 是否有套餐/支付
|
||||
|
||||
### 6.2 系统边界
|
||||
|
||||
还需要明确:
|
||||
|
||||
- 素材文件存在哪里
|
||||
- 视频生成在哪里跑
|
||||
- 任务如何调度
|
||||
- 节点/worker 怎么管理
|
||||
- 哪些功能是一期,哪些延后
|
||||
|
||||
### 6.3 技术边界
|
||||
|
||||
还需要明确:
|
||||
|
||||
- 前端框架
|
||||
- 后端框架
|
||||
- 数据库
|
||||
- 队列系统
|
||||
- 对象存储
|
||||
- 鉴权方式
|
||||
- 部署方式
|
||||
- 监控和日志方案
|
||||
|
||||
## 7. AI 团队工具怎么来
|
||||
|
||||
### 7.1 原则
|
||||
|
||||
前期 **不自研 AI 平台**。
|
||||
|
||||
策略:
|
||||
|
||||
- 直接购买/使用现成 AI 工具
|
||||
- 我们自己做的是协作机制、流程和规则
|
||||
- 工具是能力来源,制度才是稳定产出的关键
|
||||
|
||||
### 7.2 工具来源分类
|
||||
|
||||
#### 编码类 AI
|
||||
|
||||
用于:
|
||||
|
||||
- 写代码
|
||||
- 重构
|
||||
- 补测试
|
||||
- 查仓库
|
||||
- 做具体模块实现
|
||||
|
||||
候选工具:
|
||||
|
||||
- Codex / Codex CLI
|
||||
- Claude Code
|
||||
- Cursor
|
||||
- Windsurf
|
||||
|
||||
#### 通用大模型类
|
||||
|
||||
用于:
|
||||
|
||||
- 需求整理
|
||||
- 架构讨论
|
||||
- 文档撰写
|
||||
- 方案对比
|
||||
- 总结与沟通
|
||||
|
||||
候选工具:
|
||||
|
||||
- ChatGPT
|
||||
- Claude
|
||||
- Gemini
|
||||
|
||||
#### UI / 原型类
|
||||
|
||||
用于:
|
||||
|
||||
- 页面草图
|
||||
- 交互流程
|
||||
- 组件原型
|
||||
- 设计样稿
|
||||
|
||||
候选工具:
|
||||
|
||||
- Figma
|
||||
- Figma AI
|
||||
- v0
|
||||
- Lovable
|
||||
|
||||
#### 协作 / 自动化基础设施
|
||||
|
||||
用于:
|
||||
|
||||
- 代码托管
|
||||
- 持续集成
|
||||
- 告警巡检
|
||||
- 发布流程
|
||||
- 项目推进
|
||||
|
||||
保留资产:
|
||||
|
||||
- Gitea
|
||||
- CI/CD
|
||||
- 巡检机器人
|
||||
- 轻量项目推进机制
|
||||
|
||||
### 7.3 工具获取顺序
|
||||
|
||||
第一批先具备:
|
||||
|
||||
1. ChatGPT / Claude
|
||||
2. Cursor 或同类编码 AI IDE
|
||||
3. Figma
|
||||
4. 保留现有 Gitea / CI / 巡检机器人
|
||||
|
||||
第二批按需要补:
|
||||
|
||||
1. v0 / Lovable
|
||||
2. 专门 Review / QA 的 AI 会话体系
|
||||
3. 更完整的测试辅助和流程自动化工具
|
||||
|
||||
### 7.4 工具不是越多越好
|
||||
|
||||
关键不是堆模型,而是:
|
||||
|
||||
- 谁负责什么
|
||||
- 谁来拍板
|
||||
- 谁来 review
|
||||
- 谁来回归
|
||||
- 所有人是否围绕同一套真相文档工作
|
||||
|
||||
## 8. AI 团队角色分工
|
||||
|
||||
### 8.1 人类角色
|
||||
|
||||
#### 你(产品负责人)
|
||||
|
||||
负责:
|
||||
|
||||
- 方向
|
||||
- 目标
|
||||
- 优先级
|
||||
- 体验偏好
|
||||
- 最终拍板
|
||||
|
||||
#### 小虾(技术负责人 / 总协调)
|
||||
|
||||
负责:
|
||||
|
||||
- 架构方案
|
||||
- 任务拆解
|
||||
- 规范制定
|
||||
- 边界约束
|
||||
- 技术决策收口
|
||||
- 最终合并口径
|
||||
|
||||
### 8.2 AI 角色
|
||||
|
||||
#### UI 设计 AI
|
||||
|
||||
负责:
|
||||
|
||||
- 页面结构
|
||||
- 交互流
|
||||
- 页面草图
|
||||
- 视觉方案辅助
|
||||
|
||||
#### 编码 AI
|
||||
|
||||
负责:
|
||||
|
||||
- 前端实现
|
||||
- 后端实现
|
||||
- 基础模块实现
|
||||
- 脚手架和具体模块开发
|
||||
|
||||
#### Review AI
|
||||
|
||||
负责:
|
||||
|
||||
- 代码审查
|
||||
- 风险检查
|
||||
- 架构偏移检查
|
||||
- 规范一致性检查
|
||||
|
||||
#### QA / Bug AI
|
||||
|
||||
负责:
|
||||
|
||||
- 复现问题
|
||||
- 补测试
|
||||
- 回归验证
|
||||
- 测试建议
|
||||
|
||||
#### 文档 AI
|
||||
|
||||
负责:
|
||||
|
||||
- PRD 草稿
|
||||
- 接口文档
|
||||
- 架构文档
|
||||
- 发布说明
|
||||
- 迁移说明
|
||||
|
||||
### 8.3 协作铁律
|
||||
|
||||
- 一个问题只有一个 owner
|
||||
- 架构只能由一个统一 owner 维护
|
||||
- 写代码的人不能代替 Review
|
||||
- 修 bug 的人不能代替 QA
|
||||
- 所有 AI 输出必须围绕统一文档体系
|
||||
|
||||
## 9. 现有基础设施去留
|
||||
|
||||
### 9.1 保留
|
||||
|
||||
- 代码仓库(Gitea)
|
||||
- CI/CD 理念和流程能力
|
||||
- 巡检机器人
|
||||
- 项目推进机制(轻量化)
|
||||
|
||||
### 9.2 不照搬
|
||||
|
||||
- 旧桌面版安装/同步/发布链路
|
||||
- Tkinter 相关工程结构
|
||||
- 旧系统临时修补脚本文化
|
||||
- 为兼容旧桌面版而存在的特殊逻辑
|
||||
|
||||
### 9.3 新系统要重建的基础设施
|
||||
|
||||
- 新项目仓库结构
|
||||
- 新 CI/CD 流水线
|
||||
- 新日志/监控/告警规范
|
||||
- 新部署流程
|
||||
- 新测试流程
|
||||
|
||||
## 10. 每一步怎么走
|
||||
|
||||
### 第 1 步:盘点现状
|
||||
|
||||
盘 4 张表:
|
||||
|
||||
1. 我们现在已有的功能
|
||||
2. 我们已有的工程资产
|
||||
3. 我们已有的工具
|
||||
4. 我们缺少的能力和基础设施
|
||||
|
||||
### 第 2 步:确认产品边界
|
||||
|
||||
确认:
|
||||
|
||||
- SaaS 给谁用
|
||||
- 单人还是团队
|
||||
- 多租户是否需要
|
||||
- 一期和后续范围怎么划分
|
||||
|
||||
### 第 3 步:确认核心对象
|
||||
|
||||
确认是否保留和如何设计:
|
||||
|
||||
- 用户
|
||||
- 团队 / 工作区
|
||||
- 项目
|
||||
- 素材库
|
||||
- 配音库
|
||||
- 生成任务
|
||||
- 成片
|
||||
- 任务节点 / worker
|
||||
- 版本 / 发布对象
|
||||
|
||||
### 第 4 步:确认核心流程
|
||||
|
||||
要把这些完整走通:
|
||||
|
||||
- 上传素材
|
||||
- 素材分类
|
||||
- 配音管理
|
||||
- 生成任务
|
||||
- 批量生成
|
||||
- 结果查看
|
||||
- 下载/发布
|
||||
- 后台任务监控
|
||||
|
||||
### 第 5 步:确认技术栈
|
||||
|
||||
需要明确:
|
||||
|
||||
- 前端
|
||||
- 后端
|
||||
- 数据库
|
||||
- 队列
|
||||
- 对象存储
|
||||
- 鉴权
|
||||
- 部署方式
|
||||
- 监控日志
|
||||
|
||||
### 第 6 步:确认制度
|
||||
|
||||
包括:
|
||||
|
||||
- 目录结构
|
||||
- 代码规范
|
||||
- 接口规范
|
||||
- 数据模型规范
|
||||
- 提交流程
|
||||
- 发布流程
|
||||
- AI 协作流程
|
||||
|
||||
### 第 7 步:定义 MVP
|
||||
|
||||
列出:
|
||||
|
||||
- 一期必须做什么
|
||||
- 可以延后什么
|
||||
- 明确不做什么
|
||||
|
||||
### 第 8 步:再开始搭骨架
|
||||
|
||||
包括:
|
||||
|
||||
- 新项目目录
|
||||
- 前后端工程骨架
|
||||
- 数据库迁移体系
|
||||
- CI/CD 初版
|
||||
- 日志与配置体系
|
||||
|
||||
### 第 9 步:模块化开发
|
||||
|
||||
严格按架构和边界执行。
|
||||
|
||||
### 第 10 步:Review / QA / 上线
|
||||
|
||||
必须经过:
|
||||
|
||||
- Code Review
|
||||
- 自动化测试
|
||||
- 回归验证
|
||||
- 预发布验证
|
||||
- 正式发布与监控
|
||||
|
||||
## 11. 制度怎么落实
|
||||
|
||||
### 制度 1:先文档后开发
|
||||
|
||||
没有边界、架构、一期范围确认,不开工。
|
||||
|
||||
### 制度 2:单一真相源
|
||||
|
||||
必须建立统一文档源:
|
||||
|
||||
- PRD
|
||||
- 架构文档
|
||||
- 数据模型
|
||||
- API 文档
|
||||
- 里程碑
|
||||
|
||||
### 制度 3:架构 owner 唯一
|
||||
|
||||
- 架构由小虾负责收口
|
||||
- 你负责最终确认
|
||||
- 其他 AI 不得随意改方向
|
||||
|
||||
### 制度 4:AI 不直接上线
|
||||
|
||||
AI 产出必须经过:
|
||||
|
||||
- Review
|
||||
- 测试
|
||||
- CI
|
||||
- 验收
|
||||
|
||||
### 制度 5:禁止临时修补进入主线
|
||||
|
||||
- 所有变更必须走既定流程
|
||||
- 不允许口头规则替代正式规则
|
||||
|
||||
### 制度 6:一次只处理一个阶段问题
|
||||
|
||||
- 先定需求
|
||||
- 再定架构
|
||||
- 再定页面
|
||||
- 再开发
|
||||
- 再测试
|
||||
|
||||
### 制度 7:发布必须可回滚
|
||||
|
||||
- 每次上线必须可回滚
|
||||
- 必须有监控与告警
|
||||
|
||||
### 制度 8:新旧彻底隔离
|
||||
|
||||
- 旧项目只参考
|
||||
- 新项目主线独立开发
|
||||
|
||||
## 12. 当前最重要的下一步
|
||||
|
||||
不是写 SaaS 代码,而是先盘点:
|
||||
|
||||
### 第一张表:我们现在有什么
|
||||
|
||||
需要逐项盘清:
|
||||
|
||||
- 现有功能
|
||||
- 现有工具
|
||||
- 现有基础设施
|
||||
- 可复用的业务规则
|
||||
- 缺少的能力
|
||||
|
||||
只有这一步做清楚,后面技术选型、架构设计、AI 分工、项目骨架才不会跑偏。
|
||||
|
||||
## 13. 当前状态结论
|
||||
|
||||
我们现在已经统一了方向:
|
||||
|
||||
- 旧桌面版不再作为未来主线
|
||||
- 新 SaaS 项目全新开始
|
||||
- 必须正规化开发
|
||||
- 必须先盘点、先定边界、先定制度
|
||||
- AI 工具采用现成能力 + 明确分工,不自研底座
|
||||
- 现有 Gitea / CI / 巡检机器人保留,但未来服务于新系统
|
||||
|
||||
下一步进入:
|
||||
|
||||
**《SaaS 重建盘点总表》第一阶段:我们现在有什么、还缺什么。**
|
||||
|
||||
配套文档:
|
||||
|
||||
- `docs/saas-rebuild-inventory.md`
|
||||
- `docs/ai-team-tooling-checklist.md`
|
||||
- `docs/infrastructure-keep-rebuild-matrix.md`
|
||||
- `docs/saas-core-objects.md`
|
||||
- `docs/saas-core-flows.md`
|
||||
- `docs/saas-tech-stack-options.md`
|
||||
- `docs/saas-mvp-scope.md`
|
||||
- `docs/saas-project-structure-spec.md`
|
||||
- `docs/saas-development-workflow.md`
|
||||
@@ -0,0 +1,274 @@
|
||||
# SaaS 技术栈选型表(第一版)
|
||||
|
||||
本文档用于明确新 SaaS 项目应该采用什么技术栈,以及为什么这样选。
|
||||
|
||||
当前阶段的原则不是追求“最新最炫”,而是追求:
|
||||
|
||||
- 稳定
|
||||
- 清晰
|
||||
- 好维护
|
||||
- 适合两人 + AI 协作
|
||||
- 便于长期迭代
|
||||
|
||||
---
|
||||
|
||||
## 1. 选型原则
|
||||
|
||||
### 必须满足
|
||||
|
||||
- 简单清晰
|
||||
- 社区成熟
|
||||
- 适合视频/任务型系统
|
||||
- 适合长期演进
|
||||
- 对 AI 编码工具友好
|
||||
- 前后端边界明确
|
||||
|
||||
### 尽量避免
|
||||
|
||||
- 过重的企业级复杂框架组合
|
||||
- 需要大量人力维护的微服务架构
|
||||
- 前期就引入过多中间件
|
||||
- 因“先进”而牺牲可维护性
|
||||
|
||||
---
|
||||
|
||||
## 2. 前端建议
|
||||
|
||||
### 推荐方向
|
||||
|
||||
- **React + TypeScript + Next.js(管理台 / Web 前端)**
|
||||
|
||||
### 原因
|
||||
|
||||
- 生态成熟
|
||||
- AI 工具支持好
|
||||
- 组件化能力强
|
||||
- 适合后台系统和 SaaS 管理界面
|
||||
- 后续做 SSR / 管理台 / 仪表盘都顺手
|
||||
|
||||
### UI 组件建议
|
||||
|
||||
- `shadcn/ui` 或同类现代组件体系
|
||||
- 配合 Tailwind CSS
|
||||
|
||||
### 结论建议
|
||||
|
||||
前端推荐:
|
||||
|
||||
- `Next.js + TypeScript + Tailwind + shadcn/ui`
|
||||
|
||||
---
|
||||
|
||||
## 3. 后端建议
|
||||
|
||||
### 推荐方向
|
||||
|
||||
- **Python + FastAPI**
|
||||
|
||||
### 原因
|
||||
|
||||
- 和现有业务逻辑语言认知一致
|
||||
- 适合 AI 工具协作
|
||||
- 非常适合 API + 后台任务系统
|
||||
- 类型标注、文档、异步支持都成熟
|
||||
- 对视频处理、脚本、任务调度的生态友好
|
||||
|
||||
### 不建议前期改成 Node 作为主后端的原因
|
||||
|
||||
- 现有业务规则和视频处理经验都更接近 Python 生态
|
||||
- 后续 worker、ffmpeg、诊断逻辑、脚本集成都更自然
|
||||
|
||||
### 结论建议
|
||||
|
||||
后端推荐:
|
||||
|
||||
- `FastAPI + Python`
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据库建议
|
||||
|
||||
### 推荐方向
|
||||
|
||||
- **PostgreSQL**
|
||||
|
||||
### 原因
|
||||
|
||||
- 稳定成熟
|
||||
- 非常适合 SaaS 主业务数据
|
||||
- 对 JSON 字段、索引、事务支持好
|
||||
- 生态完整
|
||||
|
||||
### 结论建议
|
||||
|
||||
主数据库:
|
||||
|
||||
- `PostgreSQL`
|
||||
|
||||
---
|
||||
|
||||
## 5. 异步任务 / 队列建议
|
||||
|
||||
### 推荐方向
|
||||
|
||||
- **Celery + Redis**
|
||||
|
||||
### 原因
|
||||
|
||||
- 视频生成、素材分类、同步、诊断都属于典型长任务
|
||||
- 需要 worker、状态、重试、任务分离
|
||||
- Python 生态成熟
|
||||
|
||||
### 可替代方向
|
||||
|
||||
- RQ / Dramatiq / 自建轻量任务层
|
||||
|
||||
### 当前建议
|
||||
|
||||
如果追求成熟和可扩展,优先:
|
||||
|
||||
- `Celery + Redis`
|
||||
|
||||
---
|
||||
|
||||
## 6. 对象存储建议
|
||||
|
||||
### 推荐方向
|
||||
|
||||
- **S3 兼容对象存储 / OSS**
|
||||
|
||||
### 原因
|
||||
|
||||
- SaaS 素材和结果文件不适合长期直接绑本地磁盘
|
||||
- 对象存储天然适合大文件、归档、分发
|
||||
- 便于后续多 worker / 多环境扩展
|
||||
|
||||
### 当前建议
|
||||
|
||||
抽象成统一存储接口,底层可接:
|
||||
|
||||
- 阿里云 OSS
|
||||
- MinIO
|
||||
- S3 兼容存储
|
||||
|
||||
---
|
||||
|
||||
## 7. 鉴权建议
|
||||
|
||||
### 推荐方向
|
||||
|
||||
- **JWT + Session/Refresh Token 组合**
|
||||
|
||||
### 原因
|
||||
|
||||
- 适合 Web SaaS
|
||||
- 前后端分离支持好
|
||||
- 后续权限系统扩展方便
|
||||
|
||||
### 当前建议
|
||||
|
||||
一期先做:
|
||||
|
||||
- 登录
|
||||
- 用户身份校验
|
||||
- 工作空间边界
|
||||
|
||||
高级权限后续再扩。
|
||||
|
||||
---
|
||||
|
||||
## 8. 部署建议
|
||||
|
||||
### 推荐方向
|
||||
|
||||
- **Docker Compose 起步,后续再升级**
|
||||
|
||||
### 原因
|
||||
|
||||
- 我们当前团队规模不适合一开始上 Kubernetes
|
||||
- Compose 足够支撑首版和测试环境
|
||||
- 后续如果规模上来,再考虑更复杂调度
|
||||
|
||||
### 初期部署对象
|
||||
|
||||
- Web 前端
|
||||
- FastAPI 后端
|
||||
- PostgreSQL
|
||||
- Redis
|
||||
- Worker
|
||||
- Nginx / 反向代理
|
||||
|
||||
---
|
||||
|
||||
## 9. 日志与监控建议
|
||||
|
||||
### 一期最小配置
|
||||
|
||||
- 应用日志
|
||||
- 任务日志
|
||||
- 错误追踪
|
||||
- 健康检查
|
||||
- 巡检机器人
|
||||
|
||||
### 后续建议
|
||||
|
||||
- Sentry(错误追踪)
|
||||
- Prometheus + Grafana(指标)
|
||||
- Loki / ELK(日志检索,按需要)
|
||||
|
||||
---
|
||||
|
||||
## 10. 开发规范建议
|
||||
|
||||
### 前端
|
||||
|
||||
- TypeScript 强制
|
||||
- ESLint / Prettier
|
||||
- 组件边界清晰
|
||||
- 页面逻辑与数据逻辑分离
|
||||
|
||||
### 后端
|
||||
|
||||
- 类型标注
|
||||
- Pydantic / schema 明确
|
||||
- 分层结构清晰
|
||||
- 用例层和适配器层分开
|
||||
|
||||
### 测试
|
||||
|
||||
- 单元测试
|
||||
- API 测试
|
||||
- 任务流关键路径测试
|
||||
- CI 自动执行
|
||||
|
||||
---
|
||||
|
||||
## 11. 当前推荐组合(第一版)
|
||||
|
||||
### 推荐首选方案
|
||||
|
||||
- **前端**:Next.js + TypeScript + Tailwind + shadcn/ui
|
||||
- **后端**:FastAPI + Python
|
||||
- **数据库**:PostgreSQL
|
||||
- **队列**:Redis + Celery
|
||||
- **存储**:S3/OSS 兼容对象存储
|
||||
- **部署**:Docker Compose
|
||||
- **监控**:巡检机器人 + 基础日志 + 后续接 Sentry
|
||||
|
||||
这是我现在最推荐的首版 SaaS 技术栈。
|
||||
|
||||
---
|
||||
|
||||
## 12. 当前阶段结论
|
||||
|
||||
对于我们这种“两个人 + AI 工具”的实际情况,最重要的不是用最潮的栈,而是:
|
||||
|
||||
- 选成熟的
|
||||
- 选 AI 容易协作的
|
||||
- 选长期不容易后悔的
|
||||
- 选和视频任务系统天然契合的
|
||||
|
||||
当前推荐方向已经足够支撑下一步:
|
||||
|
||||
- 正式定义 SaaS 项目目录结构
|
||||
- 开始搭第一版工程骨架
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
ENV PYTHONPATH=/app
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY apps/api /app/apps/api
|
||||
COPY packages /app/packages
|
||||
WORKDIR /app/apps/api
|
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -0,0 +1,55 @@
|
||||
version: '3.9'
|
||||
services:
|
||||
web:
|
||||
image: node:20
|
||||
working_dir: /app/apps/web
|
||||
command: sh -c "npm install && npm run dev"
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ../..:/app
|
||||
depends_on:
|
||||
- api
|
||||
|
||||
api:
|
||||
image: xiaoxia-saas-api:dev
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: infra/docker/api.Dockerfile
|
||||
env_file:
|
||||
- ../../.env.example
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
|
||||
worker:
|
||||
image: xiaoxia-saas-worker:dev
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: infra/docker/worker.Dockerfile
|
||||
env_file:
|
||||
- ../../.env.example
|
||||
depends_on:
|
||||
- redis
|
||||
- postgres
|
||||
|
||||
postgres:
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_DB: xiaoxia_saas
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:7
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
ENV PYTHONPATH=/app
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY apps/worker /app/apps/worker
|
||||
COPY packages /app/packages
|
||||
WORKDIR /app/apps/worker
|
||||
CMD ["celery", "-A", "worker_app.celery_app.celery_app", "worker", "--loglevel=info"]
|
||||
@@ -0,0 +1,12 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://api:8000/;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 200 'xiaoxia-saas nginx scaffold';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# Bootstrap notes
|
||||
|
||||
Current scaffold status:
|
||||
|
||||
- `apps/web`: Next.js-style skeleton files created
|
||||
- `apps/api`: FastAPI package layout created (`app/core`, `app/api`, `app/schemas`)
|
||||
- `apps/worker`: Celery worker package layout created (`worker_app/core`, `worker_app/tasks`)
|
||||
- `infra/docker/compose.yml`: local dev compose with web/api/worker/postgres/redis
|
||||
|
||||
Next actions:
|
||||
|
||||
1. install frontend dependencies
|
||||
2. add first real domain models in `packages/domain`
|
||||
3. add first application use cases in `packages/application`
|
||||
4. connect API routes to use cases
|
||||
5. add CI workflow
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "xiaoxia-saas",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/web"
|
||||
],
|
||||
"scripts": {
|
||||
"dev:web": "npm --workspace apps/web run dev",
|
||||
"build:web": "npm --workspace apps/web run build",
|
||||
"lint:web": "npm --workspace apps/web run lint",
|
||||
"lint": "npm run lint:web",
|
||||
"test": "echo TODO: add frontend/backend tests"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
"""Packages root."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Adapters package for external implementations."""
|
||||
@@ -0,0 +1,11 @@
|
||||
from .asset_library_repository import InMemoryAssetLibraryRepository
|
||||
from .asset_repository import InMemoryAssetRepository
|
||||
from .ingest_job_repository import InMemoryIngestJobRepository
|
||||
from .project_repository import InMemoryProjectRepository
|
||||
|
||||
__all__ = [
|
||||
"InMemoryAssetLibraryRepository",
|
||||
"InMemoryAssetRepository",
|
||||
"InMemoryIngestJobRepository",
|
||||
"InMemoryProjectRepository",
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from packages.domain import AssetLibrary, AssetLibraryKind
|
||||
|
||||
|
||||
class InMemoryAssetLibraryRepository:
|
||||
def __init__(self):
|
||||
self._items: dict[str, list[AssetLibrary]] = {}
|
||||
|
||||
def list_by_project(self, project_id: str, kind: AssetLibraryKind | None = None) -> list[AssetLibrary]:
|
||||
items = list(self._items.get(project_id, []))
|
||||
if kind is None:
|
||||
return items
|
||||
return [item for item in items if item.kind == kind]
|
||||
|
||||
def create(self, library: AssetLibrary) -> AssetLibrary:
|
||||
items = self._items.setdefault(library.project_id, [])
|
||||
items.append(library)
|
||||
return library
|
||||
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from packages.domain import Asset
|
||||
|
||||
|
||||
class InMemoryAssetRepository:
|
||||
def __init__(self):
|
||||
self._items: dict[str, list[Asset]] = {}
|
||||
|
||||
def list_by_library(self, library_id: str) -> list[Asset]:
|
||||
return list(self._items.get(library_id, []))
|
||||
|
||||
def create(self, asset: Asset) -> Asset:
|
||||
items = self._items.setdefault(asset.library_id, [])
|
||||
items.append(asset)
|
||||
return asset
|
||||
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from packages.domain import IngestJob
|
||||
|
||||
|
||||
class InMemoryIngestJobRepository:
|
||||
def __init__(self):
|
||||
self._items: dict[str, IngestJob] = {}
|
||||
|
||||
def create(self, job: IngestJob) -> IngestJob:
|
||||
self._items[job.id] = job
|
||||
return job
|
||||
|
||||
def get(self, job_id: str) -> IngestJob | None:
|
||||
return self._items.get(job_id)
|
||||
|
||||
def update(self, job: IngestJob) -> IngestJob:
|
||||
self._items[job.id] = job
|
||||
return job
|
||||
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from packages.domain import Project
|
||||
|
||||
|
||||
class InMemoryProjectRepository:
|
||||
def __init__(self):
|
||||
self._items: dict[str, list[Project]] = {}
|
||||
|
||||
def list_by_workspace(self, workspace_id: str) -> list[Project]:
|
||||
return list(self._items.get(workspace_id, []))
|
||||
|
||||
def create(self, project: Project) -> Project:
|
||||
items = self._items.setdefault(project.workspace_id, [])
|
||||
items.append(project)
|
||||
return project
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Application use cases package."""
|
||||
|
||||
from .asset_libraries import CreateAssetLibraryCommand, CreateAssetLibraryUseCase, ListAssetLibrariesUseCase
|
||||
from .assets import CreateAssetCommand, CreateAssetUseCase, ListAssetsUseCase
|
||||
from .ingest_jobs import SubmitIngestJobCommand, SubmitIngestJobUseCase
|
||||
from .projects import CreateProjectCommand, CreateProjectUseCase, ListProjectsUseCase
|
||||
|
||||
__all__ = [
|
||||
"CreateAssetCommand",
|
||||
"CreateAssetLibraryCommand",
|
||||
"CreateAssetLibraryUseCase",
|
||||
"CreateAssetUseCase",
|
||||
"CreateProjectCommand",
|
||||
"CreateProjectUseCase",
|
||||
"ListAssetLibrariesUseCase",
|
||||
"ListAssetsUseCase",
|
||||
"ListProjectsUseCase",
|
||||
"SubmitIngestJobCommand",
|
||||
"SubmitIngestJobUseCase",
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from packages.domain import AssetLibrary, AssetLibraryKind
|
||||
from packages.ports.asset_library_repository import AssetLibraryRepository
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CreateAssetLibraryCommand:
|
||||
workspace_id: str
|
||||
project_id: str
|
||||
name: str
|
||||
kind: AssetLibraryKind
|
||||
|
||||
|
||||
class ListAssetLibrariesUseCase:
|
||||
def __init__(self, asset_library_repository: AssetLibraryRepository):
|
||||
self.asset_library_repository = asset_library_repository
|
||||
|
||||
def execute(self, project_id: str, kind: AssetLibraryKind | None = None) -> list[AssetLibrary]:
|
||||
if not project_id.strip():
|
||||
raise ValueError("project_id 不能为空")
|
||||
return self.asset_library_repository.list_by_project(project_id.strip(), kind=kind)
|
||||
|
||||
|
||||
class CreateAssetLibraryUseCase:
|
||||
def __init__(self, asset_library_repository: AssetLibraryRepository):
|
||||
self.asset_library_repository = asset_library_repository
|
||||
|
||||
def execute(self, command: CreateAssetLibraryCommand) -> AssetLibrary:
|
||||
library = AssetLibrary.create(
|
||||
workspace_id=command.workspace_id,
|
||||
project_id=command.project_id,
|
||||
name=command.name,
|
||||
kind=command.kind,
|
||||
)
|
||||
return self.asset_library_repository.create(library)
|
||||
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from packages.domain import Asset
|
||||
from packages.ports.asset_repository import AssetRepository
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CreateAssetCommand:
|
||||
workspace_id: str
|
||||
project_id: str
|
||||
library_id: str
|
||||
name: str
|
||||
storage_key: str
|
||||
mime_type: str
|
||||
metadata: dict[str, object] | None = None
|
||||
|
||||
|
||||
class ListAssetsUseCase:
|
||||
def __init__(self, asset_repository: AssetRepository):
|
||||
self.asset_repository = asset_repository
|
||||
|
||||
def execute(self, library_id: str) -> list[Asset]:
|
||||
if not library_id.strip():
|
||||
raise ValueError("library_id 不能为空")
|
||||
return self.asset_repository.list_by_library(library_id.strip())
|
||||
|
||||
|
||||
class CreateAssetUseCase:
|
||||
def __init__(self, asset_repository: AssetRepository):
|
||||
self.asset_repository = asset_repository
|
||||
|
||||
def execute(self, command: CreateAssetCommand) -> Asset:
|
||||
asset = Asset.create(
|
||||
workspace_id=command.workspace_id,
|
||||
project_id=command.project_id,
|
||||
library_id=command.library_id,
|
||||
name=command.name,
|
||||
storage_key=command.storage_key,
|
||||
mime_type=command.mime_type,
|
||||
metadata=command.metadata,
|
||||
)
|
||||
return self.asset_repository.create(asset)
|
||||
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from packages.domain import IngestJob
|
||||
from packages.ports.ingest_job_repository import IngestJobRepository
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SubmitIngestJobCommand:
|
||||
workspace_id: str
|
||||
project_id: str
|
||||
library_id: str
|
||||
storage_key: str
|
||||
|
||||
|
||||
class SubmitIngestJobUseCase:
|
||||
def __init__(self, ingest_job_repository: IngestJobRepository):
|
||||
self.ingest_job_repository = ingest_job_repository
|
||||
|
||||
def execute(self, command: SubmitIngestJobCommand) -> IngestJob:
|
||||
job = IngestJob.create(
|
||||
workspace_id=command.workspace_id,
|
||||
project_id=command.project_id,
|
||||
library_id=command.library_id,
|
||||
storage_key=command.storage_key,
|
||||
)
|
||||
return self.ingest_job_repository.create(job)
|
||||
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from packages.domain import Project
|
||||
from packages.ports import ProjectRepository
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CreateProjectCommand:
|
||||
workspace_id: str
|
||||
name: str
|
||||
description: str = ""
|
||||
|
||||
|
||||
class ListProjectsUseCase:
|
||||
def __init__(self, project_repository: ProjectRepository):
|
||||
self.project_repository = project_repository
|
||||
|
||||
def execute(self, workspace_id: str) -> list[Project]:
|
||||
if not workspace_id.strip():
|
||||
raise ValueError("workspace_id 不能为空")
|
||||
return self.project_repository.list_by_workspace(workspace_id.strip())
|
||||
|
||||
|
||||
class CreateProjectUseCase:
|
||||
def __init__(self, project_repository: ProjectRepository):
|
||||
self.project_repository = project_repository
|
||||
|
||||
def execute(self, command: CreateProjectCommand) -> Project:
|
||||
project = Project.create(
|
||||
workspace_id=command.workspace_id,
|
||||
name=command.name,
|
||||
description=command.description,
|
||||
)
|
||||
return self.project_repository.create(project)
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Domain package for core business entities and rules."""
|
||||
|
||||
from .entities import Asset, AssetLibrary, AssetLibraryKind, IngestJob, IngestJobStatus, Project, User, Workspace
|
||||
|
||||
__all__ = [
|
||||
"Asset",
|
||||
"AssetLibrary",
|
||||
"AssetLibraryKind",
|
||||
"IngestJob",
|
||||
"IngestJobStatus",
|
||||
"Project",
|
||||
"User",
|
||||
"Workspace",
|
||||
]
|
||||
@@ -0,0 +1,165 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class AssetLibraryKind(StrEnum):
|
||||
VIDEO = "video"
|
||||
VOICE = "voice"
|
||||
|
||||
|
||||
class IngestJobStatus(StrEnum):
|
||||
PENDING = "pending"
|
||||
PROCESSING = "processing"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class User:
|
||||
id: str
|
||||
email: str
|
||||
display_name: str
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Workspace:
|
||||
id: str
|
||||
name: str
|
||||
owner_user_id: str
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Project:
|
||||
id: str
|
||||
workspace_id: str
|
||||
name: str
|
||||
description: str = ""
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
@classmethod
|
||||
def create(cls, workspace_id: str, name: str, description: str = "") -> "Project":
|
||||
clean_name = name.strip()
|
||||
if not clean_name:
|
||||
raise ValueError("项目名称不能为空")
|
||||
return cls(
|
||||
id=uuid4().hex,
|
||||
workspace_id=workspace_id,
|
||||
name=clean_name,
|
||||
description=description.strip(),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class AssetLibrary:
|
||||
id: str
|
||||
workspace_id: str
|
||||
project_id: str
|
||||
name: str
|
||||
kind: AssetLibraryKind
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
workspace_id: str,
|
||||
project_id: str,
|
||||
name: str,
|
||||
kind: AssetLibraryKind,
|
||||
) -> "AssetLibrary":
|
||||
clean_name = name.strip()
|
||||
if not clean_name:
|
||||
raise ValueError("素材库名称不能为空")
|
||||
return cls(
|
||||
id=uuid4().hex,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
name=clean_name,
|
||||
kind=kind,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Asset:
|
||||
id: str
|
||||
workspace_id: str
|
||||
project_id: str
|
||||
library_id: str
|
||||
name: str
|
||||
storage_key: str
|
||||
mime_type: str
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
workspace_id: str,
|
||||
project_id: str,
|
||||
library_id: str,
|
||||
name: str,
|
||||
storage_key: str,
|
||||
mime_type: str,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> "Asset":
|
||||
clean_name = name.strip()
|
||||
if not clean_name:
|
||||
raise ValueError("素材名称不能为空")
|
||||
if not storage_key.strip():
|
||||
raise ValueError("storage_key 不能为空")
|
||||
if not mime_type.strip():
|
||||
raise ValueError("mime_type 不能为空")
|
||||
return cls(
|
||||
id=uuid4().hex,
|
||||
workspace_id=workspace_id,
|
||||
project_id=project_id,
|
||||
library_id=library_id,
|
||||
name=clean_name,
|
||||
storage_key=storage_key.strip(),
|
||||
mime_type=mime_type.strip(),
|
||||
metadata=metadata or {},
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class IngestJob:
|
||||
id: str
|
||||
workspace_id: str
|
||||
project_id: str
|
||||
library_id: str
|
||||
storage_key: str
|
||||
status: IngestJobStatus = IngestJobStatus.PENDING
|
||||
error_message: str = ""
|
||||
result_asset_id: str = ""
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
workspace_id: str,
|
||||
project_id: str,
|
||||
library_id: str,
|
||||
storage_key: str,
|
||||
) -> "IngestJob":
|
||||
if not workspace_id.strip():
|
||||
raise ValueError("workspace_id 不能为空")
|
||||
if not project_id.strip():
|
||||
raise ValueError("project_id 不能为空")
|
||||
if not library_id.strip():
|
||||
raise ValueError("library_id 不能为空")
|
||||
if not storage_key.strip():
|
||||
raise ValueError("storage_key 不能为空")
|
||||
return cls(
|
||||
id=uuid4().hex,
|
||||
workspace_id=workspace_id.strip(),
|
||||
project_id=project_id.strip(),
|
||||
library_id=library_id.strip(),
|
||||
storage_key=storage_key.strip(),
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Ports package for infrastructure-agnostic interfaces."""
|
||||
|
||||
from .asset_library_repository import AssetLibraryRepository
|
||||
from .asset_repository import AssetRepository
|
||||
from .ingest_job_repository import IngestJobRepository
|
||||
from .project_repository import ProjectRepository
|
||||
|
||||
__all__ = [
|
||||
"AssetLibraryRepository",
|
||||
"AssetRepository",
|
||||
"IngestJobRepository",
|
||||
"ProjectRepository",
|
||||
]
|
||||
@@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
from packages.domain import AssetLibrary, AssetLibraryKind
|
||||
|
||||
|
||||
class AssetLibraryRepository(Protocol):
|
||||
def list_by_project(self, project_id: str, kind: AssetLibraryKind | None = None) -> list[AssetLibrary]:
|
||||
"""List asset libraries for a project."""
|
||||
|
||||
def create(self, library: AssetLibrary) -> AssetLibrary:
|
||||
"""Persist an asset library and return it."""
|
||||
@@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
from packages.domain import Asset
|
||||
|
||||
|
||||
class AssetRepository(Protocol):
|
||||
def list_by_library(self, library_id: str) -> list[Asset]:
|
||||
"""List assets for a library."""
|
||||
|
||||
def create(self, asset: Asset) -> Asset:
|
||||
"""Persist an asset and return it."""
|
||||
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
from packages.domain import IngestJob
|
||||
|
||||
|
||||
class IngestJobRepository(Protocol):
|
||||
def create(self, job: IngestJob) -> IngestJob:
|
||||
"""Persist an ingest job and return it."""
|
||||
|
||||
def get(self, job_id: str) -> IngestJob | None:
|
||||
"""Retrieve an ingest job by ID."""
|
||||
|
||||
def update(self, job: IngestJob) -> IngestJob:
|
||||
"""Update an ingest job and return it."""
|
||||
@@ -0,0 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
from packages.domain import Project
|
||||
|
||||
|
||||
class ProjectRepository(Protocol):
|
||||
def list_by_workspace(self, workspace_id: str) -> list[Project]:
|
||||
"""List projects for a workspace."""
|
||||
|
||||
def create(self, project: Project) -> Project:
|
||||
"""Persist a project and return it."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Shared package for cross-cutting, non-business-specific utilities."""
|
||||
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
pythonpath = .
|
||||
testpaths = tests
|
||||
@@ -0,0 +1,10 @@
|
||||
# Python runtime dependencies for API and worker will be managed here.
|
||||
# Initial scaffold only; versions to be pinned after stack confirmation.
|
||||
fastapi
|
||||
uvicorn
|
||||
pydantic
|
||||
celery
|
||||
redis
|
||||
sqlalchemy
|
||||
alembic
|
||||
psycopg[binary]
|
||||
@@ -0,0 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
@@ -0,0 +1 @@
|
||||
# End-to-end tests placeholder
|
||||
@@ -0,0 +1 @@
|
||||
# Integration tests placeholder
|
||||
@@ -0,0 +1,102 @@
|
||||
from packages.application import (
|
||||
CreateAssetCommand,
|
||||
CreateAssetLibraryCommand,
|
||||
CreateAssetLibraryUseCase,
|
||||
CreateAssetUseCase,
|
||||
CreateProjectCommand,
|
||||
CreateProjectUseCase,
|
||||
ListAssetLibrariesUseCase,
|
||||
ListAssetsUseCase,
|
||||
ListProjectsUseCase,
|
||||
SubmitIngestJobCommand,
|
||||
SubmitIngestJobUseCase,
|
||||
)
|
||||
from packages.adapters.in_memory import (
|
||||
InMemoryAssetLibraryRepository,
|
||||
InMemoryAssetRepository,
|
||||
InMemoryIngestJobRepository,
|
||||
InMemoryProjectRepository,
|
||||
)
|
||||
from packages.domain import AssetLibraryKind, IngestJobStatus
|
||||
|
||||
|
||||
def test_create_and_list_projects():
|
||||
repository = InMemoryProjectRepository()
|
||||
create_use_case = CreateProjectUseCase(repository)
|
||||
list_use_case = ListProjectsUseCase(repository)
|
||||
|
||||
project = create_use_case.execute(CreateProjectCommand(workspace_id="ws-1", name=" Demo Project "))
|
||||
assert project.name == "Demo Project"
|
||||
assert project.workspace_id == "ws-1"
|
||||
|
||||
items = list_use_case.execute("ws-1")
|
||||
assert len(items) == 1
|
||||
assert items[0].id == project.id
|
||||
|
||||
|
||||
def test_create_and_list_asset_libraries():
|
||||
repository = InMemoryAssetLibraryRepository()
|
||||
create_use_case = CreateAssetLibraryUseCase(repository)
|
||||
list_use_case = ListAssetLibrariesUseCase(repository)
|
||||
|
||||
library = create_use_case.execute(
|
||||
CreateAssetLibraryCommand(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
name=" 素材库 A ",
|
||||
kind=AssetLibraryKind.VIDEO,
|
||||
)
|
||||
)
|
||||
assert library.name == "素材库 A"
|
||||
assert library.kind == AssetLibraryKind.VIDEO
|
||||
|
||||
items = list_use_case.execute("proj-1", kind=AssetLibraryKind.VIDEO)
|
||||
assert len(items) == 1
|
||||
assert items[0].id == library.id
|
||||
|
||||
|
||||
def test_create_and_list_assets():
|
||||
repository = InMemoryAssetRepository()
|
||||
create_use_case = CreateAssetUseCase(repository)
|
||||
list_use_case = ListAssetsUseCase(repository)
|
||||
|
||||
asset = create_use_case.execute(
|
||||
CreateAssetCommand(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
library_id="lib-1",
|
||||
name=" demo.mp4 ",
|
||||
storage_key="uploads/demo.mp4",
|
||||
mime_type="video/mp4",
|
||||
metadata={"duration": 12.5},
|
||||
)
|
||||
)
|
||||
assert asset.name == "demo.mp4"
|
||||
assert asset.storage_key == "uploads/demo.mp4"
|
||||
assert asset.metadata["duration"] == 12.5
|
||||
|
||||
items = list_use_case.execute("lib-1")
|
||||
assert len(items) == 1
|
||||
assert items[0].id == asset.id
|
||||
|
||||
|
||||
def test_submit_ingest_job():
|
||||
repository = InMemoryIngestJobRepository()
|
||||
use_case = SubmitIngestJobUseCase(repository)
|
||||
|
||||
job = use_case.execute(
|
||||
SubmitIngestJobCommand(
|
||||
workspace_id="ws-1",
|
||||
project_id="proj-1",
|
||||
library_id="lib-1",
|
||||
storage_key="uploads/raw-video.mp4",
|
||||
)
|
||||
)
|
||||
assert job.workspace_id == "ws-1"
|
||||
assert job.storage_key == "uploads/raw-video.mp4"
|
||||
assert job.status == IngestJobStatus.PENDING
|
||||
assert job.result_asset_id == ""
|
||||
|
||||
retrieved = repository.get(job.id)
|
||||
assert retrieved is not None
|
||||
assert retrieved.id == job.id
|
||||
Reference in New Issue
Block a user