import { Controller, Get, Post, Put, Delete, Param, Body, ParseIntPipe, Query, HttpException, HttpStatus, UseGuards, Request, } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse as SwaggerApiResponse, ApiParam, ApiQuery, ApiBody, } from '@nestjs/swagger'; import { N8nTemplateFactoryService } from '../services/n8n-template-factory.service'; import { N8nTemplateEntity, TemplateType, } from '../entities/n8n-template.entity'; import { CreateTemplateDto, UpdateTemplateDto, TemplateExecuteDto, TemplateExecuteResponseDto, TemplateListDto, BatchExecuteDto, } from '../dto/template.dto'; import { TemplateExecutionEntity, ExecutionStatus, ExecutionType, } from '../entities/template-execution.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ApiCommonResponses } from '../decorators/api-common-responses.decorator'; import { PlatformAuthGuard } from 'src/platform/guards/platform-auth.guard'; import { ResponseUtil, ApiResponse } from '../utils/response.util'; import { UnifiedContentService } from '../content-moderation/services/unified-content.service'; import { AuditConclusion } from '../content-moderation/interfaces/content-moderation.interface'; @ApiTags('AI模板系统') @Controller('templates') @ApiCommonResponses() export class TemplateController { constructor( private readonly templateFactory: N8nTemplateFactoryService, @InjectRepository(N8nTemplateEntity) private readonly templateRepository: Repository, @InjectRepository(TemplateExecutionEntity) private readonly executionRepository: Repository, private readonly unifiedContentService: UnifiedContentService, ) {} @Post(':templateId/execute') @ApiOperation({ summary: '执行模板生成', description: '根据模板ID执行AI生成任务,支持图片和视频生成', }) @ApiParam({ name: 'templateId', description: '模板ID', example: 1 }) @ApiBody({ type: TemplateExecuteDto }) @SwaggerApiResponse({ status: 200, description: '执行成功', type: TemplateExecuteResponseDto, }) @SwaggerApiResponse({ status: 400, description: '参数错误', schema: { example: { code: 400, message: 'imageUrl is required', data: null, }, }, }) async executeTemplateById( @Param('templateId', ParseIntPipe) templateId: number, @Body() body: { imageUrl: string }, ) { try { const { imageUrl } = body; if (!imageUrl) { throw new HttpException('imageUrl is required', HttpStatus.BAD_REQUEST); } // 通过工厂服务创建模板实例并执行 const template = await this.templateFactory.createTemplate(templateId); const result = await template.execute(imageUrl); return ResponseUtil.success( { templateId, templateCode: template.code, templateName: template.name, inputUrl: imageUrl, outputUrl: result, creditCost: template.creditCost, }, '执行成功', ); } catch (error) { throw new HttpException( error.message || 'Template execution failed', HttpStatus.INTERNAL_SERVER_ERROR, ); } } @Post('code/:code/execute') @UseGuards(PlatformAuthGuard) @ApiOperation({ summary: '通过代码执行模板', description: '根据模板代码执行AI生成任务,支持图片和视频生成。执行前会进行图片内容审核。', }) @ApiParam({ name: 'code', description: '模板代码', example: 'character_figurine_v1', }) @ApiBody({ type: TemplateExecuteDto }) @SwaggerApiResponse({ status: 200, description: '执行成功', type: TemplateExecuteResponseDto, }) @SwaggerApiResponse({ status: 403, description: '图片审核未通过', schema: { example: { code: 403, message: '图片审核未通过: 包含不当内容', data: null, }, }, }) @SwaggerApiResponse({ status: 429, description: '任务数量限制', schema: { example: { code: 429, message: '当前账号有3个任务正在进行中,且距开始时间不足5分钟,请稍后再试', data: null, }, }, }) async executeTemplateByCode( @Param('code') code: string, @Body() body: { imageUrl: string }, @Request() req, ): Promise> { try { const { imageUrl } = body; if (!imageUrl) { throw new HttpException('imageUrl is required', HttpStatus.BAD_REQUEST); } const userId = req.user.userId; // 检查用户当前的任务限制 await this.checkUserTaskLimit(userId); // 首先获取模板配置以确定模板类型 const templateConfig = await this.templateFactory.getTemplateByCode(code); if (!templateConfig) { throw new HttpException('Template not found', HttpStatus.NOT_FOUND); } const platform = req.user.platform; // 1. 先进行图片内容审核 const auditResult = await this.unifiedContentService.auditImage( platform, { imageUrl, userId, businessType: 'template_execution', extraData: { templateId: templateConfig.id, templateCode: code, }, }, ); // 2. 检查审核结果 if (auditResult.conclusion !== AuditConclusion.PASS) { const failureReason = auditResult.details.length > 0 ? auditResult.details.map((d) => d.description).join(', ') : '包含不当内容'; throw new HttpException( `图片审核未通过: ${failureReason}`, HttpStatus.FORBIDDEN, ); } // 3. 审核通过,执行模板 const template = await this.templateFactory.createTemplateByCode(code); const taskId = await template.execute(imageUrl); // 4. 将任务保存到 TemplateExecutionEntity const execution = this.executionRepository.create({ templateId: templateConfig.id, userId, platform: req.user.platform, type: templateConfig.templateType === TemplateType.VIDEO ? ExecutionType.VIDEO : ExecutionType.IMAGE, prompt: '', // 可以从请求参数中获取,如果有的话 inputImageUrl: imageUrl, taskId: taskId, // 保存外部系统返回的任务ID,用于回调匹配 status: ExecutionStatus.PROCESSING, progress: 0, creditCost: templateConfig.creditCost, startedAt: new Date(), }); const savedExecution = await this.executionRepository.save(execution); // 返回任务id (执行记录的ID) 以及审核信息 return ResponseUtil.success(savedExecution.id, '模板执行已启动'); } catch (error) { console.error(error); throw new HttpException( error.message || 'Template execution failed', error instanceof HttpException ? error.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR, ); } } /** * 检查用户当前任务限制 * @param userId 用户ID * @throws HttpException 如果超出任务限制 */ private async checkUserTaskLimit(userId: string): Promise { // 查询用户当前进行中的任务 const processingTasks = await this.executionRepository.find({ where: { userId, status: ExecutionStatus.PROCESSING, }, order: { startedAt: 'ASC', }, }); // 如果当前没有进行中的任务,允许执行 if (processingTasks.length === 0) { return; } // 如果当前进行中任务数量已达到3个,需要检查时间限制 if (processingTasks.length >= 3) { const now = new Date(); const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000); // 检查是否有任务在5分钟内开始 const recentTasks = processingTasks.filter( (task) => task.startedAt && task.startedAt.getTime() > fiveMinutesAgo.getTime(), ); if (recentTasks.length > 3) { throw new HttpException( `有${recentTasks.length}个任务正在进行中,请稍后再试`, HttpStatus.TOO_MANY_REQUESTS, ); } } } @Post(':templateId/batch-execute') @ApiOperation({ summary: '批量执行模板', description: '批量执行模板生成任务,支持多张图片同时处理', }) @ApiParam({ name: 'templateId', description: '模板ID', example: 1 }) @ApiBody({ type: BatchExecuteDto }) @SwaggerApiResponse({ status: 200, description: '批量执行成功', schema: { example: { success: true, data: { templateId: 1, totalCount: 2, results: [ { inputUrl: 'image1.jpg', outputUrl: 'output1.jpg' }, { inputUrl: 'image2.jpg', outputUrl: 'output2.jpg' }, ], }, }, }, }) async batchExecuteTemplate( @Param('templateId', ParseIntPipe) templateId: number, @Body() body: { imageUrls: string[] }, ) { try { const { imageUrls } = body; if (!imageUrls || !Array.isArray(imageUrls) || imageUrls.length === 0) { throw new HttpException( 'imageUrls array is required', HttpStatus.BAD_REQUEST, ); } const results = await this.templateFactory.batchExecute( templateId, imageUrls, ); return ResponseUtil.success( { templateId, totalCount: imageUrls.length, results: imageUrls.map((inputUrl, index) => ({ inputUrl, outputUrl: results[index], })), }, '批量执行成功', ); } catch (error) { throw new HttpException( error.message || 'Batch execution failed', HttpStatus.INTERNAL_SERVER_ERROR, ); } } @Get() @ApiOperation({ summary: '获取所有模板列表', description: '获取所有可用的AI生成模板,支持按类型筛选', }) @SwaggerApiResponse({ status: 200, description: '获取成功', type: [TemplateListDto], }) async getAllTemplates(): Promise> { try { const templates = await this.templateFactory.getAllTemplates(); const data = templates.map((template) => ({ id: template.id, code: template.code, name: template.name, description: template.description, creditCost: template.creditCost, version: template.version, templateType: template.templateType, templateClass: template.templateClass, tags: template.tags, inputExample: template.inputExampleUrl, outputExample: template.outputExampleUrl, sortOrder: template.sortOrder, isActive: template.isActive, createdAt: template.createdAt, updatedAt: template.updatedAt, })); return ResponseUtil.success(data, '获取模板列表成功'); } catch (error) { throw new HttpException( error.message || 'Failed to fetch templates', HttpStatus.INTERNAL_SERVER_ERROR, ); } } @Get('image') @ApiOperation({ summary: '获取图片模板列表', description: '获取所有图片类型的AI生成模板', }) @SwaggerApiResponse({ status: 200, description: '获取成功', type: [TemplateListDto], }) async getImageTemplates() { try { const templates = await this.templateFactory.getImageTemplates(); return ResponseUtil.success(templates, '获取图片模板列表成功'); } catch (error) { throw new HttpException( error.message || 'Failed to fetch image templates', HttpStatus.INTERNAL_SERVER_ERROR, ); } } @Get('video') @ApiOperation({ summary: '获取视频模板列表', description: '获取所有视频类型的AI生成模板', }) @SwaggerApiResponse({ status: 200, description: '获取成功', type: [TemplateListDto], }) async getVideoTemplates() { try { const templates = await this.templateFactory.getVideoTemplates(); return ResponseUtil.success(templates, '获取视频模板列表成功'); } catch (error) { throw new HttpException( error.message || 'Failed to fetch video templates', HttpStatus.INTERNAL_SERVER_ERROR, ); } } @Get('recommend') @ApiOperation({ summary: '推荐模板', description: '根据用户偏好标签推荐适合的模板', }) @ApiQuery({ name: 'tags', required: false, description: '用户偏好标签,逗号分隔', example: '人物,手办', }) @ApiQuery({ name: 'limit', required: false, description: '推荐数量', example: 5, }) @SwaggerApiResponse({ status: 200, description: '推荐成功', schema: { example: { success: true, data: { userTags: ['人物', '手办'], recommendedTemplates: [], }, }, }, }) async recommendTemplates( @Query('tags') tags?: string, @Query('limit') limit: number = 5, ) { try { const userTags = tags ? tags.split(',').map((tag) => tag.trim()) : []; const templates = await this.templateFactory.recommendTemplates( userTags, limit, ); return ResponseUtil.success( { userTags, recommendedTemplates: templates, }, '模板推荐成功', ); } catch (error) { throw new HttpException( error.message || 'Failed to recommend templates', HttpStatus.INTERNAL_SERVER_ERROR, ); } } @Get(':templateId') @ApiOperation({ summary: '获取模板详情', description: '根据模板ID获取详细信息', }) @ApiParam({ name: 'templateId', description: '模板ID', example: 1 }) @SwaggerApiResponse({ status: 200, description: '获取成功', type: TemplateListDto, }) @SwaggerApiResponse({ status: 404, description: '模板不存在', }) async getTemplateById(@Param('templateId', ParseIntPipe) templateId: number) { try { const template = await this.templateFactory.createTemplate(templateId); return ResponseUtil.success( { id: templateId, code: template.code, name: template.name, description: template.description, creditCost: template.creditCost, version: template.version, tags: template.tags, inputExample: template.input, outputExample: template.output, }, '获取模板详情成功', ); } catch (error) { throw new HttpException( error.message || 'Template not found', HttpStatus.NOT_FOUND, ); } } /** * 查询模板执行进度 * @param taskId 任务ID * @returns 执行进度信息 */ @Get('execution/:taskId/progress') async getExecutionProgress( @Param('taskId', ParseIntPipe) taskId: number, ): Promise> { try { const execution = await this.executionRepository.findOne({ where: { id: taskId }, relations: ['template'], }); if (!execution) { throw new HttpException( 'Execution task not found', HttpStatus.NOT_FOUND, ); } return ResponseUtil.success( { taskId: execution.id, templateId: execution.templateId, templateName: execution.template?.name, userId: execution.userId, platform: execution.platform, type: execution.type, status: execution.status, progress: execution.progress, inputImageUrl: execution.inputImageUrl, outputUrl: execution.outputUrl, thumbnailUrl: execution.thumbnailUrl, errorMessage: execution.errorMessage, creditCost: execution.creditCost, startedAt: execution.startedAt, completedAt: execution.completedAt, executionDuration: execution.executionDuration, createdAt: execution.createdAt, updatedAt: execution.updatedAt, executionResult: execution.executionResult, }, '获取执行进度成功', ); } catch (error) { if (error instanceof HttpException) { throw error; } throw new HttpException( error.message || 'Failed to fetch execution progress', HttpStatus.INTERNAL_SERVER_ERROR, ); } } /** * 查询用户的执行任务列表 * @param userId 用户ID * @param status 状态筛选 * @param limit 数量限制 * @returns 用户执行任务列表 */ @Get('executions/user/:userId') async getUserExecutions( @Param('userId') userId: string, @Query('status') status?: string, @Query('limit') limit: number = 20, ) { try { const queryBuilder = this.executionRepository .createQueryBuilder('execution') .leftJoinAndSelect('execution.template', 'template') .where('execution.userId = :userId', { userId }) .orderBy('execution.createdAt', 'DESC') .limit(limit); if (status) { queryBuilder.andWhere('execution.status = :status', { status }); } const executions = await queryBuilder.getMany(); return ResponseUtil.success( executions.map((execution) => ({ taskId: execution.id, templateId: execution.templateId, templateName: execution.template?.name, templateCode: execution.template?.code, type: execution.type, status: execution.status, progress: execution.progress, inputImageUrl: execution.inputImageUrl, outputUrl: execution.outputUrl, thumbnailUrl: execution.thumbnailUrl, creditCost: execution.creditCost, startedAt: execution.startedAt, completedAt: execution.completedAt, executionDuration: execution.executionDuration, createdAt: execution.createdAt, })), '获取用户执行任务列表成功', ); } catch (error) { throw new HttpException( error.message || 'Failed to fetch user executions', HttpStatus.INTERNAL_SERVER_ERROR, ); } } @Post() @ApiOperation({ summary: '创建新模板', description: '创建一个新的AI生成模板', }) @ApiBody({ type: CreateTemplateDto }) @SwaggerApiResponse({ status: 200, description: '创建成功', schema: { example: { success: true, data: { id: 1, code: 'new_template_v1', name: '新模板', }, }, }, }) @SwaggerApiResponse({ status: 409, description: '模板代码已存在', }) async createTemplate(@Body() createTemplateDto: CreateTemplateDto) { try { const existingTemplate = await this.templateRepository.findOne({ where: { code: createTemplateDto.code }, }); if (existingTemplate) { throw new HttpException( `Template with code '${createTemplateDto.code}' already exists`, HttpStatus.CONFLICT, ); } const template = this.templateRepository.create(createTemplateDto); const savedTemplate = await this.templateRepository.save(template); return ResponseUtil.success(savedTemplate, '模板创建成功'); } catch (error) { if (error instanceof HttpException) { throw error; } throw new HttpException( error.message || 'Failed to create template', HttpStatus.BAD_REQUEST, ); } } @Put(':templateId') @ApiOperation({ summary: '更新模板', description: '更新指定模板的配置信息', }) @ApiParam({ name: 'templateId', description: '模板ID', example: 1 }) @ApiBody({ type: UpdateTemplateDto }) @SwaggerApiResponse({ status: 200, description: '更新成功', type: TemplateListDto, }) @SwaggerApiResponse({ status: 404, description: '模板不存在', }) @SwaggerApiResponse({ status: 409, description: '模板代码已存在', }) async updateTemplate( @Param('templateId', ParseIntPipe) templateId: number, @Body() updateTemplateDto: UpdateTemplateDto, ) { try { const existingTemplate = await this.templateRepository.findOne({ where: { id: templateId }, }); if (!existingTemplate) { throw new HttpException('Template not found', HttpStatus.NOT_FOUND); } if ( updateTemplateDto.code && updateTemplateDto.code !== existingTemplate.code ) { const codeConflict = await this.templateRepository.findOne({ where: { code: updateTemplateDto.code }, }); if (codeConflict) { throw new HttpException( `Template with code '${updateTemplateDto.code}' already exists`, HttpStatus.CONFLICT, ); } } await this.templateRepository.update(templateId, updateTemplateDto); const updatedTemplate = await this.templateRepository.findOne({ where: { id: templateId }, }); return ResponseUtil.success(updatedTemplate, '模板更新成功'); } catch (error) { if (error instanceof HttpException) { throw error; } throw new HttpException( error.message || 'Failed to update template', HttpStatus.BAD_REQUEST, ); } } /** * 删除模板 * @param templateId 模板ID * @returns 删除结果 */ @Delete(':templateId') async deleteTemplate(@Param('templateId', ParseIntPipe) templateId: number) { try { const template = await this.templateRepository.findOne({ where: { id: templateId }, }); if (!template) { throw new HttpException('Template not found', HttpStatus.NOT_FOUND); } await this.templateRepository.delete(templateId); return ResponseUtil.success(null, '模板删除成功'); } catch (error) { throw new HttpException( error.message || 'Failed to delete template', HttpStatus.BAD_REQUEST, ); } } /** * 获取模板详情(管理后台专用) * @param templateId 模板ID * @returns 模板完整详情(包含非激活状态的模板) */ @Get('admin/:templateId') async getTemplateForAdmin( @Param('templateId', ParseIntPipe) templateId: number, ) { try { const template = await this.templateRepository.findOne({ where: { id: templateId }, }); if (!template) { throw new HttpException('Template not found', HttpStatus.NOT_FOUND); } return ResponseUtil.success(template, '获取模板详情成功'); } catch (error) { throw new HttpException( error.message || 'Template not found', HttpStatus.NOT_FOUND, ); } } /** * 获取所有模板列表(管理后台专用) * @param page 页码 * @param limit 每页数量 * @param type 模板类型筛选 * @param isActive 激活状态筛选 * @returns 分页的模板列表(包含非激活状态的模板) */ @Get('admin') async getAllTemplatesForAdmin( @Query('page') page: number = 1, @Query('limit') limit: number = 10, @Query('type') type?: TemplateType, @Query('isActive') isActive?: boolean, ) { try { const queryBuilder = this.templateRepository.createQueryBuilder('template'); if (type) { queryBuilder.andWhere('template.templateType = :type', { type }); } if (isActive !== undefined) { queryBuilder.andWhere('template.isActive = :isActive', { isActive }); } queryBuilder .orderBy('template.sortOrder', 'DESC') .addOrderBy('template.createdAt', 'DESC') .skip((page - 1) * limit) .take(limit); const [templates, total] = await queryBuilder.getManyAndCount(); return ResponseUtil.paginated( templates, total, page, limit, '获取模板列表成功', ); } catch (error) { throw new HttpException( error.message || 'Failed to fetch templates', HttpStatus.INTERNAL_SERVER_ERROR, ); } } }