# 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<string>;

  /**
   * 查询任务状态
   */
  queryTask(taskId: string): Promise<AI3DGenerateResult>;
}
```

### 4.3 Mock Provider 实现逻辑

```typescript
// mock.provider.ts
@Injectable()
export class MockAI3DProvider implements AI3DProvider {
  private tasks = new Map<string, MockTask>();

  async submitTask(inputType: string, inputContent: string): Promise<string> {
    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<AI3DGenerateResult> {
    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<AI3DTask>('/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<AI3DTask>(`/ai-3d/tasks/${id}`)
}

// 删除任务
export function deleteAI3DTask(id: number) {
  return request.delete(`/ai-3d/tasks/${id}`)
}

// 重试任务
export function retryAI3DTask(id: number) {
  return request.post<AI3DTask>(`/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
<template>
  <a-card v-for="task in tasks" :key="task.id">
    <template v-if="['failed', 'timeout'].includes(task.status)">
      <a-button
        type="link"
        @click="handleRetry(task.id)"
        :disabled="task.retryCount >= 3"
      >
        {{ task.retryCount >= 3 ? '已达重试上限' : '重试' }}
        <span v-if="task.retryCount > 0">({{ task.retryCount }}/3)</span>
      </a-button>
    </template>
  </a-card>
</template>

<script setup>
const handleRetry = async (taskId: number) => {
  try {
    await retryAI3DTask(taskId);
    message.success('重试已提交');
    fetchTasks();
  } catch (error: any) {
    message.error(error.response?.data?.message || '重试失败');
  }
};
</script>
```

---

### 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 天前的失败/超时任务自动清理
