library-picturebook-activity/docs/legacy/ai-3d-generation.md
aid 418aa57ea8 Day4: 超管端设计优化 + UGC绘本创作社区P0实现
一、超管端设计优化
- 文档管理SOP体系建立,docs目录重组
- 统一用户管理:跨租户全局视角,合并用户管理+公众用户
- 活动监管全模块重构:全部活动(统计卡片+阶段筛选+SuperDetail详情页)、报名数据/作品数据/评审进度(两层合一扁平列表)、成果发布(去Tab+统计+隐藏写操作)
- 菜单精简:移除评委管理/评审规则/通知管理
- Bug修复:租户编辑丢失隐藏菜单、pageSize限制、主色统一

二、UGC绘本创作社区P0
- 数据库:10张新表(user_works/user_work_pages/work_tags等)
- 子女账号独立化:Child升级为独立User,家长切换+独立登录
- 用户作品库:CRUD+发布审核,8个API
- AI创作流程:提交→生成→保存到作品库,4个API
- 作品广场:首页改造为推荐流,标签+搜索+排序
- 内容审核(超管端):作品审核+作品管理+标签管理
- 活动联动:WorkSelector作品选择器
- 布局改造:底部5Tab(发现/创作/活动/作品库/我的)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:20:25 +08:00

1045 lines
28 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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