Files
video-gen/CODEBASE.md
T
2026-08-06 16:44:28 +08:00

31 KiB
Raw Blame History

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

# 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=Trueondelete="CASCADE" 按需
  • 所有 str 类型指定长度:String(32) / String(128) / String(255)
  • 时间统一用 DateTime(timezone=True)
  • 新增 model 后必须在 app/models/__init__.py 中 import
# 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 约定

# 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 路由约定

# 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/apimain.pyinclude_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 依赖注入约定

# 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 服务层约定

# 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 枚举约定

# 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 异常处理约定

# app/utils/exceptions.py — 预定义异常
InsufficientCreditsError     # 402 积分不足
CaptchaFailedError           # 400 验证码失败
RecordNotFoundError          # 404 记录不存在
ProjectNotFoundError         # 404 项目不存在
InvalidStatusError           # 400 状态不允许操作

# 在 service 或 route 中抛出
raise InsufficientCreditsError()

2.10 认证约定

# 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 模式

# 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):

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 任务约定

# 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.pytask_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

完整启动命令

# ── 单机部署(所有队列一个 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 配置约定

# 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 操作日志约定

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 三层)
  • 路由结构 SPAreact-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 调用约定(核心)

// ── 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 时不跳转登录页
}
// ── 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 自动转 camelCaseuser_id → userId
  • 文件上传不走 apiRequest,直接用 fetch + FormData(见 uploadAudio/uploadImage 等)
  • 上传相关 API 直接读 import.meta.env.VITE_API_BASE 拼 URL

3.4 状态管理约定(Zustand

// 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 页面组件约定

// 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 路由约定

// 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 类型约定

// 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 加密约定

// 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 时,前端走本地假数据,无需后端。

// 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.tskeysToCamel() 递归转换所有 key

4.2 API 路径约定

/api/auth/login                  → 登录(公开)
/api/projects                    → 项目管理(需登录)
/api/admin/users                 → 用户管理(需管理员)
/admin/teams                     → 团队管理(需管理员,二级路由)
  • /api/ 前缀统一
  • 管理接口路径含 /admin/
  • 路径用小写 + 短横线(kebab-case

4.3 分页约定

后端返回:

@router.get("", response_model=GenerationRecordPageListOut)
# { items: [...], total: 100, page: 1, page_size: 20 }

前端类型:

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.pytask_routes