# AI 3D 模型生成功能开发文档 ## 1. 功能概述 ### 1.1 功能描述 用户可以通过文字描述或上传图片,利用 AI 技术生成 3D 模型(GLB/GLTF 格式)。类似腾讯混元 3D (https://3d.hunyuan.tencent.com/) 的功能。 ### 1.2 核心特性 - **文字生成 3D**:用户输入一段文字描述,AI 生成对应的 3D 模型 - **图片生成 3D**:用户上传一张图片,AI 根据图片生成 3D 模型 - **任务历史**:保存用户的生成历史记录,支持查看和删除 - **3D 预览**:生成完成后可在线预览 3D 模型 ### 1.3 技术选型 - **开发阶段**:使用 Mock 数据模拟 AI 生成过程 - **生产阶段**:可对接腾讯混元 3D API 或 Meshy AI API - **入口方式**:独立页面 `/ai-3d` --- ## 2. 数据库设计 ### 2.1 AI3DTask 表结构 ```prisma model AI3DTask { id Int @id @default(autoincrement()) tenantId Int @map("tenant_id") userId Int @map("user_id") inputType String @map("input_type") // text | image inputContent String @db.Text @map("input_content") // 文字描述或图片URL status String @default("pending") // pending | processing | completed | failed | timeout resultUrl String? @map("result_url") // 生成的3D模型URL previewUrl String? @map("preview_url") // 预览图URL errorMessage String? @map("error_message") // 失败时的错误信息 externalTaskId String? @map("external_task_id") // 外部AI服务的任务ID retryCount Int @default(0) @map("retry_count") // 已重试次数 createTime DateTime @default(now()) @map("create_time") completeTime DateTime? @map("complete_time") tenant Tenant @relation(fields: [tenantId], references: [id]) user User @relation(fields: [userId], references: [id]) @@index([userId]) @@index([tenantId]) @@index([status]) @@index([createTime]) @@map("t_ai_3d_task") } ``` ### 2.2 字段说明 | 字段 | 类型 | 说明 | |------|------|------| | id | Int | 主键,自增 | | tenantId | Int | 租户ID,用于多租户隔离 | | userId | Int | 用户ID,任务归属用户 | | inputType | String | 输入类型:`text`(文字) 或 `image`(图片) | | inputContent | Text | 输入内容:文字描述或图片URL | | status | String | 任务状态:pending/processing/completed/failed/timeout | | resultUrl | String | 生成的3D模型文件URL | | previewUrl | String | 模型预览图URL | | errorMessage | String | 失败时的错误信息 | | externalTaskId | String | 外部AI服务的任务ID,用于查询状态 | | retryCount | Int | 已重试次数,默认0 | | createTime | DateTime | 创建时间 | | completeTime | DateTime | 完成时间 | ### 2.3 状态流转 ``` pending (待处理) ↓ processing (处理中) ↓ completed (已完成) / failed (失败) / timeout (超时) ↓ [用户重试] → pending ``` ### 2.4 配置常量 | 常量 | 默认值 | 说明 | |------|--------|------| | MAX_CONCURRENT_TASKS | 3 | 每用户最大并行任务数 | | TASK_TIMEOUT_MINUTES | 10 | 任务超时时间(分钟) | | MAX_RETRY_COUNT | 3 | 最大重试次数 | | CLEANUP_INTERVAL_MINUTES | 5 | 超时清理任务执行间隔 | | OLD_TASK_RETENTION_DAYS | 30 | 失败任务保留天数 | --- ## 3. API 接口设计 ### 3.1 创建生成任务 **POST** `/api/ai-3d/generate` **Request Body:** ```json { "inputType": "text", // "text" | "image" "inputContent": "一只可爱的小猫" // 文字描述或图片URL } ``` **Response:** ```json { "code": 0, "message": "success", "data": { "id": 1, "inputType": "text", "inputContent": "一只可爱的小猫", "status": "pending", "createTime": "2024-01-13T10:00:00.000Z" } } ``` ### 3.2 获取任务列表 **GET** `/api/ai-3d/tasks` **Query Parameters:** | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | page | number | 否 | 页码,默认1 | | pageSize | number | 否 | 每页数量,默认10 | | status | string | 否 | 状态筛选 | **Response:** ```json { "code": 0, "message": "success", "data": { "list": [ { "id": 1, "inputType": "text", "inputContent": "一只可爱的小猫", "status": "completed", "resultUrl": "/uploads/ai-3d/xxx.glb", "previewUrl": "/uploads/ai-3d/xxx-preview.png", "createTime": "2024-01-13T10:00:00.000Z", "completeTime": "2024-01-13T10:01:30.000Z" } ], "total": 1, "page": 1, "pageSize": 10 } } ``` ### 3.3 获取任务详情 **GET** `/api/ai-3d/tasks/:id` **Response:** ```json { "code": 0, "message": "success", "data": { "id": 1, "inputType": "text", "inputContent": "一只可爱的小猫", "status": "completed", "resultUrl": "/uploads/ai-3d/xxx.glb", "previewUrl": "/uploads/ai-3d/xxx-preview.png", "errorMessage": null, "createTime": "2024-01-13T10:00:00.000Z", "completeTime": "2024-01-13T10:01:30.000Z" } } ``` ### 3.4 删除任务 **DELETE** `/api/ai-3d/tasks/:id` **Response:** ```json { "code": 0, "message": "success", "data": null } ``` ### 3.5 重试任务 **POST** `/api/ai-3d/tasks/:id/retry` **说明:** 仅支持状态为 `failed` 或 `timeout` 的任务,每个任务最多重试 3 次 **Response (成功):** ```json { "code": 0, "message": "success", "data": { "id": 1, "inputType": "text", "inputContent": "一只可爱的小猫", "status": "processing", "retryCount": 1, "createTime": "2024-01-13T10:00:00.000Z" } } ``` **Response (失败 - 达到重试上限):** ```json { "code": 400, "message": "已达到最大重试次数 3 次,请创建新任务" } ``` **Response (失败 - 并发限制):** ```json { "code": 400, "message": "您当前有 3 个任务正在处理中,请等待完成后再重试" } ``` --- ## 4. 后端模块设计 ### 4.1 目录结构 ``` backend/src/ai-3d/ ├── ai-3d.module.ts # 模块定义 ├── ai-3d.controller.ts # 控制器 ├── ai-3d.service.ts # 业务服务 ├── ai-3d-cleanup.service.ts # 超时清理定时任务 ├── dto/ │ ├── create-task.dto.ts # 创建任务DTO │ └── query-task.dto.ts # 查询任务DTO └── providers/ ├── ai-3d-provider.interface.ts # AI服务接口 ├── mock.provider.ts # Mock实现 ├── hunyuan.provider.ts # 腾讯混元实现(预留) └── meshy.provider.ts # Meshy AI实现(预留) ``` ### 4.2 AI Provider 接口 ```typescript // ai-3d-provider.interface.ts export interface AI3DGenerateResult { taskId: string; // 外部任务ID status: 'pending' | 'processing' | 'completed' | 'failed'; resultUrl?: string; // 3D模型URL previewUrl?: string; // 预览图URL errorMessage?: string; // 错误信息 } export interface AI3DProvider { /** * 提交生成任务 */ submitTask(inputType: 'text' | 'image', inputContent: string): Promise; /** * 查询任务状态 */ queryTask(taskId: string): Promise; } ``` ### 4.3 Mock Provider 实现逻辑 ```typescript // mock.provider.ts @Injectable() export class MockAI3DProvider implements AI3DProvider { private tasks = new Map(); async submitTask(inputType: string, inputContent: string): Promise { const taskId = generateUUID(); this.tasks.set(taskId, { status: 'processing', startTime: Date.now(), }); // 模拟5-10秒后完成 setTimeout(() => { this.tasks.set(taskId, { status: 'completed', resultUrl: '/mock/sample-model.glb', previewUrl: '/mock/sample-preview.png', }); }, 5000 + Math.random() * 5000); return taskId; } async queryTask(taskId: string): Promise { const task = this.tasks.get(taskId); if (!task) { throw new NotFoundException('Task not found'); } return task; } } ``` ### 4.4 Service 核心逻辑 ```typescript // ai-3d.service.ts @Injectable() export class AI3DService { constructor( private prisma: PrismaService, private ai3dProvider: AI3DProvider, ) {} async createTask(userId: number, tenantId: number, dto: CreateTaskDto) { // 1. 创建数据库记录 const task = await this.prisma.aI3DTask.create({ data: { userId, tenantId, inputType: dto.inputType, inputContent: dto.inputContent, status: 'pending', }, }); // 2. 提交到AI服务 const externalTaskId = await this.ai3dProvider.submitTask( dto.inputType, dto.inputContent, ); // 3. 更新状态为处理中 await this.prisma.aI3DTask.update({ where: { id: task.id }, data: { status: 'processing' }, }); // 4. 启动轮询检查任务状态(或使用消息队列) this.pollTaskStatus(task.id, externalTaskId); return task; } async getTasks(userId: number, query: QueryTaskDto) { const { page = 1, pageSize = 10, status } = query; const where = { userId, ...(status && { status }), }; const [list, total] = await Promise.all([ this.prisma.aI3DTask.findMany({ where, skip: (page - 1) * pageSize, take: pageSize, orderBy: { createTime: 'desc' }, }), this.prisma.aI3DTask.count({ where }), ]); return { list, total, page, pageSize }; } async getTask(userId: number, id: number) { const task = await this.prisma.aI3DTask.findFirst({ where: { id, userId }, }); if (!task) { throw new NotFoundException('任务不存在'); } return task; } async deleteTask(userId: number, id: number) { const task = await this.getTask(userId, id); await this.prisma.aI3DTask.delete({ where: { id: task.id }, }); return null; } private async pollTaskStatus(taskId: number, externalTaskId: string) { // 轮询检查任务状态,完成后更新数据库 const checkStatus = async () => { try { const result = await this.ai3dProvider.queryTask(externalTaskId); if (result.status === 'completed' || result.status === 'failed') { await this.prisma.aI3DTask.update({ where: { id: taskId }, data: { status: result.status, resultUrl: result.resultUrl, previewUrl: result.previewUrl, errorMessage: result.errorMessage, completeTime: new Date(), }, }); } else { // 继续轮询 setTimeout(checkStatus, 2000); } } catch (error) { console.error('Poll task status error:', error); setTimeout(checkStatus, 5000); } }; setTimeout(checkStatus, 2000); } } ``` --- ## 5. 前端页面设计 ### 5.1 目录结构 ``` frontend/src/ ├── views/ │ └── ai-3d/ │ ├── Index.vue # 主页面 │ └── components/ │ ├── GenerateForm.vue # 生成表单 │ ├── TaskList.vue # 任务列表 │ └── TaskCard.vue # 任务卡片 ├── api/ │ └── ai-3d.ts # API接口 └── router/ └── index.ts # 添加路由 ``` ### 5.2 页面布局 ``` ┌─────────────────────────────────────────────────────┐ │ AI 3D 模型生成 │ ├─────────────────────────────────────────────────────┤ │ ┌─────────────────────────────────────────────┐ │ │ │ 生成方式: ○ 文字描述 ○ 图片上传 │ │ │ │ │ │ │ │ [输入框/上传区域] │ │ │ │ │ │ │ │ [开始生成] │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ 生成历史 │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ 预览图 │ │ 预览图 │ │ 预览图 │ │ 预览图 │ │ │ │ │ │ │ │ │ │ │ │ │ │ 已完成 │ │ 处理中 │ │ 已完成 │ │ 失败 │ │ │ │ [查看] │ │ [...] │ │ [查看] │ │ [重试] │ │ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │ │ │ [加载更多] │ └─────────────────────────────────────────────────────┘ ``` ### 5.3 API 接口封装 ```typescript // api/ai-3d.ts import request from '@/utils/request' export interface CreateTaskParams { inputType: 'text' | 'image' inputContent: string } export interface QueryTaskParams { page?: number pageSize?: number status?: string } export interface AI3DTask { id: number inputType: 'text' | 'image' inputContent: string status: 'pending' | 'processing' | 'completed' | 'failed' | 'timeout' resultUrl?: string previewUrl?: string errorMessage?: string retryCount: number createTime: string completeTime?: string } // 创建生成任务 export function createAI3DTask(data: CreateTaskParams) { return request.post('/ai-3d/generate', data) } // 获取任务列表 export function getAI3DTasks(params: QueryTaskParams) { return request.get<{ list: AI3DTask[]; total: number }>('/ai-3d/tasks', { params }) } // 获取任务详情 export function getAI3DTask(id: number) { return request.get(`/ai-3d/tasks/${id}`) } // 删除任务 export function deleteAI3DTask(id: number) { return request.delete(`/ai-3d/tasks/${id}`) } // 重试任务 export function retryAI3DTask(id: number) { return request.post(`/ai-3d/tasks/${id}/retry`) } ``` ### 5.4 路由配置 ```typescript // router/index.ts - 添加到 baseRoutes 的 Main children 中 { path: "ai-3d", name: "AI3DGenerate", component: () => import("@/views/ai-3d/Index.vue"), meta: { title: "AI 3D生成", requiresAuth: true, }, }, ``` --- ## 6. 开发步骤 ### 第一阶段:数据库与后端基础 1. **添加 Prisma Schema** - 在 `schema.prisma` 中添加 `AI3DTask` 模型 - 在 `Tenant` 和 `User` 模型中添加关联 2. **运行数据库迁移** ```bash cd backend npx prisma migrate dev --name add_ai_3d_task ``` 3. **创建后端模块** - 创建 `ai-3d` 目录结构 - 实现 Mock Provider - 实现 Service 和 Controller - 注册模块到 AppModule ### 第二阶段:前端页面 4. **创建 API 接口文件** - 创建 `src/api/ai-3d.ts` 5. **创建页面组件** - 创建 `src/views/ai-3d/Index.vue` 主页面 - 实现生成表单 - 实现任务列表展示 6. **配置路由** - 在 `router/index.ts` 中添加路由 ### 第三阶段:功能完善 7. **状态轮询** - 实现前端任务状态轮询 - 任务完成时自动刷新 8. **3D 预览集成** - 复用现有的 ModelViewer 组件 9. **测试与优化** - 测试完整流程 - 优化用户体验 --- ## 7. 后续扩展 ### 7.1 对接真实 AI 服务 当需要对接真实 AI 服务时,只需: 1. 实现对应的 Provider(如 `HunyuanProvider`) 2. 通过环境变量切换 Provider 3. 无需修改 Service 层代码 ```typescript // 通过环境变量选择 Provider const provider = process.env.AI_3D_PROVIDER || 'mock'; switch (provider) { case 'hunyuan': return new HunyuanProvider(config); case 'meshy': return new MeshyProvider(config); default: return new MockProvider(); } ``` ### 7.2 腾讯混元 3D API 参考 - 官网:https://3d.hunyuan.tencent.com/ - 支持文字生成3D、图片生成3D - 需要腾讯云账号和 API 密钥 ### 7.3 Meshy AI API 参考 - 官网:https://www.meshy.ai/ - 提供 REST API - 支持 text-to-3d、image-to-3d - 文档:https://docs.meshy.ai/ --- ## 8. 并发控制与任务超时 ### 8.1 并发控制 #### 8.1.1 设计目标 - 限制每个用户同时进行的任务数量,避免资源滥用 - 保证系统稳定性,防止单用户占用过多资源 - 提供友好的错误提示 #### 8.1.2 实现方案 ```typescript // ai-3d.service.ts import { BadRequestException } from '@nestjs/common'; // 配置常量 const MAX_CONCURRENT_TASKS = 3; // 每用户最大并行任务数 @Injectable() export class AI3DService { async createTask(userId: number, tenantId: number, dto: CreateTaskDto) { // 1. 检查用户当前进行中的任务数量 const activeTaskCount = await this.prisma.aI3DTask.count({ where: { userId, status: { in: ['pending', 'processing'] }, }, }); if (activeTaskCount >= MAX_CONCURRENT_TASKS) { throw new BadRequestException( `您当前有 ${activeTaskCount} 个任务正在处理中,最多同时处理 ${MAX_CONCURRENT_TASKS} 个任务,请等待完成后再提交` ); } // 2. 创建任务... const task = await this.prisma.aI3DTask.create({ data: { userId, tenantId, inputType: dto.inputType, inputContent: dto.inputContent, status: 'pending', }, }); // 3. 提交到AI服务并更新状态 try { const externalTaskId = await this.ai3dProvider.submitTask( dto.inputType, dto.inputContent, ); await this.prisma.aI3DTask.update({ where: { id: task.id }, data: { status: 'processing', externalTaskId, }, }); // 4. 启动轮询检查任务状态 this.pollTaskStatus(task.id, externalTaskId, Date.now()); return task; } catch (error) { // 提交失败,更新状态 await this.prisma.aI3DTask.update({ where: { id: task.id }, data: { status: 'failed', errorMessage: error.message || 'AI服务提交失败', completeTime: new Date(), }, }); throw error; } } } ``` #### 8.1.3 前端处理 ```typescript // 前端提交任务时处理并发限制错误 const handleGenerate = async () => { try { loading.value = true; await createAI3DTask({ inputType: inputType.value, inputContent: inputContent.value, }); message.success('任务已提交'); fetchTasks(); // 刷新列表 } catch (error: any) { if (error.response?.status === 400) { // 并发限制错误,显示友好提示 message.warning(error.response.data.message); } else { message.error('提交失败,请重试'); } } finally { loading.value = false; } }; ``` --- ### 8.2 任务超时处理 #### 8.2.1 设计目标 - 防止任务长时间卡在处理中状态 - 自动标记超时任务,释放用户并发配额 - 支持用户重试超时任务 #### 8.2.2 方案A:轮询时检查超时(适用于 Mock 开发阶段) ```typescript // ai-3d.service.ts const TASK_TIMEOUT_MS = 10 * 60 * 1000; // 10分钟超时 private async pollTaskStatus( taskId: number, externalTaskId: string, startTime: number ) { const checkStatus = async () => { // 1. 检查是否超时 if (Date.now() - startTime > TASK_TIMEOUT_MS) { await this.prisma.aI3DTask.update({ where: { id: taskId }, data: { status: 'timeout', errorMessage: '任务处理超时,请重试', completeTime: new Date(), }, }); console.log(`Task ${taskId} timeout after ${TASK_TIMEOUT_MS}ms`); return; // 停止轮询 } // 2. 查询外部任务状态 try { const result = await this.ai3dProvider.queryTask(externalTaskId); if (result.status === 'completed' || result.status === 'failed') { await this.prisma.aI3DTask.update({ where: { id: taskId }, data: { status: result.status, resultUrl: result.resultUrl, previewUrl: result.previewUrl, errorMessage: result.errorMessage, completeTime: new Date(), }, }); } else { // 继续轮询,每2秒检查一次 setTimeout(checkStatus, 2000); } } catch (error) { console.error(`Poll task ${taskId} error:`, error); // 出错后延长轮询间隔,每5秒重试 setTimeout(checkStatus, 5000); } }; // 首次检查延迟2秒 setTimeout(checkStatus, 2000); } ``` #### 8.2.3 方案B:定时任务批量清理(推荐生产环境) ```typescript // ai-3d-cleanup.service.ts import { Injectable, Logger } from '@nestjs/common'; import { Cron, CronExpression } from '@nestjs/schedule'; import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class AI3DCleanupService { private readonly logger = new Logger(AI3DCleanupService.name); constructor(private prisma: PrismaService) {} /** * 每5分钟检查并处理超时任务 */ @Cron(CronExpression.EVERY_5_MINUTES) async handleTimeoutTasks() { const TIMEOUT_MINUTES = 10; const timeoutThreshold = new Date(Date.now() - TIMEOUT_MINUTES * 60 * 1000); const result = await this.prisma.aI3DTask.updateMany({ where: { status: { in: ['pending', 'processing'] }, createTime: { lt: timeoutThreshold }, }, data: { status: 'timeout', errorMessage: '任务处理超时,请重试', completeTime: new Date(), }, }); if (result.count > 0) { this.logger.log(`清理了 ${result.count} 个超时任务`); } } /** * 每天凌晨2点清理30天前的失败/超时任务记录 */ @Cron('0 2 * * *') async cleanupOldTasks() { const RETENTION_DAYS = 30; const retentionThreshold = new Date( Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000 ); const result = await this.prisma.aI3DTask.deleteMany({ where: { status: { in: ['failed', 'timeout'] }, createTime: { lt: retentionThreshold }, }, }); if (result.count > 0) { this.logger.log(`清理了 ${result.count} 个过期失败任务`); } } } ``` #### 8.2.4 模块注册 ```typescript // ai-3d.module.ts import { Module } from '@nestjs/common'; import { ScheduleModule } from '@nestjs/schedule'; import { AI3DController } from './ai-3d.controller'; import { AI3DService } from './ai-3d.service'; import { AI3DCleanupService } from './ai-3d-cleanup.service'; import { MockAI3DProvider } from './providers/mock.provider'; @Module({ imports: [ScheduleModule.forRoot()], controllers: [AI3DController], providers: [ AI3DService, AI3DCleanupService, { provide: 'AI3D_PROVIDER', useClass: MockAI3DProvider, }, ], }) export class AI3DModule {} ``` --- ### 8.3 重试机制 #### 8.3.1 重试API **POST** `/api/ai-3d/tasks/:id/retry` ```typescript // ai-3d.controller.ts @Post('tasks/:id/retry') async retryTask(@Param('id') id: number, @Request() req) { return this.ai3dService.retryTask(req.user.id, id); } ``` #### 8.3.2 重试逻辑 ```typescript // ai-3d.service.ts const MAX_RETRY_COUNT = 3; async retryTask(userId: number, taskId: number) { const task = await this.prisma.aI3DTask.findFirst({ where: { id: taskId, userId }, }); if (!task) { throw new NotFoundException('任务不存在'); } // 只有失败或超时的任务可以重试 if (!['failed', 'timeout'].includes(task.status)) { throw new BadRequestException('只有失败或超时的任务可以重试'); } // 检查重试次数 if (task.retryCount >= MAX_RETRY_COUNT) { throw new BadRequestException( `已达到最大重试次数 ${MAX_RETRY_COUNT} 次,请创建新任务` ); } // 检查并发限制 const activeTaskCount = await this.prisma.aI3DTask.count({ where: { userId, status: { in: ['pending', 'processing'] }, }, }); if (activeTaskCount >= MAX_CONCURRENT_TASKS) { throw new BadRequestException( `您当前有 ${activeTaskCount} 个任务正在处理中,请等待完成后再重试` ); } // 重置任务状态 await this.prisma.aI3DTask.update({ where: { id: taskId }, data: { status: 'pending', errorMessage: null, completeTime: null, retryCount: { increment: 1 }, }, }); // 重新提交任务 const externalTaskId = await this.ai3dProvider.submitTask( task.inputType, task.inputContent, ); await this.prisma.aI3DTask.update({ where: { id: taskId }, data: { status: 'processing', externalTaskId, }, }); this.pollTaskStatus(taskId, externalTaskId, Date.now()); return this.getTask(userId, taskId); } ``` #### 8.3.3 前端重试按钮 ```vue ``` --- ### 8.4 方案对比与推荐 | 方案 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 方案A(轮询检查) | 实现简单,无需额外依赖 | 服务重启后轮询丢失 | Mock 开发、小规模部署 | | 方案B(定时任务) | 可靠性高,服务重启不影响 | 需要引入 @nestjs/schedule | 生产环境 | **推荐策略**: - 开发阶段使用方案A,快速验证功能 - 生产环境使用方案B + 方案A 结合,双重保障 --- ## 9. 注意事项 1. **数据归属**:任务数据通过 `userId` 关联用户,确保用户只能访问自己的数据 2. **多租户隔离**:保留 `tenantId` 字段,便于管理员按租户统计 3. **文件存储**:生成的 3D 模型文件建议存储到 OSS,已实现 OSS 集成 4. **并发控制**:每用户最多 3 个并行任务,可通过配置调整 5. **任务超时**:10 分钟未完成自动标记为超时,支持重试 6. **重试限制**:单任务最多重试 3 次,超过后需创建新任务 7. **数据清理**:30 天前的失败/超时任务自动清理