From 61599117dcef5e2f1788f82a0a2af9a2408dfe64 Mon Sep 17 00:00:00 2001 From: zhangxiaohua <827885272@qq.com> Date: Mon, 19 Jan 2026 16:33:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=B9=B6=E5=8F=91=E9=99=90?= =?UTF-8?q?=E5=88=B6=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/ai-3d/ai-3d.service.ts | 345 ++++++++++++------ frontend/src/api/ai-3d.ts | 2 + .../src/views/workbench/ai-3d/Generate.vue | 43 ++- .../src/views/workbench/ai-3d/History.vue | 6 +- frontend/src/views/workbench/ai-3d/Index.vue | 4 +- 5 files changed, 277 insertions(+), 123 deletions(-) diff --git a/backend/src/ai-3d/ai-3d.service.ts b/backend/src/ai-3d/ai-3d.service.ts index 73071f2..ed4ff8d 100644 --- a/backend/src/ai-3d/ai-3d.service.ts +++ b/backend/src/ai-3d/ai-3d.service.ts @@ -12,19 +12,141 @@ import { QueryTaskDto } from './dto/query-task.dto'; import { AI3DProvider, AI3D_PROVIDER } from './providers/ai-3d-provider.interface'; // 配置常量 -const MAX_CONCURRENT_TASKS = 3; // 每用户最大并行任务数 +const MAX_USER_TASKS = 3; // 每用户最大任务数(pending + processing) +const API_MAX_CONCURRENT = 3; // 混元API全局最大并发数(所有用户共享) const TASK_TIMEOUT_MS = 10 * 60 * 1000; // 10分钟超时 const MAX_RETRY_COUNT = 3; // 最大重试次数 +const QUEUE_CHECK_INTERVAL = 3000; // 队列检查间隔(毫秒) @Injectable() export class AI3DService { private readonly logger = new Logger(AI3DService.name); + private queueCheckTimer: NodeJS.Timeout | null = null; + private isProcessingQueue = false; // 防止并发处理队列 constructor( private prisma: PrismaService, private ossService: OssService, @Inject(AI3D_PROVIDER) private ai3dProvider: AI3DProvider, - ) {} + ) { + // 启动队列检查定时器 + this.startQueueChecker(); + } + + /** + * 启动队列检查定时器 + */ + private startQueueChecker() { + if (this.queueCheckTimer) return; + + this.queueCheckTimer = setInterval(async () => { + await this.processQueuedTasks(); + }, QUEUE_CHECK_INTERVAL); + + this.logger.log('队列检查器已启动'); + } + + /** + * 获取当前正在API执行的任务数(全局,所有用户) + */ + private async getGlobalProcessingCount(): Promise { + return this.prisma.aI3DTask.count({ + where: { + status: 'processing', // 只统计已提交到API的任务 + }, + }); + } + + /** + * 处理排队中的任务 + */ + private async processQueuedTasks() { + // 防止并发处理 + if (this.isProcessingQueue) return; + this.isProcessingQueue = true; + + try { + // 检查当前全局并发数 + const processingCount = await this.getGlobalProcessingCount(); + const availableSlots = API_MAX_CONCURRENT - processingCount; + + if (availableSlots <= 0) { + return; // 没有可用槽位 + } + + // 获取等待中的任务(按创建时间排序,先进先出) + const pendingTasks = await this.prisma.aI3DTask.findMany({ + where: { status: 'pending' }, + orderBy: { createTime: 'asc' }, + take: availableSlots, + }); + + if (pendingTasks.length === 0) return; + + this.logger.log( + `队列处理: ${pendingTasks.length} 个任务待提交,可用槽位: ${availableSlots}`, + ); + + // 逐个提交任务 + for (const task of pendingTasks) { + // 再次检查并发数(防止并发提交) + const currentProcessing = await this.getGlobalProcessingCount(); + if (currentProcessing >= API_MAX_CONCURRENT) { + this.logger.log('全局并发已满,停止提交'); + break; + } + + await this.submitTaskToAPI(task); + } + } catch (error) { + this.logger.error(`处理队列任务出错: ${error.message}`); + } finally { + this.isProcessingQueue = false; + } + } + + /** + * 提交任务到混元API + */ + private async submitTaskToAPI(task: any) { + try { + // 构建生成选项 + const options: any = { + generateType: task.generateType, + }; + + const externalTaskId = await this.ai3dProvider.submitTask( + task.inputType as 'text' | 'image', + task.inputContent, + options, + ); + + // 更新状态为处理中 + await this.prisma.aI3DTask.update({ + where: { id: task.id }, + data: { + status: 'processing', + externalTaskId, + }, + }); + + // 启动轮询检查任务状态 + this.pollTaskStatus(task.id, externalTaskId, Date.now()); + + this.logger.log(`任务 ${task.id} 已提交到API,外部ID: ${externalTaskId}`); + } catch (error) { + // 提交失败,标记为失败 + await this.prisma.aI3DTask.update({ + where: { id: task.id }, + data: { + status: 'failed', + errorMessage: error.message || 'AI服务提交失败', + completeTime: new Date(), + }, + }); + this.logger.error(`任务 ${task.id} 提交API失败: ${error.message}`); + } + } /** * 创建生成任务 @@ -34,21 +156,21 @@ export class AI3DService { tenantId: number, dto: CreateTaskDto, ) { - // 1. 检查用户当前进行中的任务数量 - const activeTaskCount = await this.prisma.aI3DTask.count({ + // 1. 检查用户当前任务数量(pending + processing) + const userTaskCount = await this.prisma.aI3DTask.count({ where: { userId, status: { in: ['pending', 'processing'] }, }, }); - if (activeTaskCount >= MAX_CONCURRENT_TASKS) { + if (userTaskCount >= MAX_USER_TASKS) { throw new BadRequestException( - `您当前有 ${activeTaskCount} 个任务正在处理中,最多同时处理 ${MAX_CONCURRENT_TASKS} 个任务,请等待完成后再提交`, + `您当前有 ${userTaskCount} 个任务正在排队或处理中,最多同时 ${MAX_USER_TASKS} 个任务,请等待完成后再提交`, ); } - // 2. 创建数据库记录 + // 2. 创建数据库记录(初始状态为 pending,表示排队中) const task = await this.prisma.aI3DTask.create({ data: { userId, @@ -60,77 +182,88 @@ export class AI3DService { }, }); - // 3. 提交到 AI 服务 - try { - // 构建生成选项 - const options: any = { - generateType: dto.generateType, - faceCount: dto.faceCount, - }; + this.logger.log(`任务 ${task.id} 已创建,进入队列`); - // 处理多视图图片(图生3D支持) - if (dto.inputType === 'image' && dto.multiViewImages) { - const viewKeyMap: Record = { - left: 'left', - right: 'right', - back: 'back', - top: 'top', - bottom: 'bottom', - left45: 'left_front', - right45: 'right_front', + // 3. 检查全局并发数,决定是立即提交还是等待队列处理 + const processingCount = await this.getGlobalProcessingCount(); + + if (processingCount < API_MAX_CONCURRENT) { + // 有空闲槽位,立即提交 + try { + // 构建生成选项 + const options: any = { + generateType: dto.generateType, + faceCount: dto.faceCount, }; - const multiViewImages: { viewType: string; imageUrl: string }[] = []; - for (const [key, url] of Object.entries(dto.multiViewImages)) { - if (url && key !== 'front' && viewKeyMap[key]) { - multiViewImages.push({ - viewType: viewKeyMap[key], - imageUrl: url, - }); + // 处理多视图图片(图生3D支持) + if (dto.inputType === 'image' && dto.multiViewImages) { + const viewKeyMap: Record = { + left: 'left', + right: 'right', + back: 'back', + top: 'top', + bottom: 'bottom', + left45: 'left_front', + right45: 'right_front', + }; + + const multiViewImages: { viewType: string; imageUrl: string }[] = []; + for (const [key, url] of Object.entries(dto.multiViewImages)) { + if (url && key !== 'front' && viewKeyMap[key]) { + multiViewImages.push({ + viewType: viewKeyMap[key], + imageUrl: url, + }); + } + } + + if (multiViewImages.length > 0) { + options.multiViewImages = multiViewImages; + this.logger.log(`多视图模式: ${multiViewImages.length} 张额外视图`); } } - if (multiViewImages.length > 0) { - options.multiViewImages = multiViewImages; - this.logger.log(`多视图模式: ${multiViewImages.length} 张额外视图`); - } + const externalTaskId = await this.ai3dProvider.submitTask( + dto.inputType, + dto.inputContent, + options, + ); + + // 更新状态为处理中 + await this.prisma.aI3DTask.update({ + where: { id: task.id }, + data: { + status: 'processing', + externalTaskId, + }, + }); + + // 启动轮询检查任务状态 + this.pollTaskStatus(task.id, externalTaskId, Date.now()); + + this.logger.log(`任务 ${task.id} 已提交到API,外部ID: ${externalTaskId}`); + } catch (error) { + // 提交失败,标记为失败 + await this.prisma.aI3DTask.update({ + where: { id: task.id }, + data: { + status: 'failed', + errorMessage: error.message || 'AI服务提交失败', + completeTime: new Date(), + }, + }); + this.logger.error(`任务 ${task.id} 提交失败: ${error.message}`); + throw error; } - - const externalTaskId = await this.ai3dProvider.submitTask( - dto.inputType, - dto.inputContent, - options, + } else { + // 全局并发已满,任务保持 pending 状态,等待队列调度 + this.logger.log( + `全局并发已满 (${processingCount}/${API_MAX_CONCURRENT}),任务 ${task.id} 进入排队`, ); - - // 4. 更新状态为处理中 - await this.prisma.aI3DTask.update({ - where: { id: task.id }, - data: { - status: 'processing', - externalTaskId, - }, - }); - - // 5. 启动轮询检查任务状态 - this.pollTaskStatus(task.id, externalTaskId, Date.now()); - - this.logger.log(`任务 ${task.id} 创建成功,外部ID: ${externalTaskId}`); - - return this.getTask(userId, task.id); - } catch (error) { - // 提交失败,更新状态 - await this.prisma.aI3DTask.update({ - where: { id: task.id }, - data: { - status: 'failed', - errorMessage: error.message || 'AI服务提交失败', - completeTime: new Date(), - }, - }); - - this.logger.error(`任务 ${task.id} 提交失败: ${error.message}`); - throw error; } + + return this.getTask(userId, task.id); } /** @@ -174,9 +307,32 @@ export class AI3DService { throw new NotFoundException('任务不存在'); } + // 如果任务在排队中,计算队列位置 + if (task.status === 'pending') { + const queuePosition = await this.getQueuePosition(task.id, task.createTime); + return { + ...task, + queuePosition, + }; + } + return task; } + /** + * 获取任务在队列中的位置 + */ + private async getQueuePosition(taskId: number, createTime: Date): Promise { + // 统计在当前任务之前创建的、仍在排队的任务数量 + const position = await this.prisma.aI3DTask.count({ + where: { + status: 'pending', + createTime: { lte: createTime }, + }, + }); + return position; + } + /** * 删除任务 */ @@ -216,64 +372,47 @@ export class AI3DService { ); } - // 检查并发限制 - const activeTaskCount = await this.prisma.aI3DTask.count({ + // 检查用户任务数限制 + const userTaskCount = await this.prisma.aI3DTask.count({ where: { userId, status: { in: ['pending', 'processing'] }, }, }); - if (activeTaskCount >= MAX_CONCURRENT_TASKS) { + if (userTaskCount >= MAX_USER_TASKS) { throw new BadRequestException( - `您当前有 ${activeTaskCount} 个任务正在处理中,请等待完成后再重试`, + `您当前有 ${userTaskCount} 个任务正在排队或处理中,请等待完成后再重试`, ); } - // 重置任务状态 + // 重置任务状态为 pending(进入队列) await this.prisma.aI3DTask.update({ where: { id }, data: { status: 'pending', errorMessage: null, completeTime: null, + externalTaskId: null, retryCount: { increment: 1 }, }, }); - // 重新提交任务 - try { - const externalTaskId = await this.ai3dProvider.submitTask( - task.inputType as 'text' | 'image', - task.inputContent, - ); + this.logger.log(`任务 ${id} 已重新加入队列,等待处理`); - await this.prisma.aI3DTask.update({ + // 检查是否可以立即提交 + const processingCount = await this.getGlobalProcessingCount(); + if (processingCount < API_MAX_CONCURRENT) { + // 有空闲槽位,立即提交 + const updatedTask = await this.prisma.aI3DTask.findUnique({ where: { id }, - data: { - status: 'processing', - externalTaskId, - }, }); - - this.pollTaskStatus(id, externalTaskId, Date.now()); - - this.logger.log(`任务 ${id} 重试成功,外部ID: ${externalTaskId}`); - - return this.getTask(userId, id); - } catch (error) { - await this.prisma.aI3DTask.update({ - where: { id }, - data: { - status: 'failed', - errorMessage: error.message || 'AI服务提交失败', - completeTime: new Date(), - }, - }); - - this.logger.error(`任务 ${id} 重试失败: ${error.message}`); - throw error; + if (updatedTask) { + await this.submitTaskToAPI(updatedTask); + } } + + return this.getTask(userId, id); } /** diff --git a/frontend/src/api/ai-3d.ts b/frontend/src/api/ai-3d.ts index 37c1052..7edf639 100644 --- a/frontend/src/api/ai-3d.ts +++ b/frontend/src/api/ai-3d.ts @@ -38,6 +38,8 @@ export interface AI3DTask { retryCount: number; createTime: string; completeTime?: string; + // 队列位置(仅 pending 状态时返回) + queuePosition?: number; } /** diff --git a/frontend/src/views/workbench/ai-3d/Generate.vue b/frontend/src/views/workbench/ai-3d/Generate.vue index 40cc812..2e522b1 100644 --- a/frontend/src/views/workbench/ai-3d/Generate.vue +++ b/frontend/src/views/workbench/ai-3d/Generate.vue @@ -73,8 +73,10 @@
-
AI 生成中
-
+
+ {{ task?.status === 'pending' ? '排队中' : 'AI 生成中' }} +
+

