Files
video-gen/CODEBASE.md
2026-07-11 12:51:48 +08:00

813 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)` 传递
### 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 三层)
- 路由结构 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 调用约定(核心)
```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` |