library-picturebook-activity/docs/ai-3d-generation.md
2026-01-13 11:11:49 +08:00

27 KiB
Raw Permalink Blame History

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 表结构

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:

{
  "inputType": "text",           // "text" | "image"
  "inputContent": "一只可爱的小猫"  // 文字描述或图片URL
}

Response:

{
  "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:

{
  "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:

{
  "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:

{
  "code": 0,
  "message": "success",
  "data": null
}

3.5 重试任务

POST /api/ai-3d/tasks/:id/retry

说明: 仅支持状态为 failedtimeout 的任务,每个任务最多重试 3 次

Response (成功):

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 1,
    "inputType": "text",
    "inputContent": "一只可爱的小猫",
    "status": "processing",
    "retryCount": 1,
    "createTime": "2024-01-13T10:00:00.000Z"
  }
}

Response (失败 - 达到重试上限):

{
  "code": 400,
  "message": "已达到最大重试次数 3 次,请创建新任务"
}

Response (失败 - 并发限制):

{
  "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 接口

// 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 实现逻辑

// 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 核心逻辑

// 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 接口封装

// 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 路由配置

// 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 模型
    • TenantUser 模型中添加关联
  2. 运行数据库迁移

    cd backend
    npx prisma migrate dev --name add_ai_3d_task
    
  3. 创建后端模块

    • 创建 ai-3d 目录结构
    • 实现 Mock Provider
    • 实现 Service 和 Controller
    • 注册模块到 AppModule

第二阶段:前端页面

  1. 创建 API 接口文件

    • 创建 src/api/ai-3d.ts
  2. 创建页面组件

    • 创建 src/views/ai-3d/Index.vue 主页面
    • 实现生成表单
    • 实现任务列表展示
  3. 配置路由

    • router/index.ts 中添加路由

第三阶段:功能完善

  1. 状态轮询

    • 实现前端任务状态轮询
    • 任务完成时自动刷新
  2. 3D 预览集成

    • 复用现有的 ModelViewer 组件
  3. 测试与优化

    • 测试完整流程
    • 优化用户体验

7. 后续扩展

7.1 对接真实 AI 服务

当需要对接真实 AI 服务时,只需:

  1. 实现对应的 ProviderHunyuanProvider
  2. 通过环境变量切换 Provider
  3. 无需修改 Service 层代码
// 通过环境变量选择 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 参考

7.3 Meshy AI API 参考


8. 并发控制与任务超时

8.1 并发控制

8.1.1 设计目标

  • 限制每个用户同时进行的任务数量,避免资源滥用
  • 保证系统稳定性,防止单用户占用过多资源
  • 提供友好的错误提示

8.1.2 实现方案

// 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 前端处理

// 前端提交任务时处理并发限制错误
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 开发阶段)

// 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定时任务批量清理推荐生产环境

// 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 模块注册

// 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

// 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 重试逻辑

// 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 前端重试按钮

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