You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

13 KiB

前后端接口对接指南 - 创建项目功能

最后更新: 2026-02-06
维护者: Jointo 开发团队


当前状态

创建项目功能已完整实现,前后端接口对接完成。

  • 后端接口: POST /api/v1/projects
  • 前端 API 服务: client/src/services/api/projects.ts
  • React Query Hook: useCreateProject()
  • UI 组件: CreateProjectModal.tsx
  • 类型定义: TypeScript 类型已对齐

📋 架构概览

┌─────────────────────────────────────────────────────────────┐
│                    前端架构(React)                          │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  UI 层:CreateProjectModal.tsx                               │
│    ↓                                                          │
│  Hook 层:useCreateProject() (TanStack Query)                │
│    ↓                                                          │
│  API 层:projectApi.create() (Axios)                         │
│    ↓                                                          │
│  HTTP:POST /api/v1/projects                                 │
│                                                               │
└───────────────────────────────┬─────────────────────────────┘
                                │
                                ↓
┌─────────────────────────────────────────────────────────────┐
│                    后端架构(FastAPI)                        │
├─────────────────────────────────────────────────────────────┤
│                                                               │
│  API 层:projects.py → create_project()                      │
│    ↓                                                          │
│  Service 层:ProjectService.create_project()                 │
│    ↓                                                          │
│  Repository 层:ProjectRepository.create()                   │
│    ↓                                                          │
│  Database:PostgreSQL                                         │
│                                                               │
└─────────────────────────────────────────────────────────────┘

🔌 接口详情

后端接口

文件: server/app/api/v1/projects.py

@router.post("", response_model=ApiResponse[ProjectResponse], summary="创建项目")
async def create_project(
    project_data: ProjectCreate,
    current_user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session)
):
    """
    创建项目
    
    - 支持个人项目(mine)和协作项目(collab)
    - 可指定所属文件夹
    - 自动检查同文件夹下名称唯一性
    """

请求体(ProjectCreate):

{
  "name": str,                  # 项目名称(必填)
  "description": Optional[str], # 项目描述
  "type": "mine" | "collab",    # 项目类型(默认 "mine")
  "folderId": Optional[UUID],   # 所属文件夹ID
  "contentType": Optional[str], # 内容类型(ad/movie/series等)
  "aspectRatio": Optional[str], # 画幅比例(16:9/9:16等)
  "plannedDuration": Optional[int], # 计划时长(秒)
  "styleAndCharacters": Optional[str], # 风格和角色
  "settings": Optional[dict]    # 项目设置
}

响应格式(ApiResponse):

{
  "success": true,
  "code": 200,
  "message": "Success",
  "data": {
    "id": "uuid",
    "name": "项目名称",
    "description": "项目描述",
    "type": "mine",
    "ownerId": "uuid",
    "folderId": "uuid",
    "displayOrder": 0,
    "thumbnailUrl": null,
    "contentType": "ad",
    "aspectRatio": "16:9",
    "plannedDuration": 60,
    "actualDuration": null,
    "styleAndCharacters": "风格描述",
    "settings": {},
    "status": "active",
    "createdAt": "2026-02-06T12:00:00Z",
    "updatedAt": "2026-02-06T12:00:00Z",
    "trashedAt": null
  },
  "timestamp": "2026-02-06T12:00:00Z"
}

前端 API 服务

文件: client/src/services/api/projects.ts

/**
 * 创建项目
 * POST /api/v1/projects
 */
async create(data: CreateProjectDto): Promise<Project> {
  return apiClient.post('/projects', data) as Promise<Project>;
}

TypeScript 类型定义:

// client/src/types/project.ts

export interface CreateProjectDto {
  name: string;                      // 项目名称
  description?: string;              // 项目描述
  type?: ProjectType;                // 项目类型(1=mine, 2=collab)
  folderId?: string;                 // 所属文件夹ID
  contentType?: ProjectContentType;  // 内容类型
  aspectRatio?: AspectRatioType;     // 画幅比例
  plannedDuration?: number;          // 计划时长(秒)
  styleAndCharacters?: string;       // 风格和角色
  settings?: Partial<ProjectSettings>; // 项目设置
}

export type ProjectContentType = 
  | 'ad'      // 广告片
  | 'movie'   // 电影
  | 'series'  // 剧集
  | 'anime'   // 动画
  | 'short'   // 短视频
  | 'concept' // 概念片

export type AspectRatioType = 
  | '16:9' | '9:16' | '4:3' | '21:9' | '1:1' | '2.35:1' | '2.39:1'

React Query Hook

文件: client/src/hooks/api/useProjects.ts

/**
 * 创建项目
 */
export function useCreateProject() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: CreateProjectDto) => projectApi.create(data),
    onSuccess: () => {
      // 失效所有项目列表查询
      queryClient.invalidateQueries({ queryKey: projectKeys.lists() });
    },
  });
}

功能特性:

  • 自动处理 loading 状态
  • 自动处理错误
  • 成功后自动刷新项目列表
  • 支持乐观更新(可扩展)

🎯 使用示例

1. 在组件中使用

import { useCreateProject } from '@/hooks/api/useProjects';
import { useToast } from '@/hooks/use-toast';

function MyComponent() {
  const createProject = useCreateProject();
  const { toast } = useToast();

  const handleSubmit = async (formData: any) => {
    try {
      const newProject = await createProject.mutateAsync({
        name: formData.name,
        contentType: formData.projectType, // ad/movie/series等
        aspectRatio: formData.aspectRatio, // 16:9等
        plannedDuration: formData.duration, // 秒
        styleAndCharacters: formData.style,
        folderId: formData.folderId, // 可选
      });

      toast({
        title: '项目已创建',
        description: `项目 "${newProject.name}" 已成功创建`,
      });

      // 导航到新项目
      navigate(`/project/${newProject.id}`);
    } catch (error) {
      toast({
        title: '创建失败',
        description: error instanceof Error ? error.message : '创建失败,请重试',
        variant: 'destructive',
      });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* 表单字段 */}
      <button 
        type="submit" 
        disabled={createProject.isPending}
      >
        {createProject.isPending ? '创建中...' : '创建项目'}
      </button>
    </form>
  );
}