队列位置: {{ queueInfo.position }} @@ -82,10 +84,13 @@

预计时间: {{ queueInfo.estimatedTime }}s{{ formatEstimatedTime(queueInfo.estimatedTime) }}

+
+

正在生成3D模型,请耐心等待...

+
@@ -171,12 +176,28 @@ const selectedIndex = ref(null) // Polling timer let pollingTimer: number | null = null -// Queue info (simulated) -const queueInfo = ref({ - position: 1, - estimatedTime: 190, +// 每个任务预估耗时(秒) +const ESTIMATED_TIME_PER_TASK = 180 + +// Queue info +const queueInfo = computed(() => { + const position = task.value?.queuePosition || 0 + // 预估时间 = 队列位置 * 每个任务耗时 + const estimatedTime = position * ESTIMATED_TIME_PER_TASK + return { + position, + estimatedTime, + } }) +// 格式化预估时间 +const formatEstimatedTime = (seconds: number) => { + if (seconds <= 0) return "计算中..." + if (seconds < 60) return `${seconds}秒` + const minutes = Math.ceil(seconds / 60) + return `约${minutes}分钟` +} + // Page title const pageTitle = computed(() => { if (task.value?.inputType === "text") { @@ -308,14 +329,6 @@ const fetchTask = async () => { ) { stopPolling() } - - // Update queue info (simulated) - if (taskData.status === "pending" || taskData.status === "processing") { - queueInfo.value.estimatedTime = Math.max( - 10, - queueInfo.value.estimatedTime - 10 - ) - } } catch (error) { console.error("获取任务详情失败:", error) message.error("获取任务详情失败") diff --git a/frontend/src/views/workbench/ai-3d/History.vue b/frontend/src/views/workbench/ai-3d/History.vue index 11fb32e..3b5820f 100644 --- a/frontend/src/views/workbench/ai-3d/History.vue +++ b/frontend/src/views/workbench/ai-3d/History.vue @@ -27,7 +27,7 @@ 全部 已完成 生成中 - 等待中 + 排队中 失败 超时 @@ -85,7 +85,7 @@
- 生成中 + {{ task.status === 'pending' ? '排队中' : '生成中' }}
{ // 获取状态文本 const getStatusText = (status: string) => { const texts: Record = { - pending: "等待中", + pending: "排队中", processing: "生成中", completed: "已完成", failed: "失败", diff --git a/frontend/src/views/workbench/ai-3d/Index.vue b/frontend/src/views/workbench/ai-3d/Index.vue index c6465bf..e75efe3 100644 --- a/frontend/src/views/workbench/ai-3d/Index.vue +++ b/frontend/src/views/workbench/ai-3d/Index.vue @@ -565,7 +565,7 @@
- 生成中 + {{ task.status === 'pending' ? '排队中' : '生成中' }}