library-picturebook-activity/docs/ai-3d-generation.md

1045 lines
27 KiB
Markdown
Raw Normal View History

2026-01-13 11:11:49 +08:00
# 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 天前的失败/超时任务自动清理