2. 完整示例:CreateProjectModal

参考文件:client/src/components/features/project/CreateProjectModal.tsx

const createProject = useCreateProject();

const onSubmit = async (data: CreateProjectForm) => {
  try {
    const newProject = await createProject.mutateAsync({
      name: data.name,
      folderId: data.folderId?.startsWith('virtual-') ? undefined : data.folderId,
      contentType: data.projectType,
      aspectRatio: data.aspectRatio,
      plannedDuration: durationInSeconds,
      styleAndCharacters: data.styleAndRoles,
    });

    toast({ title: '项目已创建' });
    navigate(`/project/${newProject.id}`);
  } catch (error) {
    toast({ 
      title: '创建失败', 
      description: error.message,
      variant: 'destructive' 
    });
  }
};

🧪 测试联调

方法 1:使用现有 UI(推荐)

  1. 启动前后端服务:

    # 终端 1:启动后端
    cd server
    uvicorn app.main:app --reload --port 8000
    
    # 终端 2:启动前端
    cd client
    npm run dev
    
  2. 手动测试:

    • 访问 http://localhost:5173
    • 点击"创建项目"按钮
    • 填写表单字段
    • 点击"创建"按钮
    • 观察网络请求和响应
  3. 检查点:

    • 请求发送到 POST /api/v1/projects
    • 请求头包含 Authorization: Bearer <token>
    • 响应格式为 { success: true, data: {...} }
    • 创建成功后跳转到项目详情页
    • 项目列表自动刷新

方法 2:使用 Playwright 自动化测试

import { test, expect } from '@playwright/test';

test('创建项目功能', async ({ page }) => {
  // 1. 登录
  await page.goto('http://localhost:5173/login');
  await page.fill('input[name="email"]', 'test@example.com');
  await page.fill('input[name="password"]', 'password');
  await page.click('button[type="submit"]');

  // 2. 打开创建项目弹窗
  await page.click('button:has-text("创建项目")');

  // 3. 填写表单
  await page.fill('input[name="name"]', '测试项目');
  await page.selectOption('select[name="projectType"]', 'ad');
  await page.selectOption('select[name="aspectRatio"]', '16:9');
  await page.fill('input[name="duration"]', '60');

  // 4. 提交
  await page.click('button[type="submit"]');

  // 5. 验证
  await expect(page).toHaveURL(/\/project\/[a-f0-9-]+/);
  await expect(page.locator('h1')).toContainText('测试项目');
});

方法 3:使用 API 测试工具

Postman / Insomnia:

POST http://localhost:8000/api/v1/projects
Authorization: Bearer <your_token>
Content-Type: application/json

{
  "name": "测试项目",
  "contentType": "ad",
  "aspectRatio": "16:9",
  "plannedDuration": 60,
  "styleAndCharacters": "现代简约风格"
}

cURL:

curl -X POST http://localhost:8000/api/v1/projects \
  -H "Authorization: Bearer <your_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "测试项目",
    "contentType": "ad",
    "aspectRatio": "16:9",
    "plannedDuration": 60
  }'

🔍 故障排查

问题 1:401 Unauthorized

原因: Token 未传递或已过期

解决:

// 检查 localStorage 中的 token
const token = localStorage.getItem('token');
console.log('Token:', token);

// 手动设置 token(测试用)
localStorage.setItem('token', 'your_valid_token');

问题 2:400 Bad Request

原因: 请求数据格式错误

解决:

  • 检查必填字段:name 不能为空
  • 检查字段类型:plannedDuration 必须是数字
  • 检查枚举值:contentType 必须是 ad/movie/series 等

问题 3:网络错误

原因: 后端服务未启动或 CORS 配置错误

解决:

# 检查后端是否运行
curl http://localhost:8000/health

# 检查环境变量
echo $VITE_API_BASE_URL
# 应该输出:http://localhost:8000/api/v1

问题 4:响应数据类型不匹配

原因: 前后端字段命名不一致

检查:

  • 后端使用 snake_casefolder_id, content_type
  • 前端使用 camelCasefolderId, contentType
  • 响应拦截器已自动转换(apiClient.interceptors.response

📝 快速提示词

创建新功能时使用

为前端 [功能名] 功能对接后端 API

后端接口:
- 路径:[METHOD] /api/v1/[path]
- Schema:[SchemaName]
- 响应:ApiResponse[ResponseType]

前端需求:
1. 在 client/src/services/api/[module].ts 中添加 API 调用
2. 参考后端 Schema 定义 TypeScript 类型
3. 创建 React Query Hook
4. 处理成功/失败响应
5. 添加 loading 状态

参考:
- 后端接口:server/app/api/v1/projects.py
- 前端实现:client/src/services/api/projects.ts
- Hook 实现:client/src/hooks/api/useProjects.ts

测试联调时使用

使用 Playwright 测试 [功能名] 的前后端联调

测试流程:
1. 启动前后端服务
2. 登录系统
3. 执行功能操作([具体步骤])
4. 验证响应数据
5. 验证 UI 更新

检查点:
- 请求路径正确
- 请求头包含 Token
- 响应格式符合 ApiResponse
- UI 状态正确更新

📚 相关文档


最后更新: 2026-02-06
维护者: Jointo 开发团队