diff --git a/CODEBASE.md b/CODEBASE.md new file mode 100644 index 00000000..9cc25c3f --- /dev/null +++ b/CODEBASE.md @@ -0,0 +1,802 @@ +# VideoGen 代码架构指南 + +> 本文档面向开发者或 AI 模型,帮助快速理解代码风格、架构约定和修改规则。 + +--- + +## 一、整体架构 + +``` +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 ` +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 三层) +- 路由结构 SPA(react-router-dom v7) + +### 3.2 目录结构(以 video-gen-app 为例) + +``` +src/ +├── api/ # API 调用层 +│ ├── client.ts # 核心:apiRequest(加密/解密、错误处理) +│ ├── 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,自动处理加密和响应解析 +async function apiRequest(path: string, options: RequestOptions): Promise + +// 调用参数: +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 { + 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 { + if (USE_MOCK) return mock.mockGetProjects(); + return api.get('/projects'); +} + +// POST 创建 +export async function createProject(name: string, industry: Industry): Promise { + if (USE_MOCK) return mock.mockCreateProject(name, industry); + return api.post('/projects', { name, industry }); +} + +// 带查询参数的 GET +export async function getRecordsPage(params): Promise { + const query = new URLSearchParams(); + if (params.projectId) query.set('project_id', params.projectId); + query.set('page', String(page)); + return api.get(`/generation-records?${query.toString()}`); +} +``` + +**关键约定:** +- `api.get/post/put/delete(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; + logout: () => Promise; + checkAuth: () => Promise; // 启动时验证 token 有效性 +} + +export const useAuthStore = create((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([]); + 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 ( +
+ 我的项目 + {/* ... */} +
+ ); +}; + +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 — 路由结构 +// 公开路由 +} /> +} /> + +// 受保护路由(需要登录) +}> + } /> + } /> + } /> + } /> + +``` + +**约定:** +- 路由定义集中在 `App.tsx` +- 登录保护通过 `ProtectedRoute` 包装器实现(检查 `useAuthStore.user`) +- 未登录用户访问受保护路由 → 跳 `/login` +- 动态路由参数用 `:paramName` +- 嵌套路由用 `` 渲染子页面 + +### 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 { + if (USE_MOCK) return mock.mockGetProjects(); // ← 先看 mock + return api.get('/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` 中注册路由 +- [ ] 受保护路由包裹 `` +- [ ] 在 `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` | \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 05376543..9acde179 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -5,11 +5,13 @@ ``` video_item/ ├── video-gen-api/ # 后端 (Python FastAPI + SQLAlchemy + Alembic) -├── video-gen-app/ # 前台 (React 19 + Vite 8 + Ant Design 6 + Tailwind) -├── video-gen-admin/ # 后台管理 (React 19 + Vite 8 + Ant Design 6) +├── video-gen-app/ # 前台/用户端 (React 19 + Vite 8 + Ant Design 6) +├── video-gen-admin/ # 后台管理端 (React 19 + Vite 8 + Ant Design 6) └── DEPLOYMENT.md # 本文档 ``` +**三个前端/后端的关系:** `video-gen-app` (用户前台) 和 `video-gen-admin` (管理后台) 都连接同一个 `video-gen-api` 后端。 + --- ## 一、环境要求 @@ -18,14 +20,13 @@ video_item/ |------|---------|------| | Python | >= 3.10 | 推荐 3.12 | | Node.js | >= 18 | 推荐 20+ | -| PostgreSQL | >= 14 | 推荐 16 | -| Redis | >= 6 | 可选,推荐用于限流/验证码/Celery | -| FFmpeg | 任意 | 可选,用于视频封面截帧 | -| alipay-sdk-python | >=3.7.1160 | 可选,用于支付 | -| wechatpayv3 | >=2.0.2 | 可选,用于支付 | -| volcengine-python-sdk | >=1.1.0 | 可选,用于视频生成 | +| PostgreSQL | >= 14 | 推荐 16;**必须** | +| Redis | >= 6 | 推荐用于: 限流/验证码/Celery/任务状态 | +| FFmpeg | 任意 | 用于视频封面截帧,留空时从 PATH 自动查找 | | ca-certificates | 任意 | **必须**,HTTPS 请求需要(新服务器/容器常缺) | - +| alipay-sdk-python | >=3.7.1160 | 可选,支付宝支付 | +| wechatpayv3 | >=2.0.2 | 可选,微信支付 | +| volcengine-python-sdk | >=1.1.0 | 可选,视频生成/短信 | --- @@ -34,7 +35,7 @@ video_item/ ### 1. 安装依赖 ```bash -# ⚠️ 新服务器/容器必须先装 CA 证书,否则 HTTPS 请求(支付宝/火山等)全部失败 +# ⚠️ 新服务器/容器必须先装 CA 证书,否则 HTTPS(支付宝/火山等)全部失败 # CentOS/RHEL sudo yum install -y ca-certificates # Ubuntu/Debian @@ -49,7 +50,10 @@ python -m venv .venv # Linux/Mac source .venv/bin/activate -# 安装基础依赖 + PostgreSQL 驱动 +# 安装基础依赖 +pip install -e . + +# 安装 PostgreSQL 驱动(生产环境必须) pip install -e ".[pg]" # 如需 Redis 支持(限流、验证码、Celery) @@ -58,21 +62,19 @@ pip install -e ".[pg,redis]" # 如需 Celery 异步任务(ChatAPI 生成流水线) pip install -e ".[pg,redis,celery]" -#安装阿里支付sdk +# 如需支付宝 pip install -e ".[pg,redis,celery,alipay]" -# 安装微信支付sdk -pip install -e ".[pg,redis,celery,alipay,wechatpayv3]" - -#安装火山sdk -pip install -e ".[pg,redis,celery,alipay,wechatpayv3,volc]" - +# 如需微信支付 +pip install -e ".[pg,redis,celery,alipay,wxpay]" +# 如需火山引擎 SDK(短信等) +pip install -e ".[pg,redis,celery,alipay,wxpay,volc]" ``` ### 2. 配置环境变量 -复制 `.env.example` 为 `.env`,修改以下关键配置: +复制 `.env.example` 为 `.env`,修改关键配置: ```bash cp .env.example .env @@ -81,13 +83,14 @@ cp .env.example .env ```ini # ── 基础配置 ── APP_NAME=VideoGen API +APP_VERSION=1.0.0 DEBUG=false -SECRET_KEY=改成一个随机的长字符串 +SECRET_KEY=改成一个随机的长字符串(JWT 签名密钥) -# ── 数据库 ── +# ── 数据库(必须) ── DATABASE_URL=postgresql+asyncpg://用户名:密码@localhost:5432/videogen -# ── Redis(可选,留空则禁用限流和验证码) ── +# ── Redis(可选,留空则禁用限流/验证码/Celery) ── REDIS_URL=redis://localhost:6379/0 # ── JWT ── @@ -101,24 +104,30 @@ SEEDANCE_API_BASE=https://ark.cn-beijing.volces.com/api/v3 SEEDANCE_CALLBACK_URL=https://你的域名/api/generation-records/callback # ── LLM 提示词优化 ── -LLM_MOCK=false +LLM_API_BASE=https://api.openai.com/v1 +LLM_API_KEY= +LLM_MODEL=gpt-4o +LLM_MOCK=true # true=使用 mock 响应,不调用真实 LLM # ── 前后端通信加密(32字节 base64,留空则禁用) ── ENCRYPTION_KEY=你的32字节base64密钥 -# ── 存储路径 ── +# ── 存储路径(本地存储) ── STORAGE_TYPE=local STORAGE_LOCAL_PATH=./storage/generate/videos STORAGE_IMAGE_LOCAL_PATH=./storage/generate/images STORAGE_VIDEO_COVER_LOCAL_PATH=./storage/generate/covers UPLOAD_LOCAL_PATH=./storage/uploads -# ── 跨域(生产环境务必限制域名) ── +# ── 跨域(生产环境务必限制域名,默认 ["*"] 允许所有) ── CORS_ORIGINS=["https://你的前台域名.com", "https://你的后台域名.com"] # ── 回调基础地址 ── BASE_URL=https://你的域名 +# ── 验证码 ── +CAPTCHA_ENABLED=true + # ── 短信(火山引擎 SDK,SMS_MOCK=false 时生效) ── SMS_MOCK=true VOLC_SMS_ACCESS_KEY_ID= @@ -129,6 +138,12 @@ VOLC_SMS_SIGN=短信签名 # ── 支付(PAYMENT_MOCK=false 时生效) ── PAYMENT_MOCK=true +WECHAT_MCH_ID= +WECHAT_API_KEY= +ALIPAY_APP_ID= +ALIPAY_PRIVATE_KEY= +ALIPAY_PUBLIC_KEY= +ALIPAY_NOTIFY_URL= # ── Celery(可选,留空则禁用 ChatAPI 异步流水线) ── CELERY_BROKER_URL=redis://localhost:6379/5 @@ -138,27 +153,30 @@ CELERY_RESULT_BACKEND=redis://localhost:6379/6 FFMPEG_BIN=/usr/bin/ffmpeg ``` +> **关于 ENCRYPTION_KEY 的生成:** 需要 32 字节(256 位)的 base64 编码字符串。生成方式:`openssl rand -base64 32`。前后端必须使用**完全相同**的密钥。 + ### 3. 初始化数据库 ```bash # 创建 PostgreSQL 数据库 psql -U postgres -c "CREATE DATABASE videogen OWNER videogen;" -# 启动后端(首次启动自动建表 + 填充种子数据) +# 启动后端(首次启动自动建表) python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` -首次启动会自动完成: -- 创建所有数据表(23 个 model) -- 创建管理员账号:`admin` / `123456` -- 创建演示用户:`demo` / `123456`(手机号 `13888888888`) -- 填充系统配置、视频引擎(Seedance 2.0 / 2.0 fast)、图片引擎(Seedream 5.0)、模型配置、积分比例、菜单配置、充值套餐(4 档)、行业配置 +首次启动时会**自动创建所有数据表**(41 个 model)。 -**首次部署后务必修改默认密码。** +> ⚠️ **关于种子数据:** 代码中包含 `_seed_data()` 函数(创建管理员/演示用户、系统配置、引擎配置等),但当前在 `main.py` 中被**注释掉了**(`# await _seed_data()`)。因此首次启动**不会**自动创建管理员账号。 +> +> **如果你需要种子数据,**有以下选择: +> 1. 在 `main.py` 中取消注释 `# await _seed_data()` 后重启 +> 2. 手动通过 API 或数据库脚本创建管理员账号 +> 3. 自行编写独立的种子脚本调用 `_seed_data()` ### 4. 数据库迁移 (Alembic) -项目使用 Alembic 管理数据库结构变更,`env.py` 已导入全部 20 个 model。 +项目使用 Alembic 管理数据库结构变更。 ```bash cd video-gen-api @@ -179,17 +197,13 @@ python -m alembic history python -m alembic downgrade -1 ``` -**部署流程:** 拉取代码后先执行 `alembic upgrade head`,再重启后端服务。 - -**新增 model 时:** 需要在 `alembic/env.py` 中添加对应的 import。 +**部署流程:** 拉取代码后先执行 `alembic upgrade head`,再重启后端服务。 ### 5. 生产运行 ```bash -# 方式一:直接运行(推荐 4 workers) +# 直接运行(推荐 4 workers) python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 - -# 方式二:使用 systemd (Linux) ``` **systemd 服务文件** `/etc/systemd/system/videogen-api.service`: @@ -220,11 +234,22 @@ sudo systemctl start videogen-api ### 6. Celery Worker(可选) -ChatAPI 异步生成流水线需要 Celery Worker。需要 Redis 作为 Broker。 +ChatAPI 异步生成流水线需要 Celery Worker,依赖 Redis 作为 Broker。 + +Celery 使用 **6 个队列**,按功能分离: + +| 队列 | 用途 | +|------|------| +| `gen_chatapi_create` | ChatAPI 生成任务创建(含爆款开头/拆镜复刻的提词步骤) | +| `gen_provider_poll` | 轮询火山引擎生成状态 | +| `gen_result_download` | 下载生成的视频/图片结果 | +| `gen_recovery` | 容灾恢复任务(统一队列,避免占用业务 worker) | +| `gen_private_portrait` | 真人素材认证与同步 | +| `default` | 默认队列(用户 OAuth、清理任务等) | ```bash -# 启动 Worker(处理 3 个队列:gen_chatapi_create, gen_provider_poll, gen_result_download) -celery -A app.tasks.celery_app worker -l info -Q gen_chatapi_create,gen_provider_poll,gen_result_download,default +# 启动 Worker(消费所有队列) +celery -A app.tasks.celery_app worker -l info -Q gen_chatapi_create,gen_provider_poll,gen_result_download,gen_recovery,gen_private_portrait,default ``` **systemd 服务文件** `/etc/systemd/system/videogen-worker.service`: @@ -239,7 +264,7 @@ Type=simple User=www-data WorkingDirectory=/opt/video-gen-api Environment=PATH=/opt/video-gen-api/.venv/bin -ExecStart=/opt/video-gen-api/.venv/bin/celery -A app.tasks.celery_app worker -l info -Q gen_chatapi_create,gen_provider_poll,gen_result_download,default +ExecStart=/opt/video-gen-api/.venv/bin/celery -A app.tasks.celery_app worker -l info -Q gen_chatapi_create,gen_provider_poll,gen_result_download,gen_recovery,gen_private_portrait,default Restart=always RestartSec=5 @@ -249,23 +274,25 @@ WantedBy=multi-user.target ### 7. Docker 部署(可选) -项目提供 `Dockerfile` 和 `docker-compose.yml`,一键启动完整环境: +项目提供 `Dockerfile` 和 `docker-compose.yml`。 + +> ⚠️ **注意:** 默认 `Dockerfile` 只安装基础依赖(`pip install .`),生产使用需改为 `".[pg,redis,celery]"`。 ```bash cd video-gen-api -# 注意:Dockerfile 默认只装基础依赖,需修改为安装 pg+redis+celery -# 将 Dockerfile 中的 RUN pip install --no-cache-dir . 改为: +# 使用前需修改 Dockerfile 第 6 行为: # RUN pip install --no-cache-dir ".[pg,redis,celery]" docker compose up -d ``` 启动的服务: + | 服务 | 端口 | 说明 | |------|------|------| -| api | 8000 | FastAPI 应用(带 `--reload`,开发模式) | -| worker | - | Celery Worker | +| api | 8000 | FastAPI 应用(`--reload` 开发模式) | +| worker | - | Celery Worker(消费所有队列) | | postgres | 5432 | PostgreSQL 16 | | redis | 6379 | Redis 7 | @@ -276,7 +303,7 @@ server { listen 80; server_name api.yourdomain.com; - # 上传文件大小限制 + # 上传文件大小限制(视频上传需要较大值) client_max_body_size 100M; location / { @@ -300,6 +327,8 @@ server { ## 三、前台部署 (video-gen-app) +用户前台,面向最终用户。 + ### 1. 安装依赖 & 构建 ```bash @@ -310,8 +339,8 @@ npm install # 配置 API 地址(创建 .env.production) echo "VITE_API_BASE=https://api.yourdomain.com" > .env.production -# 如需前后端加密通信 -echo "VITE_ENCRYPTION_KEY=与后端ENCRYPTION_KEY相同" >> .env.production +# 如需前后端加密通信(与后端 ENCRYPTION_KEY 相同) +echo "VITE_ENCRYPTION_KEY=密钥" >> .env.production # 构建 npm run build @@ -319,7 +348,15 @@ npm run build 构建产物在 `dist/` 目录。 -### 2. Nginx 配置 +### 2. 前端环境变量 + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| `VITE_API_BASE` | 后端 API 地址 | `http://localhost:8000` | +| `VITE_USE_MOCK` | 是否使用 mock 数据(无需后端) | `false` | +| `VITE_ENCRYPTION_KEY` | 前后端通信加密密钥(需与后端一致) | 空(不加密) | + +### 3. Nginx 配置 ```nginx server { @@ -328,12 +365,12 @@ server { root /opt/video-gen-app/dist; index index.html; - # SPA 路由 + # SPA 路由:所有页面请求回退到 index.html location / { try_files $uri $uri/ /index.html; } - # 静态资源缓存 + # 静态资源缓存(带 hash 的文件名可长期缓存) location /assets/ { expires 1y; add_header Cache-Control "public, immutable"; @@ -345,6 +382,8 @@ server { ## 四、后台管理部署 (video-gen-admin) +管理员后台,面向运营/管理人员。 + ### 1. 安装依赖 & 构建 ```bash @@ -356,13 +395,21 @@ npm install echo "VITE_API_BASE=https://api.yourdomain.com" > .env.production # 如需前后端加密通信 -echo "VITE_ENCRYPTION_KEY=与后端ENCRYPTION_KEY相同" >> .env.production +echo "VITE_ENCRYPTION_KEY=密钥" >> .env.production # 构建 npm run build ``` -### 2. Nginx 配置 +### 2. 后台环境变量 + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| `VITE_API_BASE` | 后端 API 地址 | `http://localhost:8000` | +| `VITE_USE_MOCK` | 是否使用 mock 数据 | `false` | +| `VITE_ENCRYPTION_KEY` | 前后端通信加密密钥 | 空(不加密) | + +### 3. Nginx 配置 ```nginx server { @@ -456,6 +503,8 @@ sudo certbot --nginx -d yourdomain.com -d admin.yourdomain.com -d api.yourdomain ## 七、默认账号 +> ⚠️ 默认账号仅在种子数据被执行后存在(见第三节第 3 点说明)。 + | 角色 | 用户名 | 手机号 | 密码 | 积分 | |------|--------|--------|------|------| | 管理员 | admin | 13800000000 | 123456 | 10000 | @@ -467,32 +516,84 @@ sudo certbot --nginx -d yourdomain.com -d admin.yourdomain.com -d api.yourdomain ## 八、架构说明 -### 异步任务处理 +### 后端启动时的后台任务 -系统有两套异步任务机制: +后端启动时(`lifespan`)会自动启动以下**进程内**后台任务: -| 机制 | 说明 | 依赖 | -|------|------|------| -| 内置 TaskQueue | asyncio 实现,运行在 uvicorn 进程内,轮询视频/图片生成状态 | 无额外依赖 | -| Celery Worker | 处理 ChatAPI 异步生成流水线,3 个队列分优先级 | Redis 作为 Broker | +| 任务 | 说明 | +|------|------| +| `task_queue` (video_queue) | 内置 asyncio 任务队列,轮询视频/图片生成状态 | +| `upload_queue` | 上传任务队列 | +| `material_consumption_queue` | 素材消耗队列 | +| `token_refresh_scheduler` | 每 5 分钟检查并刷新即将过期的 token | +| `poll_pre_test_results` | 每分钟轮询前测结果 | +| `schedule_daily_sync` | 每天 9 点自动同步素材消耗 | +| `_order_expiry_loop` | 每分钟同步待支付订单状态 + 自动过期订单 | -### 中间件栈(从外到内) +加上 **Celery Worker**(可选,处理 ChatAPI 异步流水线)。 + +### 中间件栈(从外到内,即请求到达的顺序) 1. `RequestLoggingMiddleware` — 请求/响应日志记录 2. `AntiCrawlerMiddleware` — 反爬虫(拦截空 UA 和常见 bot) 3. `RateLimitMiddleware` — 滑动窗口限流(Redis 支撑) 4. `RequestEncryptMiddleware` — AES-256-GCM 请求/响应加密 -5. `CORSMiddleware` — 跨域 +5. `CORSMiddleware` — 跨域(expose `X-Encrypted` 响应头) -### 日志系统 +### 前后端通信加密 -| 日志类型 | 目录 | 控制方式 | +**机制:** AES-256-GCM 对称加密,前后端共享同一个 `ENCRYPTION_KEY`。使用 Web Crypto API(前端)和 `cryptography` 库(后端)。 + +**生效条件:** 同时满足以下两个条件才启用加密: +- 后端 `.env` 中 `ENCRYPTION_KEY` 非空 +- 前端 `.env` 中 `VITE_ENCRYPTION_KEY` 非空 +- 运行在安全上下文(HTTPS 或 localhost) + +**加密范围:** + +| 请求类型 | 请求体 | 响应体 | +|---------|--------|--------| +| GET(无 body) | 不加密(无内容) | **加密** | +| POST/PUT/DELETE(有 body) | **加密** | **加密** | + +**工作流:** +1. 前端发送 POST 请求时,将 JSON 请求体加密为 `{ data: "<密文>" }`,并加 `X-Encrypted: true` 请求头 +2. 后端中间件检测到 `X-Encrypted: true` 时解密请求体,处理完后加密响应体 +3. 支付回调接口(`/payments/alipay/callback`, `/payments/wechat/callback`)白名单跳过加密 + +> **GET 请求注意:** GET 没有请求体,但响应仍会被加密。前端会自动检测并解密。 + +### 文件日志加密(独立机制) + +与通信加密不同,**文件日志存储**使用另一套独立的 AES-CBC-256 加密: + +| 日志类型 | 目录 | 加密方式 | |---------|------|---------| -| 请求/响应日志 | `log/RequestResponse/{日期}.log` | 始终开启 | -| AI 模型日志 | `log/AiModel/{日期}.log` | `AI_LOG_ENABLED` 环境变量 | -| Python 控制台日志 | stderr | `DEBUG=true` 时输出 INFO,否则 WARNING | +| 请求/响应日志 | `log/RequestResponse/{日期}.log` | AES-CBC-256,密钥硬编码 | +| AI 模型日志 | `log/AiModel/{日期}.log` | AES-CBC-256,密钥硬编码 | -文件日志使用 AES-CBC 加密存储,解密工具:`/internal/decrypt-data` 端点。 +日志加密密钥与 `ENCRYPTION_KEY` 无关,是代码中硬编码的值。解密工具:`/internal/decrypt-data` 页面。 + +### 内部管理端点 + +| 端点 | 用途 | +|------|------| +| `/internal/` | 后端入口导航页 | +| `/internal/health` | 健康检查 (`{"status": "ok"}`) | +| `/internal/status` | 服务状态监控页 (Celery/Redis 连接状态) | +| `/internal/decrypt-data` | 日志数据解密工具(AES-CBC) | +| `/internal/api-docs` | Swagger API 文档 | +| `/internal/api-redoc` | ReDoc API 文档 | +| `/uploads/` | 用户上传的静态文件(挂载为静态目录) | +| `/api/decrypt` | 解密接口(POST,供解密工具调用) | + +### 前端 Mock 模式 + +前端支持 Mock 数据模式,无需后端即可开发: + +- 设置 `VITE_USE_MOCK=true` 时,所有 API 调用返回本地假数据 +- 适用场景:纯前端开发、演示、无后端环境 +- 注意:mock 模式下仍会调用部分真实 API(如站点信息、验证码) ### 存储路径 @@ -522,6 +623,15 @@ sudo certbot --nginx -d yourdomain.com -d admin.yourdomain.com -d api.yourdomain │ ├── .env # 环境变量 │ ├── .venv/ # Python 虚拟环境 │ ├── app/ # 应用代码 +│ │ ├── api/ # API 路由 +│ │ │ ├── v1/ # v1 版本接口(前端用户端 + 部分管理接口) +│ │ │ └── admin/ # 管理端接口 +│ │ ├── middleware/ # 中间件 +│ │ ├── models/ # 数据模型 (41 个) +│ │ ├── services/ # 业务服务 +│ │ ├── tasks/ # Celery 任务 +│ │ ├── enums/ # 枚举定义 +│ │ └── utils/ # 工具函数 │ ├── alembic/ # 数据库迁移文件 │ ├── storage/ │ │ ├── generate/ @@ -565,4 +675,69 @@ cd /opt/video-gen-api && python -m alembic upgrade head # 生成迁移文件 cd /opt/video-gen-api && python -m alembic revision --autogenerate -m "描述" + +# 查看 Celery 活动任务 +celery -A app.tasks.celery_app inspect active ``` + +--- + +## 十一、环境变量完整参考 + +### 后端 (video-gen-api/.env) + +| 变量 | 必填 | 默认值 | 说明 | +|------|------|--------|------| +| `SECRET_KEY` | **是** | `change-me` | JWT 签名密钥 | +| `DATABASE_URL` | **是** | sqlite | PostgreSQL 连接串 | +| `REDIS_URL` | 否 | 空 | Redis 连接串,留空禁用限流/验证码 | +| `JWT_EXPIRE_MINUTES` | 否 | 1440 | Token 有效期(分钟) | +| `JWT_EXPIRE_REMEMBER_MINUTES` | 否 | 10080 | 记住登录 Token 有效期 | +| `SEEDANCE_API_KEY` | **是** | 空 | 火山引擎 API Key | +| `SEEDANCE_API_BASE` | 否 | 火山地址 | API 基础 URL | +| `SEEDANCE_CALLBACK_URL` | 否 | 空 | 生成结果回调 URL | +| `LLM_API_BASE` | 否 | OpenAI | LLM API 地址 | +| `LLM_API_KEY` | 否 | 空 | LLM API Key | +| `LLM_MODEL` | 否 | gpt-4o | LLM 模型名称 | +| `LLM_MOCK` | 否 | true | 是否 mock LLM 响应 | +| `ENCRYPTION_KEY` | 否 | 占位符 | 前后端通信加密密钥 | +| `SMS_MOCK` | 否 | true | 是否 mock 短信 | +| `PAYMENT_MOCK` | 否 | false | 是否 mock 支付 | +| `CORS_ORIGINS` | 否 | `["*"]` | 允许的跨域来源 | +| `BASE_URL` | 否 | 测试地址 | 站点基础 URL,用于回调拼接 | +| `STORAGE_TYPE` | 否 | local | 存储类型 | +| `UPLOAD_LOCAL_PATH` | 否 | `./storage/uploads` | 上传文件存储路径 | +| `FFMPEG_BIN` | 否 | 空 | FFmpeg 路径(留空自动查找) | +| `CAPTCHA_ENABLED` | 否 | true | 是否启用验证码 | +| `CELERY_BROKER_URL` | 否 | 空 | Celery Broker(留空禁用 Celery) | +| `CELERY_RESULT_BACKEND` | 否 | 空 | Celery 结果后端 | +| `RATE_LIMIT_ENABLED` | 否 | true | 是否启用限流 | + +### 前台 (video-gen-app/.env.production) + +| 变量 | 必填 | 默认值 | 说明 | +|------|------|--------|------| +| `VITE_API_BASE` | **是** | localhost:8000 | 后端 API 地址 | +| `VITE_USE_MOCK` | 否 | false | 是否使用 mock 数据 | +| `VITE_ENCRYPTION_KEY` | 否 | 空 | 通信加密密钥(与后端一致) | + +### 后台管理 (video-gen-admin/.env.production) + +同前台。 + +--- + +## 十二、故障排查 + +| 现象 | 可能原因 | 解决方案 | +|------|---------|---------| +| 前端登录后 401 | Token 过期或 SECRET_KEY 不一致 | 检查后端 `SECRET_KEY` 是否变更 | +| 上传文件失败 413 | Nginx 上传大小限制 | 增大 `client_max_body_size` | +| 加密请求报错"解密失败" | 前后端密钥不一致 | 确保 `VITE_ENCRYPTION_KEY` = 后端 `ENCRYPTION_KEY` | +| 非 HTTPS 环境加密无效 | Web Crypto API 需要安全上下文 | 本地开发用 localhost,生产用 HTTPS | +| Celery 任务不执行 | Redis 未启动或地址错误 | 检查 `CELERY_BROKER_URL` 和 Redis | +| 限流不生效 | Redis 未配置 | 检查 `REDIS_URL` | +| 短信发送失败 | 火山配置缺失或 `SMS_MOCK=true` | 填入 `VOLC_SMS_*` 变量并设 `SMS_MOCK=false` | +| 视频封面无法生成 | FFmpeg 未安装 | 安装 FFmpeg 或设置 `FFMPEG_BIN` | +| HTTPS 请求支付宝/火山失败 | 缺少 CA 证书 | 安装 `ca-certificates` | +| 数据库迁移失败 | Model 定义与迁移不一致 | 重新生成迁移文件后执行 | \ No newline at end of file