882 lines
31 KiB
Markdown
882 lines
31 KiB
Markdown
# VideoGen 代码架构指南
|
||
|
||
> 本文档面向开发者或 AI 模型,帮助快速理解代码风格、架构约定和修改规则。
|
||
# AI 工作规则(最高优先级)
|
||
|
||
当你执行开发任务时:
|
||
|
||
1. 不要扫描整个 Repository。
|
||
2. 不要执行全量目录探索。
|
||
3. 仅阅读本任务涉及的目录。
|
||
4. 优先按照本文档中的"快速定位表"寻找文件。
|
||
5. 如果无法定位文件,再进行有限范围搜索。
|
||
6. 单次最多搜索一级目录。
|
||
7. 不允许重复搜索已经访问过的目录。
|
||
8. 阅读完目标文件后立即开始修改代码。
|
||
---
|
||
|
||
## 一、整体架构
|
||
|
||
```
|
||
video_item/
|
||
├── video-gen-api/ # 后端 (Python FastAPI)
|
||
├── video-gen-app/ # 前台/用户端 (React + Vite) ──┤
|
||
└── video-gen-admin/ # 后台管理端 (React + Vite) ──┘ 两者共用同一个后端 API
|
||
```
|
||
|
||
**核心原则:** 三个项目完全分离,前台和后台是两个独立的 SPA,部署在不同的子域名上,但调用同一套后端接口。
|
||
|
||
---
|
||
|
||
## 二、后端架构 (video-gen-api)
|
||
|
||
### 2.1 目录结构
|
||
|
||
```
|
||
app/
|
||
├── api/ # API 路由层(薄层,仅做参数解析和调用 service)
|
||
│ ├── v1/ # 前台 + 部分管理接口(FastAPI 意义上的 v1 版本)
|
||
│ │ ├── __init__.py # 聚合所有 v1 router
|
||
│ │ ├── auth.py
|
||
│ │ ├── projects.py
|
||
│ │ ├── generation.py
|
||
│ │ ├── admin.py # prefix="/admin"(管理接口混入 v1)
|
||
│ │ └── ...
|
||
│ └── admin/ # 额外的管理端细分路由
|
||
│ ├── __init__.py
|
||
│ ├── team.py
|
||
│ ├── home_material.py
|
||
│ └── ...
|
||
├── schemas/ # Pydantic 模型(请求入参 + 响应序列化)
|
||
│ ├── common.py # 公共基类:NaiveDatetime, PaginatedResponse
|
||
│ ├── generation.py
|
||
│ ├── auth.py
|
||
│ └── ...
|
||
├── models/ # SQLAlchemy ORM 模型(41 个)
|
||
│ ├── base.py # Base, TimestampMixin, SoftDeleteMixin
|
||
│ ├── user.py
|
||
│ └── ...
|
||
├── services/ # 业务逻辑层(核心业务写在这里)
|
||
│ ├── auth.py # JWT 认证、密码哈希
|
||
│ ├── credits.py # 积分扣减
|
||
│ ├── operation_log.py # 操作日志
|
||
│ └── ...
|
||
├── enums/ # 枚举定义(全部用 str, Enum)
|
||
│ ├── generation_status.py
|
||
│ ├── celery_queue.py
|
||
│ └── ...
|
||
├── tasks/ # Celery 异步任务
|
||
│ ├── celery_app.py # Celery 实例 + 配置
|
||
│ ├── cleanup.py
|
||
│ └── ...
|
||
├── middleware/ # FastAPI 中间件
|
||
│ ├── logging.py
|
||
│ ├── rate_limit.py
|
||
│ ├── request_encrypt.py
|
||
│ ├── anti_crawler.py
|
||
│ └── ...
|
||
├── utils/ # 通用工具
|
||
│ ├── id_gen.py # generate_id(), generate_order_no()
|
||
│ ├── exceptions.py # 自定义 HTTPException
|
||
│ └── security.py # AES 加密、HMAC
|
||
├── dependencies.py # FastAPI 依赖注入(get_db, get_current_user 等)
|
||
├── config.py # Pydantic Settings,读取 .env
|
||
└── main.py # FastAPI 应用入口、lifespan、种子数据
|
||
```
|
||
|
||
### 2.2 分层约定(重要)
|
||
|
||
请求处理严格遵循 **路由 → 服务 → 模型** 三层:
|
||
|
||
```
|
||
Route (api/) → Service (services/) → Model (models/)
|
||
解析参数 业务逻辑 数据库操作
|
||
调用 service 编排调用 纯 CRUD
|
||
返回序列化结果 不直接操作 ORM 无业务规则
|
||
```
|
||
|
||
**禁止:**
|
||
- 路由中直接写复杂 SQL/ORM 操作 → 应抽到 service
|
||
- Model 文件中写业务逻辑 → model 只定义字段和关系
|
||
- Service 中混入 HTTP 相关代码 → 不 import FastAPI 对象
|
||
|
||
### 2.3 SQLAlchemy 模型约定
|
||
|
||
使用 SQLAlchemy 2.0 风格的 `mapped_column`:
|
||
|
||
```python
|
||
# app/models/project.py — 标准模板
|
||
from sqlalchemy import ForeignKey, String
|
||
from sqlalchemy.orm import Mapped, mapped_column
|
||
from app.models.base import Base, TimestampMixin, SoftDeleteMixin
|
||
|
||
class Project(Base, TimestampMixin, SoftDeleteMixin):
|
||
__tablename__ = "projects"
|
||
|
||
id: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||
user_id: Mapped[str] = mapped_column(String(32), ForeignKey("users.id", ondelete="CASCADE"), index=True)
|
||
name: Mapped[str] = mapped_column(String(128))
|
||
industry: Mapped[str] = mapped_column(String(32))
|
||
```
|
||
|
||
**约定:**
|
||
- 主键统一用 `String(32)`,由 `generate_id()` 生成(13 位时间戳 hex + 6 位随机 hex)
|
||
- 继承 `TimestampMixin` 自动获得 `created_at` / `updated_at`
|
||
- 需要软删除的继承 `SoftDeleteMixin` 获得 `deleted_at`
|
||
- 外键统一加 `index=True`,`ondelete="CASCADE"` 按需
|
||
- 所有 `str` 类型指定长度:`String(32)` / `String(128)` / `String(255)` 等
|
||
- 时间统一用 `DateTime(timezone=True)`
|
||
- **新增 model 后**必须在 `app/models/__init__.py` 中 import
|
||
|
||
```python
|
||
# app/models/base.py — 公共 Mixin
|
||
class TimestampMixin:
|
||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||
|
||
class SoftDeleteMixin:
|
||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
|
||
```
|
||
|
||
### 2.4 Pydantic Schema 约定
|
||
|
||
```python
|
||
# app/schemas/project.py
|
||
from pydantic import BaseModel, Field
|
||
from app.schemas.common import NaiveDatetime
|
||
|
||
class ProjectCreate(BaseModel): # 请求入参(客户端 → 服务端)
|
||
name: str = Field(..., max_length=128)
|
||
industry: str = Field(..., max_length=64)
|
||
|
||
class ProjectOut(BaseModel): # 响应序列化(服务端 → 客户端)
|
||
id: str
|
||
name: str
|
||
industry: str
|
||
created_at: NaiveDatetime
|
||
updated_at: NaiveDatetime
|
||
|
||
model_config = {"from_attributes": True} # ← 必加!允许从 ORM 对象直接构造
|
||
```
|
||
|
||
**命名约定:**
|
||
- `XxxCreate` — POST 创建请求
|
||
- `XxxUpdate` — PUT 更新请求
|
||
- `XxxOut` / `XxxResponse` — 响应
|
||
- `XxxListOut` — 列表响应(含 items + total)
|
||
- `XxxRequest` — 通用请求
|
||
|
||
**公共工具:**
|
||
- `NaiveDatetime` / `NaiveDatetimeOptional` — 自动将带时区的 datetime 转为北京时间 naive datetime
|
||
- `PaginatedResponse` — 分页响应基类
|
||
|
||
### 2.5 API 路由约定
|
||
|
||
```python
|
||
# app/api/v1/projects.py — 标准模板
|
||
from fastapi import APIRouter, Depends, HTTPException, status
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.dependencies import get_db, get_current_user
|
||
from app.models.user import User
|
||
from app.models.project import Project
|
||
from app.schemas.project import ProjectCreate, ProjectOut
|
||
from app.utils.id_gen import generate_id
|
||
|
||
router = APIRouter(prefix="/projects", tags=["projects"]) # prefix 不含 /api
|
||
|
||
@router.get("", response_model=list[ProjectOut]) # GET 列表用 list[XxxOut]
|
||
async def list_projects(
|
||
current_user: User = Depends(get_current_user), # 鉴权依赖
|
||
db: AsyncSession = Depends(get_db), # 数据库 session
|
||
):
|
||
result = await db.execute(
|
||
select(Project).where(
|
||
Project.user_id == current_user.id,
|
||
Project.deleted_at.is_(None), # 软删除过滤
|
||
).order_by(Project.created_at.desc())
|
||
)
|
||
return result.scalars().all() # 直接返回 ORM 对象,Pydantic 自动序列化
|
||
|
||
@router.post("", response_model=ProjectOut)
|
||
async def create_project(
|
||
req: ProjectCreate, # 请求体自动校验
|
||
current_user: User = Depends(get_current_user),
|
||
db: AsyncSession = Depends(get_db),
|
||
):
|
||
project = Project(
|
||
id=generate_id(), # 主键手工生成
|
||
user_id=current_user.id,
|
||
name=req.name,
|
||
industry=req.industry,
|
||
)
|
||
db.add(project)
|
||
await db.flush() # flush 获取自增字段但不出事务
|
||
return project
|
||
```
|
||
|
||
**关键约定:**
|
||
- `prefix` 不含 `/api`(`/api` 在 `main.py` 的 `include_router` 统一加)
|
||
- 受保护接口用 `Depends(get_current_user)` 获取当前用户
|
||
- 管理员接口用 `Depends(get_admin_user)`
|
||
- 可选登录用 `Depends(get_optional_current_user)`
|
||
- 查询过滤软删除: `.where(Model.deleted_at.is_(None))`
|
||
- 返回 ORM 对象时 Schema 必须有 `model_config = {"from_attributes": True}`
|
||
- 错误抛 `HTTPException` 或用自定义异常(见 `app/utils/exceptions.py`)
|
||
|
||
### 2.6 依赖注入约定
|
||
|
||
```python
|
||
# app/dependencies.py
|
||
get_db # 生成 AsyncSession,自动 commit/rollback
|
||
get_current_user # 验证 JWT → 返回 User,要求密码已设置
|
||
get_current_user_allow_password_pending # 验证 JWT → 返回 User(允许未设置密码)
|
||
get_optional_current_user # 验证 JWT → User | None(不强制登录)
|
||
get_admin_user # 验证 JWT + is_admin + user_type=="admin"
|
||
get_backend_user # 验证 JWT + user_type=="admin"(不要求 is_admin)
|
||
```
|
||
|
||
### 2.7 服务层约定
|
||
|
||
```python
|
||
# app/services/team_service.py — 标准模板
|
||
from __future__ import annotations # 前向引用必需的 import
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from app.models.team import Team
|
||
from app.schemas.team import TeamCreate, TeamUpdate
|
||
from app.utils.id_gen import generate_id
|
||
|
||
async def create_team(db: AsyncSession, req: TeamCreate) -> Team:
|
||
team = Team(
|
||
id=generate_id(),
|
||
name=req.name,
|
||
code=req.code,
|
||
description=req.description,
|
||
)
|
||
db.add(team)
|
||
await db.flush()
|
||
return team
|
||
```
|
||
|
||
**约定:**
|
||
- 函数签名统一 `(db: AsyncSession, ...) → Model | list[Model] | dict`
|
||
- 不处理 HTTP 异常,HTTP 相关处理留给 route 层
|
||
- 用 `from __future__ import annotations` 支持前向引用
|
||
- 纯查询类服务可以接收 `db` 作为第一个参数
|
||
|
||
### 2.8 枚举约定
|
||
|
||
```python
|
||
# app/enums/generation_status.py
|
||
from enum import Enum
|
||
|
||
class GenerationStatus(str, Enum): # 继承 str 使值可直接序列化
|
||
prompt_optimized = "prompt_optimized"
|
||
generating = "generating"
|
||
completed = "completed"
|
||
failed = "failed"
|
||
|
||
class GenerationType(str, Enum):
|
||
video = "video"
|
||
image = "image"
|
||
```
|
||
|
||
**约定:**
|
||
- 所有枚举继承 `str, Enum`(可序列化为 JSON)
|
||
- 枚举值用 snake_case
|
||
- 枚举文件统一放 `app/enums/`
|
||
- 在 schema 中引用枚举做校验: `gen_type: GenerationType = Field(...)`
|
||
|
||
### 2.9 异常处理约定
|
||
|
||
```python
|
||
# app/utils/exceptions.py — 预定义异常
|
||
InsufficientCreditsError # 402 积分不足
|
||
CaptchaFailedError # 400 验证码失败
|
||
RecordNotFoundError # 404 记录不存在
|
||
ProjectNotFoundError # 404 项目不存在
|
||
InvalidStatusError # 400 状态不允许操作
|
||
|
||
# 在 service 或 route 中抛出
|
||
raise InsufficientCreditsError()
|
||
```
|
||
|
||
### 2.10 认证约定
|
||
|
||
```python
|
||
# Token 创建
|
||
create_access_token(user_id, remember_me=False) → str # JWT 令牌
|
||
|
||
# Token 解码
|
||
decode_access_token(token) → str | None # 返回 user_id 或 None
|
||
|
||
# 密码
|
||
hash_password(plain) → str # bcrypt 哈希
|
||
verify_password(plain, hashed) → bool # bcrypt 校验
|
||
```
|
||
|
||
**鉴权流程:**
|
||
1. 前端 Authorization header: `Bearer <token>`
|
||
2. `HTTPBearer` 提取 token → `decode_access_token` 获取 `user_id`
|
||
3. 查 User 表验证 `is_active`
|
||
4. 检查 `user_must_set_password`(短信注册用户首次需设置密码)
|
||
|
||
### 2.11 数据库 Session 模式
|
||
|
||
```python
|
||
# dependencies.py 中的 get_db — 自动管理事务
|
||
async def get_db():
|
||
async with async_session() as session:
|
||
try:
|
||
yield session
|
||
await session.commit() # 成功自动提交
|
||
except Exception:
|
||
await session.rollback() # 异常自动回滚
|
||
raise
|
||
finally:
|
||
await session.close()
|
||
```
|
||
|
||
**注意:** 在 route 中如果只读查询不需要手动 commit(get_db 自动处理)。在 service 中做写入时需要调用者负责 commit,或由 get_db 处理。
|
||
|
||
### 2.12 中间件约定
|
||
|
||
中间件按**注册顺序**从外到内执行(`main.py`):
|
||
|
||
```python
|
||
application.add_middleware(RequestLoggingMiddleware) # 1. 请求日志
|
||
application.add_middleware(AntiCrawlerMiddleware) # 2. 反爬虫
|
||
application.add_middleware(RateLimitMiddleware) # 3. 限流 (Redis)
|
||
application.add_middleware(RequestEncryptMiddleware) # 4. 加密/解密
|
||
application.add_middleware(CORSMiddleware, ...) # 5. CORS
|
||
```
|
||
|
||
**加密中间件行为:**
|
||
- 请求有 `X-Encrypted: true` header → 解密请求体
|
||
- GET 请求无 body 但**响应仍加密**
|
||
- 支付回调路径白名单跳过加密 (`/payments/alipay/callback`, `/payments/wechat/callback`)
|
||
|
||
### 2.13 Celery 任务约定
|
||
|
||
```python
|
||
# app/tasks/cleanup.py — 标准模板
|
||
from app.tasks.celery_app import celery_app
|
||
|
||
@celery_app.task # 装饰器注册任务
|
||
def cleanup_expired_video_urls():
|
||
"""Run hourly. Clear expired video URL tokens."""
|
||
asyncio.run(_cleanup_urls()) # 同步任务内跑异步代码
|
||
|
||
async def _cleanup_urls(): # 实际逻辑写在 async 函数里
|
||
from app.models.base import async_session # 延迟导入避免循环
|
||
async with async_session() as db:
|
||
...
|
||
await db.commit()
|
||
```
|
||
|
||
**约定:**
|
||
- 任务装饰器: `@celery_app.task`
|
||
- 同步入口 → `asyncio.run()` 包异步逻辑
|
||
- 任务名自动生成: `文件名.函数名`(如 `app.tasks.cleanup.cleanup_expired_video_urls`)
|
||
- 队列路由在 `celery_app.py` 的 `task_routes` 配置
|
||
- 必需参数通过 `apply_async(args=[...], queue="xxx", priority=0)` 传递
|
||
|
||
#### Celery 队列清单(12 个)
|
||
|
||
| 队列 | 用途 | 推荐并发 |
|
||
|------|------|---------|
|
||
| `gen_chatapi_create` | ChatAPI 生成任务创建 | 2-4 |
|
||
| `gen_provider_poll` | 轮询火山引擎生成状态 | 2-4 |
|
||
| `gen_result_download` | 下载生成的视频/图片结果 | 2-4 |
|
||
| `gen_video_upscale_local` | 本地视频超分(FFmpeg) | 1-2 |
|
||
| `gen_video_upscale_remote` | 远程视频超分(火山 MediaKit) | 1-2 |
|
||
| `gen_recovery` | 容灾恢复任务 | 1 |
|
||
| `gen_private_portrait` | 真人素材认证与同步 | 1-2 |
|
||
| `gen_shot_analysis` | 拆镜分析 | 1-2 |
|
||
| `gen_shot_split` | 拆镜切片 | 1-2 |
|
||
| `gen_api_create` | API v3 视频任务创建 | 2-4 |
|
||
| `gen_api_poll` | API v3 视频状态轮询 | 2-4 |
|
||
| `gen_api_download` | API v3 视频下载 | 2-4 |
|
||
| `gen_api_upscale` | API v3 超分 | 2-4 |
|
||
| `default` | 默认队列(用户 OAuth、清理任务等) | 1-2 |
|
||
|
||
#### 完整启动命令
|
||
|
||
```bash
|
||
# ── 单机部署(所有队列一个 Worker)──
|
||
celery -A app.tasks.celery_app worker -l info \
|
||
-Q gen_chatapi_create,gen_provider_poll,gen_result_download,gen_video_upscale_local,gen_video_upscale_remote,gen_recovery,gen_private_portrait,gen_shot_analysis,gen_shot_split,gen_api_create,gen_api_poll,gen_api_download,gen_api_upscale,default \
|
||
--concurrency=4
|
||
|
||
# ── 生产环境(按功能分离 Worker)──
|
||
|
||
# 业务 Worker
|
||
celery -A app.tasks.celery_app worker -l info \
|
||
-Q gen_chatapi_create,gen_provider_poll,gen_result_download,gen_api_create,gen_api_poll,gen_api_download \
|
||
--concurrency=4 -n worker_busy@%h
|
||
|
||
# 超分 Worker
|
||
celery -A app.tasks.celery_app worker -l info \
|
||
-Q gen_video_upscale_local,gen_video_upscale_remote,gen_api_upscale \
|
||
--concurrency=2 -n worker_upscale@%h
|
||
|
||
# 恢复 Worker
|
||
celery -A app.tasks.celery_app worker -l info \
|
||
-Q gen_recovery --concurrency=1 -n worker_recovery@%h
|
||
|
||
# 其他 Worker
|
||
celery -A app.tasks.celery_app worker -l info \
|
||
-Q gen_private_portrait,gen_shot_analysis,gen_shot_split,default \
|
||
--concurrency=2 -n worker_other@%h
|
||
|
||
# Beat 调度器(定时任务)
|
||
celery -A app.tasks.celery_app beat -l info
|
||
```
|
||
|
||
#### Beat 定时任务清单
|
||
|
||
| 任务 | 频率 | 说明 |
|
||
|------|------|------|
|
||
| `dispatch-due-poll-tasks` | 每分钟 | 调度到期的轮询任务 |
|
||
| `generation-create-recovery` | 每分钟 | 恢复未完成的创建任务 |
|
||
| `module-async-recovery` | 每分钟 | 恢复模块异步任务 |
|
||
| `video-upscale-recovery` | 每分钟 | 恢复未完成的超分任务 |
|
||
| `generation-download-recovery` | 每分钟 | 恢复未完成的下载任务 |
|
||
| `shot-split-recovery` | 每分钟 | 恢复拆镜切片任务 |
|
||
| `shot-analysis-recovery` | 每分钟 | 恢复拆镜分析任务 |
|
||
| `api-generation-recovery` | 分钟 | 恢复 API v3 未完成任务 |
|
||
| `celery-runtime-reconcile` | 每 5 分钟 | Worker 实例协调 |
|
||
| `celery-runtime-registry-gc` | 每 10 分钟 | Worker 注册表 GC |
|
||
| `private-portrait-sync-due-assets` | 每分钟 | 同步到期素材 |
|
||
| `private-portrait-recover-remote-deletes` | 每 5 分钟 | 恢复远程删除任务 |
|
||
|
||
### 2.14 配置约定
|
||
|
||
```python
|
||
# app/config.py — 基于 pydantic-settings
|
||
from pydantic_settings import BaseSettings
|
||
|
||
class Settings(BaseSettings):
|
||
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
|
||
DATABASE_URL: str = "sqlite+aiosqlite:///./videogen.db"
|
||
SECRET_KEY: str = "change-me"
|
||
# ... 所有配置项有默认值,生产通过 .env 覆盖
|
||
|
||
settings = Settings() # 全局单例
|
||
```
|
||
|
||
**约定:**
|
||
- 所有环境变量在 `config.py` 中有类型注解和默认值
|
||
- 通过 `settings.XXX` 全局访问
|
||
- 布尔值用 `bool` 类型(pydantic 自动解析 "true"/"false" 字符串)
|
||
- 列表值用 `list[str]`(pydantic 自动解析 JSON 数组字符串)
|
||
|
||
### 2.15 操作日志约定
|
||
|
||
```python
|
||
from app.services.operation_log import log_operation
|
||
|
||
# 在路由中调用(通常在变更操作后)
|
||
await log_operation(
|
||
db,
|
||
admin.id,
|
||
admin.username,
|
||
f"创建团队 {team.name}", # action 描述
|
||
"POST", # HTTP method
|
||
"/admin/teams", # path
|
||
detail=None, # 可选详情
|
||
ip=None, # 可选 IP
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
## 三、前端架构 (video-gen-app & video-gen-admin)
|
||
|
||
### 3.1 相同点
|
||
|
||
两个前端项目遵循完全相同的架构约定:
|
||
- React 19 + TypeScript + Vite 8 + Ant Design 6 + Tailwind CSS 3
|
||
- Zustand 状态管理
|
||
- 统一的 API 调用层(crypto/client/index 三层)
|
||
- 路由结构 SPA(react-router-dom v7)
|
||
|
||
### 3.2 目录结构(以 video-gen-app 为例)
|
||
|
||
```
|
||
src/
|
||
├── api/ # API 调用层
|
||
│ ├── client.ts # 核心:apiRequest<T>(加密/解密、错误处理)
|
||
│ ├── crypto.ts # AES-GCM 加密/解密(Web Crypto API)
|
||
│ ├── index.ts # 所有业务 API 函数(按业务域分组)
|
||
│ └── mock.ts # Mock 数据(VITE_USE_MOCK=true 时使用)
|
||
├── store/ # Zustand 状态
|
||
│ ├── useAuthStore.ts # 认证状态(登录/登出/当前用户)
|
||
│ └── useAppStore.ts # 应用状态
|
||
├── types/ # TypeScript 类型定义
|
||
│ └── index.ts # 所有 interface/type
|
||
├── pages/ # 页面组件(路由对应)
|
||
│ ├── LoginPage.tsx
|
||
│ ├── ProjectsPage.tsx
|
||
│ └── ...
|
||
├── components/ # 可复用组件
|
||
│ ├── Layout/
|
||
│ │ ├── AppLayout.tsx # 主布局(Header + Sider + Content)
|
||
│ │ └── AppLayout.css
|
||
│ ├── privatePortrait/
|
||
│ └── ...
|
||
├── utils/ # 工具函数
|
||
│ ├── formatDate.ts
|
||
│ └── ...
|
||
├── App.tsx # 路由定义 + 全局配置
|
||
└── main.tsx # 入口
|
||
```
|
||
|
||
### 3.3 API 调用约定(核心)
|
||
|
||
```typescript
|
||
// ── client.ts — 核心请求函数 ──
|
||
// 所有请求统一走 apiRequest<T>,自动处理加密和响应解析
|
||
async function apiRequest<T>(path: string, options: RequestOptions): Promise<T>
|
||
|
||
// 调用参数:
|
||
interface RequestOptions {
|
||
method?: string // GET/POST/PUT/DELETE,默认 GET
|
||
body?: unknown // 请求体(自动 JSON 序列化 + 加密)
|
||
auth?: boolean // 是否带 Authorization header,默认 true
|
||
encryptBody?: boolean // 是否加密,默认跟随全局开关
|
||
signal?: AbortSignal // 取消请求
|
||
skipAuthRedirect?: boolean // 401 时不跳转登录页
|
||
}
|
||
```
|
||
|
||
```typescript
|
||
// ── index.ts — 业务 API 函数 ──
|
||
// 命名约定:动词 + 业务名,camelCase
|
||
export async function login(username: string, password: string): Promise<User> {
|
||
if (USE_MOCK) return mock.mockLogin({ username, password });
|
||
const res = await api.post<{ accessToken: string; user: User }>(
|
||
'/auth/login',
|
||
{ username, password },
|
||
false, // auth: false(登录不需要 token)
|
||
true // encryptBody: true(登录凭证需要加密)
|
||
);
|
||
setToken(res.accessToken);
|
||
return res.user;
|
||
}
|
||
|
||
// GET 列表
|
||
export async function getProjects(): Promise<Project[]> {
|
||
if (USE_MOCK) return mock.mockGetProjects();
|
||
return api.get<Project[]>('/projects');
|
||
}
|
||
|
||
// POST 创建
|
||
export async function createProject(name: string, industry: Industry): Promise<Project> {
|
||
if (USE_MOCK) return mock.mockCreateProject(name, industry);
|
||
return api.post<Project>('/projects', { name, industry });
|
||
}
|
||
|
||
// 带查询参数的 GET
|
||
export async function getRecordsPage(params): Promise<GenerationRecordPageListOut> {
|
||
const query = new URLSearchParams();
|
||
if (params.projectId) query.set('project_id', params.projectId);
|
||
query.set('page', String(page));
|
||
return api.get<GenerationRecordPageListOut>(`/generation-records?${query.toString()}`);
|
||
}
|
||
```
|
||
|
||
**关键约定:**
|
||
- `api.get/post/put/delete<T>(path, body?, auth?, encryptBody?)` — 返回**已解析的 T 类型数据**(不含响应包裹层)
|
||
- 后端返回 `{ data: ... }` 的结构,`apiRequest` 会自动解包
|
||
- 所有 id 字段前端用 `string`(不转 number)
|
||
- 后端 `snake_case` 字段在 `client.ts` 自动转 `camelCase`(user_id → userId)
|
||
- 文件上传不走 `apiRequest`,直接用 `fetch` + `FormData`(见 `uploadAudio/uploadImage` 等)
|
||
- 上传相关 API 直接读 `import.meta.env.VITE_API_BASE` 拼 URL
|
||
|
||
### 3.4 状态管理约定(Zustand)
|
||
|
||
```typescript
|
||
// store/useAuthStore.ts — 标准模板
|
||
import { create } from 'zustand';
|
||
import type { User } from '../types';
|
||
import * as api from '../api';
|
||
|
||
interface AuthState {
|
||
user: User | null;
|
||
loading: boolean;
|
||
login: (username: string, password: string) => Promise<void>;
|
||
logout: () => Promise<void>;
|
||
checkAuth: () => Promise<void>; // 启动时验证 token 有效性
|
||
}
|
||
|
||
export const useAuthStore = create<AuthState>((set) => ({
|
||
user: null,
|
||
loading: true,
|
||
|
||
login: async (username, password) => {
|
||
const user = await api.login(username, password);
|
||
set({ user });
|
||
},
|
||
// ...
|
||
}));
|
||
```
|
||
|
||
**约定:**
|
||
- 全局状态用 Zustand `create`,不用 Redux
|
||
- API 调用写在 store actions 或页面中,**不要**写在 components 里
|
||
- 状态更新用 `set()`,异步操作加 `async/await`
|
||
- 组件中消费: `const { user, login } = useAuthStore()`
|
||
|
||
### 3.5 页面组件约定
|
||
|
||
```typescript
|
||
// pages/ProjectsPage.tsx — 标准模板
|
||
import React, { useEffect, useState } from 'react';
|
||
import { Button, Empty, Form, Input, message, Modal, Typography } from 'antd';
|
||
import { PlusOutlined } from '@ant-design/icons';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import { useAppStore } from '../store/useAppStore';
|
||
import { getIndustries } from '../api';
|
||
import type { IndustryConfig } from '../types';
|
||
|
||
const ProjectsPage: React.FC = () => {
|
||
const navigate = useNavigate();
|
||
const [projects, setProjects] = useState<Project[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
const loadProjects = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const data = await getProjects(); // 调 API
|
||
setProjects(data);
|
||
} catch (e: any) {
|
||
message.error(e?.message || '加载失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => { loadProjects(); }, []);
|
||
|
||
return (
|
||
<div>
|
||
<Typography.Title level={4}>我的项目</Typography.Title>
|
||
{/* ... */}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ProjectsPage;
|
||
```
|
||
|
||
**约定:**
|
||
- 页面组件是 `React.FC`,默认导出
|
||
- 业务数据类型从 `../types` 导入(`import type`)
|
||
- API 函数从 `../api` 导入
|
||
- 不用 `axios`,所有请求走 `api/index.ts`
|
||
- 用 `message.success/error/warning` 显示操作反馈
|
||
- 用 `useNavigate()` 编程式导航
|
||
- 列表加载用 `loading` 状态 + `Spin` 组件
|
||
- 错误统一 `catch (e: any)` + `message.error(e?.message || '默认消息')`
|
||
|
||
### 3.6 路由约定
|
||
|
||
```typescript
|
||
// App.tsx — 路由结构
|
||
// 公开路由
|
||
<Route path="/login" element={<LoginPage />} />
|
||
<Route path="/join-team" element={<JoinTeamPage />} />
|
||
|
||
// 受保护路由(需要登录)
|
||
<Route path="/" element={<ProtectedRoute><AppLayout /></ProtectedRoute>}>
|
||
<Route index element={<Navigate to="/projects" replace />} />
|
||
<Route path="projects" element={<ProjectsPage />} />
|
||
<Route path="projects/:projectId/generate" element={<GeneratePage />} />
|
||
<Route path="*" element={<Navigate to="/projects" replace />} />
|
||
</Route>
|
||
```
|
||
|
||
**约定:**
|
||
- 路由定义集中在 `App.tsx`
|
||
- 登录保护通过 `ProtectedRoute` 包装器实现(检查 `useAuthStore.user`)
|
||
- 未登录用户访问受保护路由 → 跳 `/login`
|
||
- 动态路由参数用 `:paramName`
|
||
- 嵌套路由用 `<Outlet />` 渲染子页面
|
||
|
||
### 3.7 TypeScript 类型约定
|
||
|
||
```typescript
|
||
// types/index.ts — 全局类型定义
|
||
export interface User {
|
||
id: string; // 所有 ID 都是 string
|
||
username: string;
|
||
credits: number;
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**约定:**
|
||
- 全局共享类型在 `types/index.ts` 定义
|
||
- 类型/接口名 PascalCase + 业务含义
|
||
- 所有 ID、外键字段类型为 `string`
|
||
- 可选字段用 `?:`,不可选但不一定传的用联合类型 `string | null`
|
||
- 从其他文件导入类型: `import type { ... } from '../types'`(用 `import type` 优化打包)
|
||
|
||
### 3.8 加密约定
|
||
|
||
```typescript
|
||
// crypto.ts
|
||
const USE_ENCRYPTION = !!import.meta.env.VITE_ENCRYPTION_KEY && isCryptoAvailable();
|
||
|
||
// 前端加密流程:
|
||
// 1. apiRequest 中如果 encryptBody=true:
|
||
// body → JSON.stringify → encrypt() → { data: "<密文>" }
|
||
// headers['X-Encrypted'] = 'true'
|
||
// 2. 收到响应如果 encryptBody=true 且 parsed.data 是字符串:
|
||
// decrypt(parsed.data) → JSON.parse → 返回
|
||
//
|
||
// 自动判断: USE_ENCRYPTION 全局开关控制是否启用加密
|
||
// 加密算法: AES-256-GCM (12 字节 IV + 128 位认证标签)
|
||
// 密钥来源: VITE_ENCRYPTION_KEY (32 字节 base64)
|
||
```
|
||
|
||
### 3.9 Mock 模式约定
|
||
|
||
当 `VITE_USE_MOCK=true` 时,前端走本地假数据,无需后端。
|
||
|
||
```typescript
|
||
// api/index.ts 中每个 API 函数都检查 USE_MOCK
|
||
const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true';
|
||
|
||
export async function getProjects(): Promise<Project[]> {
|
||
if (USE_MOCK) return mock.mockGetProjects(); // ← 先看 mock
|
||
return api.get<Project[]>('/projects'); // ← 后走真实请求
|
||
}
|
||
```
|
||
|
||
**新增 API 函数时**必须同时提供 mock 数据函数(在 `mock.ts` 中)。
|
||
|
||
---
|
||
|
||
## 四、跨项目约定(前后端协作)
|
||
|
||
### 4.1 字段名转换
|
||
|
||
| 后端 (Python) | 前端 (TypeScript) | 转换 |
|
||
|--------------|-------------------|------|
|
||
| `snake_case` | `camelCase` | client.ts 自动转 |
|
||
| `created_at` | `createdAt` | 自动 |
|
||
| `user_id` | `userId` | 自动 |
|
||
|
||
**新增 API 时**:
|
||
- 后端 request schema 字段用 `snake_case`
|
||
- 前端 types/interface 字段用 `camelCase`
|
||
- **不需要**手动转,`client.ts` 的 `keysToCamel()` 递归转换所有 key
|
||
|
||
### 4.2 API 路径约定
|
||
|
||
```
|
||
/api/auth/login → 登录(公开)
|
||
/api/projects → 项目管理(需登录)
|
||
/api/admin/users → 用户管理(需管理员)
|
||
/admin/teams → 团队管理(需管理员,二级路由)
|
||
```
|
||
|
||
- `/api/` 前缀统一
|
||
- 管理接口路径含 `/admin/`
|
||
- 路径用小写 + 短横线(kebab-case)
|
||
|
||
### 4.3 分页约定
|
||
|
||
后端返回:
|
||
```python
|
||
@router.get("", response_model=GenerationRecordPageListOut)
|
||
# { items: [...], total: 100, page: 1, page_size: 20 }
|
||
```
|
||
|
||
前端类型:
|
||
```typescript
|
||
export interface GenerationRecordPageListOut {
|
||
page: number;
|
||
pageSize: number;
|
||
total: number;
|
||
items: GenerationRecord[];
|
||
}
|
||
```
|
||
|
||
### 4.4 错误处理约定
|
||
|
||
后端抛 `HTTPException(status_code, detail)` → 前端 `apiRequest` 捕获后抛 `Error` → 页面 `catch` + `message.error(e.message)`。
|
||
|
||
常见错误码:
|
||
- `401` — token 无效/过期 → 自动跳 `/login`
|
||
- `403` — 权限不足
|
||
- `404` — 资源不存在
|
||
- `402` — 积分不足(`InsufficientCreditsError`)
|
||
- `400` — 参数错误
|
||
|
||
---
|
||
|
||
## 五、修改检查清单
|
||
|
||
### 新增后端接口时 ✅
|
||
|
||
- [ ] 在 `app/schemas/` 中定义 Request/Response Schema(含 `from_attributes`)
|
||
- [ ] 在 `app/services/` 中实现业务逻辑函数
|
||
- [ ] 在 `app/api/v1/` (或 `app/api/admin/`) 中定义路由(`APIRouter` + `Depends`)
|
||
- [ ] 确认 `__init__.py` 中注册了新 router
|
||
- [ ] 复杂操作记录 `log_operation()`
|
||
- [ ] 写入操作后正确 `flush()` + `commit()`
|
||
|
||
### 新增后端 Model 时 ✅
|
||
|
||
- [ ] 在 `app/models/` 中新建或编辑 model 文件
|
||
- [ ] 继承 `Base` + 按需加 `TimestampMixin` / `SoftDeleteMixin`
|
||
- [ ] 主键用 `generate_id()`,外键加 `index=True`
|
||
- [ ] 在 `app/models/__init__.py` 中 import
|
||
- [ ] 执行 `alembic revision --autogenerate -m "描述"` 生成迁移
|
||
- [ ] 执行 `alembic upgrade head` 应用迁移
|
||
|
||
### 新增前端页面/功能时 ✅
|
||
|
||
- [ ] 在 `types/index.ts` 中定义/补充类型
|
||
- [ ] 在 `api/index.ts` 中添加 API 函数(含 mock 分支)
|
||
- [ ] 在 `pages/` 中新建页面组件(`React.FC` + 默认导出)
|
||
- [ ] 在 `App.tsx` 中注册路由
|
||
- [ ] 受保护路由包裹 `<ProtectedRoute>`
|
||
- [ ] 在 `mock.ts` 中添加对应 mock 数据函数
|
||
|
||
---
|
||
|
||
## 六、常见反模式(应避免)
|
||
|
||
| ❌ 反模式 | ✅ 正确做法 |
|
||
|----------|-----------|
|
||
| Route 中写复杂 ORM 查询 | 抽到 `services/` 函数 |
|
||
| Model 中有业务逻辑 | Model 只定义字段和关系 |
|
||
| 前端直接用 `fetch` 调后端 API | 走 `api/index.ts` + `apiRequest` |
|
||
| 新增 Model 不加到 `__init__.py` | 立即 import |
|
||
| 主键用数据库自增 int | 用 `generate_id()` 生成 string |
|
||
| Schema 不加 `from_attributes` | 响应 Schema 必须加 |
|
||
| 前端用 `any` 不定义类型 | 在 `types/index.ts` 定义 |
|
||
| 密码明文存储 | bcrypt 哈希(通过 `hash_password`) |
|
||
|
||
---
|
||
|
||
## 七、快速定位表
|
||
|
||
| 想修改什么 | 去哪里 |
|
||
|-----------|--------|
|
||
| 新增/改表字段 | `app/models/xxx.py` + `alembic revision` |
|
||
| 新增接口 | `app/api/v1/xxx.py` + `app/schemas/xxx.py` + `app/services/xxx.py` |
|
||
| 改业务规则 | `app/services/xxx.py` |
|
||
| 改枚举值 | `app/enums/xxx.py` |
|
||
| 加新页面 | `src/pages/XxxPage.tsx` + `App.tsx` 路由 + `src/types/index.ts` |
|
||
| 加新 API 调用 | `src/api/index.ts` + `src/api/mock.ts` |
|
||
| 改全局状态 | `src/store/useAuthStore.ts` / `useAppStore.ts` |
|
||
| 改配置项 | `app/config.py` + `.env.example` |
|
||
| 改中间件行为 | `app/middleware/xxx.py` |
|
||
| 加异步任务 | `app/tasks/xxx.py` + `celery_app.py` 的 `task_routes` | |