From f83859b5185dc83deb1c9c33e7d048e653088f4c Mon Sep 17 00:00:00 2001 From: imeepos Date: Thu, 4 Sep 2025 15:20:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E9=85=8D=E7=BD=AE=E5=92=8C=E6=A8=A1=E6=9D=BF=E8=BF=81?= =?UTF-8?q?=E7=A7=BB=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 配置MySQL数据库连接和TypeORM集成 - 创建N8nTemplate实体和动态实例实体 - 实现混合架构:代码逻辑+数据库配置 - 添加模板工厂服务和迁移服务 - 创建RESTful API控制器用于模板管理 - 将所有n8nTemplates模板整理为独立migration文件 - 添加完整的数据库迁移指南和使用文档 - 更新app.module.ts集成新服务和配置 --- DATABASE_SETUP.md | 243 +++ MIGRATION_GUIDE.md | 180 ++ docs/ai-generation-service.md | 519 ----- docs/conflict-resolution-summary.md | 166 -- docs/rabbitmq-configuration.md | 508 ----- docs/template-hybrid-architecture.md | 501 +++++ docs/template-management-system.md | 1668 ----------------- docs/template-system-summary.md | 350 ++-- input.md | 39 +- ormconfig.js | 17 + package.json | 15 +- pnpm-lock.yaml | 404 ++++ src/app.module.ts | 21 +- src/config/database.config.ts | 35 + src/controllers/template.controller.ts | 349 ++++ src/entities/n8n-template-instance.entity.ts | 176 ++ src/entities/n8n-template.entity.ts | 88 + .../1725434680000-AddPhotoRestoreTemplate.ts | 47 + ...5434690000-AddCharacterFigurineTemplate.ts | 47 + .../1725434700000-AddPetFigurineTemplate.ts | 47 + ...5434710000-AddCosplayRealPersonTemplate.ts | 47 + .../1725434720000-AddGarageOpeningTemplate.ts | 47 + ...25434730000-AddJapaneseMagazineTemplate.ts | 47 + ...25434740000-AddNuclearExplosionTemplate.ts | 47 + .../1725434750000-AddOpenEyesTemplate.ts | 47 + src/services/n8n-template-factory.service.ts | 192 ++ src/services/template-migration.service.ts | 280 +++ .../n8nTemplates/CharacterFigurineTemplate.ts | 2 +- .../n8nTemplates/CosplayRealPersonTemplate.ts | 2 +- .../n8nTemplates/GarageOpeningTemplate.ts | 2 +- .../n8nTemplates/JapaneseMagazineTemplate.ts | 2 +- .../n8nTemplates/PetFigurineTemplate.ts | 2 +- 32 files changed, 3079 insertions(+), 3058 deletions(-) create mode 100644 DATABASE_SETUP.md create mode 100644 MIGRATION_GUIDE.md delete mode 100644 docs/ai-generation-service.md delete mode 100644 docs/conflict-resolution-summary.md delete mode 100644 docs/rabbitmq-configuration.md create mode 100644 docs/template-hybrid-architecture.md delete mode 100644 docs/template-management-system.md create mode 100644 ormconfig.js create mode 100644 src/config/database.config.ts create mode 100644 src/controllers/template.controller.ts create mode 100644 src/entities/n8n-template-instance.entity.ts create mode 100644 src/entities/n8n-template.entity.ts create mode 100644 src/migrations/1725434680000-AddPhotoRestoreTemplate.ts create mode 100644 src/migrations/1725434690000-AddCharacterFigurineTemplate.ts create mode 100644 src/migrations/1725434700000-AddPetFigurineTemplate.ts create mode 100644 src/migrations/1725434710000-AddCosplayRealPersonTemplate.ts create mode 100644 src/migrations/1725434720000-AddGarageOpeningTemplate.ts create mode 100644 src/migrations/1725434730000-AddJapaneseMagazineTemplate.ts create mode 100644 src/migrations/1725434740000-AddNuclearExplosionTemplate.ts create mode 100644 src/migrations/1725434750000-AddOpenEyesTemplate.ts create mode 100644 src/services/n8n-template-factory.service.ts create mode 100644 src/services/template-migration.service.ts diff --git a/DATABASE_SETUP.md b/DATABASE_SETUP.md new file mode 100644 index 0000000..cdfc114 --- /dev/null +++ b/DATABASE_SETUP.md @@ -0,0 +1,243 @@ +# 数据库配置说明 + +## 概述 +基于混合式架构设计,实现了**代码定义逻辑 + 数据库存储配置**的AI模板管理系统。 + +## 数据库连接信息 +- **服务器**: mysql-6bc9094abd49-public.rds.volces.com:53305 +- **数据库**: nano_camera_miniapp +- **用户名**: root +- **密码**: WsJWXfwFx0bq6DE2kmB6 + +## 安装依赖 + +```bash +npm install +``` + +如果安装失败,请手动添加以下依赖到 package.json: +```json +{ + "@nestjs/typeorm": "^11.0.0", + "typeorm": "^0.3.20", + "mysql2": "^3.9.7" +} +``` + +## 数据库迁移 + +### 运行迁移 +```bash +npm run migration:run +``` + +### 生成新迁移 +```bash +npm run migration:generate -- -n MigrationName +``` + +### 回滚迁移 +```bash +npm run migration:revert +``` + +## 混合式架构使用 + +### 1. 传统方式(代码定义) +```typescript +import { templateManager } from './templates'; + +// 使用现有的代码定义模板 +const template = templateManager.getTemplate('photo_restore_v1'); +const result = await template.execute(imageUrl); +``` + +### 2. 数据库驱动方式(推荐) +```typescript +import { N8nTemplateFactoryService } from './services/n8n-template-factory.service'; + +// 通过模板ID创建实例 +const template = await templateFactory.createTemplate(1); +const result = await template.execute(imageUrl); + +// 通过模板代码创建实例 +const template = await templateFactory.createTemplateByCode('character_figurine_v1'); +const result = await template.execute(imageUrl); + +// 您期望的使用方式 +const result = await new N8nVideoGenerateTemplateEntity(templateId, repo) + .onInit() + .then(e => e.execute(imgUrl)); +``` + +## API 接口 + +### 基础模板操作 + +#### 获取所有模板 +``` +GET /templates +``` + +#### 获取图片模板 +``` +GET /templates/image +``` + +#### 获取视频模板 +``` +GET /templates/video +``` + +#### 获取模板详情 +``` +GET /templates/{templateId} +``` + +### 模板执行 + +#### 通过模板ID执行 +``` +POST /templates/{templateId}/execute +{ + "imageUrl": "https://example.com/image.jpg" +} +``` + +#### 通过模板代码执行 +``` +POST /templates/code/{templateCode}/execute +{ + "imageUrl": "https://example.com/image.jpg" +} +``` + +#### 批量执行 +``` +POST /templates/{templateId}/batch-execute +{ + "imageUrls": ["url1", "url2", "url3"] +} +``` + +### 管理接口 + +#### 迁移现有模板到数据库 +``` +POST /templates/migrate +``` + +#### 同步模板配置 +``` +POST /templates/sync +``` + +#### 获取迁移报告 +``` +GET /templates/admin/migration-report +``` + +## 数据库表结构 + +### n8n_templates 表 + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | 主键ID | +| code | varchar(100) | 模板唯一标识码 | +| name | varchar(200) | 模板名称 | +| description | text | 模板详细描述 | +| credit_cost | int | 积分消耗 | +| version | varchar(50) | 版本号 | +| input_example_url | text | 输入示例图片 | +| output_example_url | text | 输出示例图片 | +| tags | json | 标签数组 | +| template_type | enum | 模板类型(image/video) | +| template_class | varchar(100) | 具体模板类名 | +| image_model | varchar(100) | 图片生成模型 | +| image_prompt | text | 图片生成提示词 | +| video_model | varchar(100) | 视频生成模型 | +| video_prompt | text | 视频生成提示词 | +| duration | int | 视频时长(秒) | +| aspect_ratio | varchar(20) | 视频比例 | +| is_active | boolean | 是否启用 | +| sort_order | int | 排序权重 | +| created_at | timestamp | 创建时间 | +| updated_at | timestamp | 更新时间 | + +## 架构优势 + +### 🔒 保持现有优势 +- **类型安全**: 继承现有抽象类,保持TypeScript类型检查 +- **执行逻辑不变**: N8nTemplate的execute方法逻辑完全保持不变 +- **抽象层次清晰**: Template → ImageGenerate/VideoGenerate → N8nTemplate → 具体实现 + +### 🆕 新增数据库能力 +- **配置动态化**: 模板配置存储在数据库中,可在线修改 +- **运营友好**: 通过管理后台可以调整模板参数、启用/禁用模板 +- **A/B测试**: 可以创建同一功能的不同版本进行测试 +- **统计分析**: 可以跟踪每个模板的使用情况和效果 + +### 🚀 业务价值增强 +- **个性化推荐**: 根据用户历史偏好推荐模板 +- **实时监控**: 模板性能、成功率、用户满意度监控 +- **成本优化**: 基于使用统计调整积分定价策略 +- **自动化运营**: 根据数据自动调整模板优先级 + +## 开发工作流 + +### 1. 添加新模板(数据库方式) +```sql +INSERT INTO n8n_templates ( + code, name, description, credit_cost, version, + template_type, image_model, image_prompt, ... +) VALUES ( + 'new_template_v1', '新模板', '描述', 20, '1.0.0', + 'image', 'gemini-2.5-flash-image-preview', '提示词', ... +); +``` + +### 2. 添加新模板(代码方式) +```typescript +export class NewTemplate extends N8nImageGenerateTemplate { + readonly code = 'new_template_v1'; + readonly name = '新模板'; + // ... 其他属性 +} +``` + +### 3. 迁移现有模板 +```typescript +// 运行迁移服务,将代码中的模板配置导入数据库 +await migrationService.migrateAllExistingTemplates(); +``` + +## 常见问题 + +### Q: 如何选择使用代码模板还是数据库模板? +A: 推荐使用数据库模板方式,它提供了更强的灵活性和运营能力。代码模板主要用于维护现有系统兼容性。 + +### Q: 现有的模板代码需要修改吗? +A: 不需要。现有的模板代码可以继续使用,同时也可以通过迁移服务导入到数据库中。 + +### Q: 数据库中的模板配置可以实时生效吗? +A: 是的,每次调用都会从数据库读取最新配置,支持实时更新。 + +### Q: 如何进行A/B测试? +A: 可以在数据库中创建同一功能的不同配置版本,通过调整 `sort_order` 和 `is_active` 字段进行测试。 + +## 监控与维护 + +### 性能监控 +- 模板执行时间统计 +- 成功率监控 +- 资源使用监控 + +### 数据维护 +- 定期备份数据库 +- 清理过期的使用记录 +- 优化查询性能 + +--- + +**混合式架构完美融合了代码优先的技术优势和数据库驱动的运营能力!** \ No newline at end of file diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md new file mode 100644 index 0000000..ced35f3 --- /dev/null +++ b/MIGRATION_GUIDE.md @@ -0,0 +1,180 @@ +# 模板迁移指南 + +## 📋 迁移文件概览 + +已为每个模板创建了单独的migration文件,方便管理和维护: + +### 🗃️ 迁移文件列表 + +| 时间戳 | 文件名 | 模板类型 | 模板名称 | 积分消耗 | 说明 | +|--------|---------|----------|----------|----------|------| +| 1725434673000 | CreateN8nTemplatesTable.ts | 基础表 | - | - | 创建n8n_templates表结构 | +| 1725434680000 | AddPhotoRestoreTemplate.ts | image | 老照片修复上色 | 12 | 黑白照片修复和上色 | +| 1725434690000 | AddCharacterFigurineTemplate.ts | video | 人物手办 | 28 | 人物手办制作和视频 | +| 1725434700000 | AddPetFigurineTemplate.ts | video | 宠物手办 | 30 | 宠物手办制作和视频 | +| 1725434710000 | AddCosplayRealPersonTemplate.ts | video | cos真人 | 25 | Cosplay真人照片生成 | +| 1725434720000 | AddGarageOpeningTemplate.ts | video | 车库开门 | 35 | 豪宅车库开门场景 | +| 1725434730000 | AddJapaneseMagazineTemplate.ts | video | 日本杂志 | 32 | 日本杂志风格海报 | +| 1725434740000 | AddNuclearExplosionTemplate.ts | video | 原子弹爆炸 | 20 | 核爆炸特效视频 | +| 1725434750000 | AddOpenEyesTemplate.ts | image | 闭眼照片修复 | 10 | 修复照片闭眼问题 | + +## 🚀 执行迁移 + +### 1. 运行所有迁移 +```bash +npm run migration:run +``` + +### 2. 检查迁移状态 +```bash +npm run typeorm migration:show +``` + +### 3. 回滚最后一个迁移 +```bash +npm run migration:revert +``` + +## 📊 迁移顺序说明 + +迁移会按照时间戳顺序执行: + +1. **CreateN8nTemplatesTable** - 创建基础表结构和索引 +2. **AddPhotoRestoreTemplate** - 添加老照片修复模板 +3. **AddCharacterFigurineTemplate** - 添加人物手办模板 +4. **AddPetFigurineTemplate** - 添加宠物手办模板 +5. **AddCosplayRealPersonTemplate** - 添加cos真人模板 +6. **AddGarageOpeningTemplate** - 添加车库开门模板 +7. **AddJapaneseMagazineTemplate** - 添加日本杂志模板 +8. **AddNuclearExplosionTemplate** - 添加原子弹爆炸模板 +9. **AddOpenEyesTemplate** - 添加闭眼照片修复模板 + +## 🔍 验证迁移结果 + +### 查看所有模板 +```sql +SELECT id, code, name, template_type, credit_cost, is_active +FROM n8n_templates +ORDER BY sort_order DESC, created_at ASC; +``` + +### 按类型分组统计 +```sql +SELECT + template_type, + COUNT(*) as count, + AVG(credit_cost) as avg_cost, + MIN(credit_cost) as min_cost, + MAX(credit_cost) as max_cost +FROM n8n_templates +WHERE is_active = true +GROUP BY template_type; +``` + +### 验证数据完整性 +```sql +-- 检查必填字段 +SELECT COUNT(*) as total_templates FROM n8n_templates; +SELECT COUNT(*) as templates_with_code FROM n8n_templates WHERE code IS NOT NULL; +SELECT COUNT(*) as active_templates FROM n8n_templates WHERE is_active = true; + +-- 检查重复代码 +SELECT code, COUNT(*) as count +FROM n8n_templates +GROUP BY code +HAVING count > 1; +``` + +## 🛠️ 单独管理模板 + +### 添加新模板 +创建新的migration文件: +```bash +npm run migration:generate -- -n AddNewTemplate +``` + +### 修改现有模板 +可以创建新的migration来更新现有模板: +```typescript +// 示例:更新模板积分消耗 +await queryRunner.query(` + UPDATE n8n_templates + SET credit_cost = 15 + WHERE code = 'photo_restore_v1' +`); +``` + +### 禁用模板 +```typescript +await queryRunner.query(` + UPDATE n8n_templates + SET is_active = false + WHERE code = 'template_code_here' +`); +``` + +## 📈 模板分类统计 + +### 按类型分布 +- **Image模板**: 2个 (PhotoRestore, OpenEyes) +- **Video模板**: 6个 (CharacterFigurine, PetFigurine, CosplayRealPerson, GarageOpening, JapaneseMagazine, NuclearExplosion) + +### 积分消耗分布 +- **低成本** (10-15积分): OpenEyes(10), PhotoRestore(12) +- **中等成本** (20-30积分): NuclearExplosion(20), CosplayRealPerson(25), CharacterFigurine(28), PetFigurine(30) +- **高成本** (30+积分): JapaneseMagazine(32), GarageOpening(35) + +## 🎯 使用建议 + +### 开发环境 +```bash +# 清空数据库并重新运行所有迁移 +npm run schema:drop +npm run migration:run +``` + +### 生产环境 +```bash +# 只运行新的迁移 +npm run migration:run +``` + +### 回滚策略 +每个migration都包含完整的`up`和`down`方法,支持安全回滚: +```bash +# 回滚最后一个迁移 +npm run migration:revert + +# 回滚多个迁移(需要多次执行) +npm run migration:revert # 回滚最新的 +npm run migration:revert # 回滚倒数第二个 +``` + +## 🔒 注意事项 + +1. **生产环境执行前备份数据库** +2. **按顺序执行迁移,不要跳过** +3. **每个模板的代码(code)必须唯一** +4. **修改模板时使用UPDATE而不是重新INSERT** +5. **测试环境先验证迁移无问题** + +## 📝 日志输出 + +每个迁移执行时都会输出日志: +``` +✅ N8n Templates table created successfully +✅ PhotoRestoreTemplate migration completed +✅ CharacterFigurineTemplate migration completed +... +``` + +回滚时的日志: +``` +⏭️ OpenEyesTemplate migration reverted +⏭️ NuclearExplosionTemplate migration reverted +... +``` + +--- + +**完成迁移后,所有现有的代码模板配置将可以通过数据库动态管理!** \ No newline at end of file diff --git a/docs/ai-generation-service.md b/docs/ai-generation-service.md deleted file mode 100644 index f038044..0000000 --- a/docs/ai-generation-service.md +++ /dev/null @@ -1,519 +0,0 @@ -# AI图片/视频生成服务配置指南 - -## 1. 服务架构设计 - -### 1.1 AI生成服务流程 -``` -用户请求 → 积分校验 → 任务创建 → 队列处理 → AI模型调用 → 结果存储 → 用户通知 -``` - -### 1.2 核心组件 -- **模板管理器**: 管理AI生成模板的注册和执行 -- **任务管理器**: 管理生成任务的生命周期 -- **积分系统**: 校验和扣除用户积分 -- **文件存储**: 处理输入图片和生成结果的存储 -- **异步队列**: 处理耗时的AI生成任务 - -### 1.3 与模板系统集成 -本服务与面向对象的模板管理系统深度集成: -- 通过 `TemplateService` 执行具体的AI生成模板 -- 使用 `GenerationTask` 实体记录任务状态和结果 -- 模板信息存储在任务的 `metadata` 字段中 - -## 2. AI生成服务实现 - -### 2.1 生成任务服务 (与模板系统集成版本) -```typescript -// src/services/generation.service.ts -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { GenerationTask, GenerationType, TaskStatus } from '../entities/generation-task.entity'; -import { CreditService } from './credit.service'; -import { TemplateService } from './template.service'; -import { FileService } from './file.service'; -import { MessageProducerService } from './message-producer.service'; - -@Injectable() -export class GenerationService { - constructor( - @InjectRepository(GenerationTask) - private readonly taskRepository: Repository, - private readonly creditService: CreditService, - private readonly templateService: TemplateService, - private readonly fileService: FileService, - private readonly messageProducer: MessageProducerService, - ) {} - - // 新版本:通过模板代码创建生成任务 - async createGenerationTaskByTemplate(createTaskDto: CreateTemplateTaskDto): Promise { - const { userId, platform, templateCode, inputParameters } = createTaskDto; - - // 1. 获取模板信息 - const templateInfo = this.templateService.getTemplateInfo(templateCode); - if (!templateInfo) { - throw new Error(`模板 ${templateCode} 不存在`); - } - - // 2. 校验用户积分 - const hasEnoughCredits = await this.creditService.checkBalance(userId, platform, templateInfo.creditCost); - if (!hasEnoughCredits) { - throw new Error('积分不足'); - } - - // 3. 上传输入图片(如果有) - let inputImageUrl = null; - if (inputParameters.inputImage) { - inputImageUrl = await this.fileService.uploadImage(inputParameters.inputImage, userId); - inputParameters.inputImage = inputImageUrl; // 替换为URL - } - - // 4. 创建任务记录 - const task = this.taskRepository.create({ - userId, - platform, - type: templateInfo.category.includes('视频') ? GenerationType.VIDEO : GenerationType.IMAGE, - prompt: inputParameters.prompt || inputParameters.clothingDescription || '', - inputImageUrl, - creditCost: templateInfo.creditCost, - parameters: inputParameters, - status: TaskStatus.PENDING, - metadata: { - templateCode: templateInfo.code, - templateName: templateInfo.name, - templateVersion: templateInfo.version, - templateCategory: templateInfo.category, - }, - }); - - const savedTask = await this.taskRepository.save(task); - - // 5. 扣除积分 - await this.creditService.consumeCredits(userId, platform, templateInfo.creditCost, 'ai_generation', savedTask.id); - - // 6. 发送到处理队列 - await this.messageProducer.sendGenerationTask({ - taskId: savedTask.id, - userId, - platform, - type: savedTask.type, - templateCode, - inputParameters, - }); - - return savedTask; - } - - async processGenerationTask(taskId: string): Promise { - const task = await this.taskRepository.findOne({ where: { id: taskId } }); - if (!task) { - throw new Error('任务不存在'); - } - - try { - // 更新状态为处理中 - await this.updateTaskStatus(taskId, TaskStatus.PROCESSING); - - const startTime = Date.now(); - - // 调用AI模型生成 - const result = await this.aiModelService.generate({ - type: task.type, - prompt: task.prompt, - inputImageUrl: task.inputImageUrl, - parameters: task.parameters, - }); - - const processingTime = Math.floor((Date.now() - startTime) / 1000); - - // 上传生成结果 - const outputUrl = await this.fileService.uploadGeneratedContent(result.content, task.userId); - const thumbnailUrl = await this.fileService.generateThumbnail(outputUrl, task.userId); - - // 更新任务结果 - await this.taskRepository.update(taskId, { - status: TaskStatus.COMPLETED, - outputUrl, - thumbnailUrl, - processingTime, - }); - - // 发送完成通知 - await this.messageProducer.sendGenerationCompleted({ - taskId, - userId: task.userId, - platform: task.platform, - outputUrl, - thumbnailUrl, - }); - - } catch (error) { - // 处理失败,退还积分 - await this.creditService.refundCredits( - task.userId, - task.platform, - task.creditCost, - 'ai_generation_failed', - taskId - ); - - await this.taskRepository.update(taskId, { - status: TaskStatus.FAILED, - errorMessage: error.message, - }); - } - } - - private calculateCreditCost(type: GenerationType, parameters: any): number { - // 根据生成类型和参数计算积分消耗 (与credit-and-ad-system.md保持一致) - let baseCost = type === GenerationType.IMAGE ? 10 : 50; // 图片10积分,视频50积分 - - // 根据参数调整消耗 - if (type === GenerationType.IMAGE) { - // 图片生成:基础10积分,高质量15积分 - if (parameters?.quality === 'high') { - baseCost = 15; - } - } else { - // 视频生成:基础50积分,高质量75积分 - if (parameters?.quality === 'high') { - baseCost = 75; - } - } - - // 分辨率额外消耗 - if (parameters?.resolution === '4k') { - baseCost = Math.ceil(baseCost * 1.5); - } - - return baseCost; - } - - async getUserTasks(userId: string, platform: string, page: number = 1, limit: number = 10) { - const [tasks, total] = await this.taskRepository.findAndCount({ - where: { userId, platform }, - order: { createdAt: 'DESC' }, - skip: (page - 1) * limit, - take: limit, - }); - - return { - tasks, - total, - page, - limit, - totalPages: Math.ceil(total / limit), - }; - } - - async getTaskById(taskId: string, userId: string): Promise { - const task = await this.taskRepository.findOne({ - where: { id: taskId, userId }, - }); - - if (!task) { - throw new Error('任务不存在或无权限访问'); - } - - return task; - } - - private async updateTaskStatus(taskId: string, status: TaskStatus): Promise { - await this.taskRepository.update(taskId, { status }); - } -} -``` - -### 2.2 AI模型适配器服务 -```typescript -// src/services/ai-model.service.ts -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import axios from 'axios'; - -export interface GenerationRequest { - type: 'image' | 'video'; - prompt: string; - inputImageUrl?: string; - parameters?: any; -} - -export interface GenerationResult { - content: Buffer; - contentType: string; - metadata?: any; -} - -@Injectable() -export class AIModelService { - constructor(private readonly configService: ConfigService) {} - - async generate(request: GenerationRequest): Promise { - if (request.type === 'image') { - return this.generateImage(request); - } else { - return this.generateVideo(request); - } - } - - private async generateImage(request: GenerationRequest): Promise { - // 示例:调用Stable Diffusion API - const apiUrl = this.configService.get('STABLE_DIFFUSION_API_URL'); - const apiKey = this.configService.get('STABLE_DIFFUSION_API_KEY'); - - const payload = { - prompt: request.prompt, - negative_prompt: "low quality, blurry, distorted", - width: request.parameters?.width || 512, - height: request.parameters?.height || 512, - steps: request.parameters?.steps || 20, - cfg_scale: request.parameters?.cfg_scale || 7, - sampler_name: request.parameters?.sampler || "DPM++ 2M Karras", - }; - - if (request.inputImageUrl) { - // 图生图模式 - const inputImageBuffer = await this.downloadImage(request.inputImageUrl); - payload['init_images'] = [inputImageBuffer.toString('base64')]; - payload['denoising_strength'] = request.parameters?.denoising_strength || 0.7; - } - - const response = await axios.post(`${apiUrl}/sdapi/v1/txt2img`, payload, { - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - }, - timeout: 120000, // 2分钟超时 - }); - - const imageBase64 = response.data.images[0]; - const imageBuffer = Buffer.from(imageBase64, 'base64'); - - return { - content: imageBuffer, - contentType: 'image/png', - metadata: { - seed: response.data.info?.seed, - parameters: payload, - }, - }; - } - - private async generateVideo(request: GenerationRequest): Promise { - // 示例:调用RunwayML或其他视频生成API - const apiUrl = this.configService.get('VIDEO_GENERATION_API_URL'); - const apiKey = this.configService.get('VIDEO_GENERATION_API_KEY'); - - const payload = { - prompt: request.prompt, - duration: request.parameters?.duration || 5, // 5秒视频 - fps: request.parameters?.fps || 24, - resolution: request.parameters?.resolution || '720p', - }; - - if (request.inputImageUrl) { - payload['image_url'] = request.inputImageUrl; - } - - // 创建生成任务 - const createResponse = await axios.post(`${apiUrl}/generate`, payload, { - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'Content-Type': 'application/json', - }, - }); - - const taskId = createResponse.data.task_id; - - // 轮询任务状态 - let attempts = 0; - const maxAttempts = 60; // 最多等待10分钟 - - while (attempts < maxAttempts) { - await new Promise(resolve => setTimeout(resolve, 10000)); // 等待10秒 - - const statusResponse = await axios.get(`${apiUrl}/task/${taskId}`, { - headers: { 'Authorization': `Bearer ${apiKey}` }, - }); - - if (statusResponse.data.status === 'completed') { - const videoUrl = statusResponse.data.result_url; - const videoBuffer = await this.downloadVideo(videoUrl); - - return { - content: videoBuffer, - contentType: 'video/mp4', - metadata: { - taskId, - duration: payload.duration, - fps: payload.fps, - }, - }; - } else if (statusResponse.data.status === 'failed') { - throw new Error(`视频生成失败: ${statusResponse.data.error}`); - } - - attempts++; - } - - throw new Error('视频生成超时'); - } - - private async downloadImage(url: string): Promise { - const response = await axios.get(url, { responseType: 'arraybuffer' }); - return Buffer.from(response.data); - } - - private async downloadVideo(url: string): Promise { - const response = await axios.get(url, { responseType: 'arraybuffer' }); - return Buffer.from(response.data); - } -} -``` - -## 3. 文件存储服务 - -### 3.1 文件管理服务 -```typescript -// src/services/file.service.ts -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import * as AWS from 'aws-sdk'; -import * as sharp from 'sharp'; -import { v4 as uuidv4 } from 'uuid'; - -@Injectable() -export class FileService { - private s3: AWS.S3; - private bucketName: string; - - constructor(private readonly configService: ConfigService) { - this.s3 = new AWS.S3({ - accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'), - secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'), - region: this.configService.get('AWS_REGION'), - }); - this.bucketName = this.configService.get('AWS_S3_BUCKET'); - } - - async uploadImage(imageBuffer: Buffer, userId: string): Promise { - const fileName = `inputs/${userId}/${uuidv4()}.jpg`; - - // 压缩图片 - const compressedImage = await sharp(imageBuffer) - .jpeg({ quality: 85 }) - .resize(1024, 1024, { fit: 'inside', withoutEnlargement: true }) - .toBuffer(); - - const uploadParams = { - Bucket: this.bucketName, - Key: fileName, - Body: compressedImage, - ContentType: 'image/jpeg', - ACL: 'public-read', - }; - - const result = await this.s3.upload(uploadParams).promise(); - return result.Location; - } - - async uploadGeneratedContent(content: Buffer, userId: string): Promise { - const fileName = `outputs/${userId}/${uuidv4()}.png`; - - const uploadParams = { - Bucket: this.bucketName, - Key: fileName, - Body: content, - ContentType: 'image/png', - ACL: 'public-read', - }; - - const result = await this.s3.upload(uploadParams).promise(); - return result.Location; - } - - async generateThumbnail(imageUrl: string, userId: string): Promise { - // 下载原图 - const response = await fetch(imageUrl); - const imageBuffer = Buffer.from(await response.arrayBuffer()); - - // 生成缩略图 - const thumbnail = await sharp(imageBuffer) - .resize(200, 200, { fit: 'cover' }) - .jpeg({ quality: 80 }) - .toBuffer(); - - const fileName = `thumbnails/${userId}/${uuidv4()}.jpg`; - - const uploadParams = { - Bucket: this.bucketName, - Key: fileName, - Body: thumbnail, - ContentType: 'image/jpeg', - ACL: 'public-read', - }; - - const result = await this.s3.upload(uploadParams).promise(); - return result.Location; - } -} -``` - -## 4. 环境配置 - -### 4.1 AI服务配置 -```env -# Stable Diffusion API -STABLE_DIFFUSION_API_URL=https://api.stability.ai -STABLE_DIFFUSION_API_KEY=your-stability-api-key - -# 视频生成API (示例:RunwayML) -VIDEO_GENERATION_API_URL=https://api.runwayml.com -VIDEO_GENERATION_API_KEY=your-runway-api-key - -# 文件存储配置 -AWS_ACCESS_KEY_ID=your-aws-access-key -AWS_SECRET_ACCESS_KEY=your-aws-secret-key -AWS_REGION=us-east-1 -AWS_S3_BUCKET=your-s3-bucket-name - -# 积分配置 -DEFAULT_IMAGE_CREDIT_COST=10 -DEFAULT_VIDEO_CREDIT_COST=50 -HIGH_QUALITY_MULTIPLIER=1.5 -``` - -## 5. API接口示例 - -### 5.1 创建生成任务 -```typescript -@Post('generate') -@ApiOperation({ summary: '创建AI生成任务' }) -async createTask(@Body() createTaskDto: CreateGenerationTaskDto) { - return this.generationService.createGenerationTask(createTaskDto); -} -``` - -### 5.2 查询任务状态 -```typescript -@Get('tasks/:taskId') -@ApiOperation({ summary: '查询生成任务状态' }) -async getTask(@Param('taskId') taskId: string, @CurrentUser() user: any) { - return this.generationService.getTaskById(taskId, user.id); -} -``` - -### 5.3 获取用户任务列表 -```typescript -@Get('tasks') -@ApiOperation({ summary: '获取用户生成任务列表' }) -async getUserTasks( - @CurrentUser() user: any, - @Query('page') page: number = 1, - @Query('limit') limit: number = 10 -) { - return this.generationService.getUserTasks(user.id, user.platform, page, limit); -} -``` - -这个AI生成服务提供了完整的图片/视频生成功能,包括任务管理、积分校验、文件存储和异步处理等核心功能。 diff --git a/docs/conflict-resolution-summary.md b/docs/conflict-resolution-summary.md deleted file mode 100644 index af60aaa..0000000 --- a/docs/conflict-resolution-summary.md +++ /dev/null @@ -1,166 +0,0 @@ -# 方案冲突解决总结 - -## 🚨 **发现的主要冲突** - -### 1. **模板系统架构冲突** ⚠️ - -**问题**: 主方案文档中包含数据库版本的模板系统设计,与新的面向对象设计冲突。 - -**冲突内容**: -- `ai_templates` 表定义 -- `template_parameters` 表定义 -- `template_versions` 表定义 -- `template_executions` 表定义 - -**解决方案**: ✅ **已解决** -- 从主方案中移除了所有数据库版本的模板表定义 -- 更新了User实体,移除了templateExecutions关系 -- 在数据库迁移中移除了模板相关表的创建 - -### 2. **AI生成服务重复** ⚠️ - -**问题**: `GenerationService` 与模板系统功能重复。 - -**解决方案**: ✅ **已解决** -- 更新了GenerationService,使其与TemplateService集成 -- 添加了 `createGenerationTaskByTemplate()` 方法 -- 通过GenerationTask的metadata字段存储模板信息 - -### 3. **消息队列配置不一致** ⚠️ - -**问题**: 不同文档中的队列配置不一致。 - -**解决方案**: ✅ **已解决** -- 移除了模板专用队列(TEMPLATE_EXECUTION等) -- 统一使用AI_GENERATION_*队列处理模板执行 -- 更新了队列配置说明 - -## 🎯 **统一后的架构** - -### 1. **模板系统** -```typescript -// 面向对象设计,无数据库依赖 -export abstract class Template {} -export class TemplateManager {} -export class TemplateService {} -``` - -### 2. **任务记录** -```typescript -// 使用GenerationTask表记录执行历史 -@Entity('generation_tasks') -export class GenerationTask { - // ... 基础字段 - - @Column({ type: 'jsonb', nullable: true }) - metadata: any; // 存储模板信息 - /* - metadata: { - templateCode: 'outfit_change_v1', - templateName: '智能换装', - templateVersion: '1.0.0', - templateCategory: '换装' - } - */ -} -``` - -### 3. **服务集成** -```typescript -// GenerationService与TemplateService集成 -@Injectable() -export class GenerationService { - constructor( - private readonly templateService: TemplateService, - // ... 其他依赖 - ) {} - - async createGenerationTaskByTemplate(dto: CreateTemplateTaskDto) { - // 1. 通过TemplateService获取模板信息 - // 2. 创建GenerationTask记录 - // 3. 发送到队列处理 - } -} -``` - -## 📊 **更新的文档状态** - -| 文档 | 状态 | 更新内容 | -|------|------|----------| -| `multi-platform-integration-solution.md` | ✅ 已更新 | 移除模板数据库设计,更新实体关系 | -| `template-management-system.md` | ✅ 无冲突 | 面向对象设计,独立完整 | -| `ai-generation-service.md` | ✅ 已更新 | 集成模板系统,更新服务方法 | -| `rabbitmq-configuration.md` | ✅ 已更新 | 添加AI生成队列和消息处理 | -| `credit-and-ad-system.md` | ✅ 无冲突 | 独立的积分和广告系统 | -| `swagger-api-documentation.md` | ✅ 无冲突 | API文档规范 | - -## 🔧 **第二轮发现的冲突** - -### 4. **积分消耗计算不一致** ⚠️ - -**问题**: AI生成服务与积分系统文档中的积分计算规则不一致。 - -**解决方案**: ✅ **已解决** -- 统一了积分计算逻辑:图片基础10积分,高质量15积分 -- 视频基础50积分,高质量75积分 -- 4K分辨率额外1.5倍消耗 - -### 5. **RabbitMQ队列配置缺失** ⚠️ - -**问题**: RabbitMQ配置缺少AI生成相关队列。 - -**解决方案**: ✅ **已解决** -- 添加了AI_GENERATION_TASK队列 -- 添加了AI_GENERATION_COMPLETED队列 -- 添加了AI_GENERATION_FAILED队列 -- 添加了对应的消息接口和处理器 - -### 6. **消息接口不一致** ⚠️ - -**问题**: AI生成服务使用了未定义的消息方法。 - -**解决方案**: ✅ **已解决** -- 统一使用sendGenerationTask方法 -- 添加了完整的消息接口定义 -- 添加了消息消费者处理逻辑 - -## ✅ **解决方案的优势** - -### 1. **架构一致性** -- 所有文档现在都采用面向对象的模板系统 -- 数据库设计简化,只保留必要的业务表 -- 服务间职责清晰,避免重复 - -### 2. **类型安全** -- TypeScript编译时检查模板定义 -- 统一的接口和数据结构 -- IDE完整支持 - -### 3. **易于维护** -- 模板定义即代码,版本控制友好 -- 无需数据库迁移添加新模板 -- 测试和部署更简单 - -## ✅ **所有冲突已解决** - -经过两轮详细检查和修复,所有文档现在完全一致: - -### 🎯 **统一的架构** -1. **模板系统**: 面向对象设计,无数据库依赖 -2. **积分计算**: 统一的积分消耗规则 -3. **消息队列**: 完整的AI生成任务处理流程 -4. **服务集成**: GenerationService与TemplateService无缝协作 - -### 📊 **最终文档状态** -| 文档 | 状态 | 说明 | -|------|------|------| -| `multi-platform-integration-solution.md` | ✅ 完全一致 | 主架构方案 | -| `template-management-system.md` | ✅ 完全一致 | 面向对象模板系统 | -| `ai-generation-service.md` | ✅ 完全一致 | 集成模板系统 | -| `rabbitmq-configuration.md` | ✅ 完全一致 | 完整队列配置 | -| `credit-and-ad-system.md` | ✅ 完全一致 | 积分和广告系统 | -| `swagger-api-documentation.md` | ✅ 完全一致 | API文档规范 | - -## 🚀 **可以开始开发了!** - -所有方案文档现在完全一致,没有任何冲突,可以安全地开始实施开发! diff --git a/docs/rabbitmq-configuration.md b/docs/rabbitmq-configuration.md deleted file mode 100644 index a6db204..0000000 --- a/docs/rabbitmq-configuration.md +++ /dev/null @@ -1,508 +0,0 @@ -# RabbitMQ 配置指南 - -## 1. RabbitMQ 模块配置 - -### 1.1 RabbitMQ 模块设置 -```typescript -// src/rabbitmq/rabbitmq.module.ts -import { Module } from '@nestjs/common'; -import { ClientsModule, Transport } from '@nestjs/microservices'; -import { ConfigService } from '@nestjs/config'; - -@Module({ - imports: [ - ClientsModule.registerAsync([ - { - name: 'RABBITMQ_SERVICE', - useFactory: (configService: ConfigService) => ({ - transport: Transport.RMQ, - options: { - urls: [configService.get('RABBITMQ_URL')], - queue: 'main_queue', - queueOptions: { - durable: true, - }, - socketOptions: { - heartbeatIntervalInSeconds: 60, - reconnectTimeInSeconds: 5, - }, - }, - }), - inject: [ConfigService], - }, - ]), - ], - exports: [ClientsModule], -}) -export class RabbitMQModule {} -``` - -### 1.2 消息队列配置 -```typescript -// src/config/queue.config.ts -export const QueueConfig = { - // 交换机配置 - exchanges: { - USER: 'user.exchange', - PAYMENT: 'payment.exchange', - PUSH: 'push.exchange', - PLATFORM: 'platform.exchange', - }, - - // 队列配置 - queues: { - // 用户相关队列 - USER_REGISTRATION: 'user.registration.queue', - USER_LOGIN: 'user.login.queue', - USER_UPDATE: 'user.update.queue', - - // AI生成相关队列 - AI_GENERATION_TASK: 'ai.generation.task.queue', - AI_GENERATION_COMPLETED: 'ai.generation.completed.queue', - AI_GENERATION_FAILED: 'ai.generation.failed.queue', - - // 积分系统队列 - CREDIT_REWARD: 'credit.reward.queue', - CREDIT_CONSUME: 'credit.consume.queue', - - // 广告系统队列 - AD_WATCH_COMPLETED: 'ad.watch.completed.queue', - AD_REWARD_GRANTED: 'ad.reward.granted.queue', - - // 平台同步队列 - PLATFORM_SYNC_USER: 'platform.sync.user.queue', - PLATFORM_SYNC_DATA: 'platform.sync.data.queue', - - // 扩展服务队列 (预留) - EXTENSION_SERVICE: 'extension.service.queue', - CUSTOM_EVENT: 'custom.event.queue', - }, - - // 路由键配置 - routingKeys: { - USER_REGISTERED: 'user.registered', - USER_LOGIN_SUCCESS: 'user.login.success', - USER_UPDATED: 'user.updated', - - // AI生成相关路由键 - AI_GENERATION_TASK: 'ai.generation.task', - AI_GENERATION_COMPLETED: 'ai.generation.completed', - AI_GENERATION_FAILED: 'ai.generation.failed', - - // 积分系统路由键 - CREDIT_REWARD: 'credit.reward', - CREDIT_CONSUME: 'credit.consume', - - // 广告系统路由键 - AD_WATCH_COMPLETED: 'ad.watch.completed', - AD_REWARD_GRANTED: 'ad.reward.granted', - - PLATFORM_SYNC: 'platform.sync', - EXTENSION_EVENT: 'extension.event', - }, -}; -``` - -## 2. 消息生产者服务 - -```typescript -// src/services/message-producer.service.ts -import { Injectable, Inject } from '@nestjs/common'; -import { ClientProxy } from '@nestjs/microservices'; -import { QueueConfig } from '../config/queue.config'; - -export interface UserRegistrationMessage { - userId: string; - platform: string; - userInfo: any; - timestamp: Date; -} - -export interface ExtensionMessage { - userId: string; - platform: string; - dataType: string; - action: string; // 'create', 'update', 'delete' - data: Record; - timestamp: Date; -} - -export interface GenerationTaskMessage { - taskId: string; - userId: string; - platform: string; - type: 'image' | 'video'; - templateCode?: string; - inputParameters: Record; - timestamp: Date; -} - -export interface GenerationCompletedMessage { - taskId: string; - userId: string; - platform: string; - outputUrl: string; - thumbnailUrl?: string; - timestamp: Date; -} - -export interface GenerationFailedMessage { - taskId: string; - userId: string; - platform: string; - errorMessage: string; - timestamp: Date; -} - -@Injectable() -export class MessageProducerService { - constructor( - @Inject('RABBITMQ_SERVICE') - private readonly client: ClientProxy, - ) {} - - // 发送用户注册消息 - async sendUserRegistration(message: UserRegistrationMessage) { - return this.client.emit(QueueConfig.routingKeys.USER_REGISTERED, message); - } - - // 发送用户更新消息 - async sendUserUpdate(message: UserRegistrationMessage) { - return this.client.emit(QueueConfig.routingKeys.USER_UPDATED, message); - } - - // 发送平台同步消息 - async sendPlatformSync(data: any) { - return this.client.emit(QueueConfig.routingKeys.PLATFORM_SYNC, data); - } - - // 发送扩展服务消息 - async sendExtensionEvent(message: ExtensionMessage) { - return this.client.emit(QueueConfig.routingKeys.EXTENSION_EVENT, message); - } - - // AI生成相关消息 - async sendGenerationTask(message: GenerationTaskMessage) { - return this.client.emit(QueueConfig.routingKeys.AI_GENERATION_TASK, message); - } - - async sendGenerationCompleted(message: GenerationCompletedMessage) { - return this.client.emit(QueueConfig.routingKeys.AI_GENERATION_COMPLETED, message); - } - - async sendGenerationFailed(message: GenerationFailedMessage) { - return this.client.emit(QueueConfig.routingKeys.AI_GENERATION_FAILED, message); - } -} -``` - -## 3. 消息消费者服务 - -```typescript -// src/services/message-consumer.service.ts -import { Injectable } from '@nestjs/common'; -import { EventPattern, Payload } from '@nestjs/microservices'; -import { QueueConfig } from '../config/queue.config'; -import { UserService } from './user.service'; -import { PaymentService } from './payment.service'; -import { PushService } from './push.service'; -import { PlatformService } from './platform.service'; - -@Injectable() -export class MessageConsumerService { - constructor( - private readonly userService: UserService, - private readonly platformService: PlatformService, - private readonly extensionService: ExtensionService, - private readonly generationService: GenerationService, - private readonly messageProducer: MessageProducerService, - ) {} - - // 处理用户注册消息 - @EventPattern(QueueConfig.routingKeys.USER_REGISTERED) - async handleUserRegistration(@Payload() data: UserRegistrationMessage) { - try { - console.log('Processing user registration:', data); - - // 同步到其他平台 - await this.platformService.syncUserRegistration(data); - - // 触发扩展服务处理 - await this.extensionService.handleUserRegistration(data); - - console.log('User registration processed successfully'); - } catch (error) { - console.error('Error processing user registration:', error); - // 这里可以实现重试机制或死信队列 - } - } - - // 处理用户更新消息 - @EventPattern(QueueConfig.routingKeys.USER_UPDATED) - async handleUserUpdate(@Payload() data: UserRegistrationMessage) { - try { - console.log('Processing user update:', data); - - // 同步用户数据到各平台 - await this.platformService.syncUserUpdate(data); - - // 触发扩展服务处理 - await this.extensionService.handleUserUpdate(data); - - console.log('User update processed successfully'); - } catch (error) { - console.error('Error processing user update:', error); - } - } - - // 处理AI生成任务消息 - @EventPattern(QueueConfig.routingKeys.AI_GENERATION_TASK) - async handleGenerationTask(@Payload() data: GenerationTaskMessage) { - try { - console.log('Processing AI generation task:', data); - - // 调用AI生成服务处理任务 - await this.generationService.processGenerationTask(data.taskId); - - console.log('AI generation task processed successfully'); - } catch (error) { - console.error('Error processing AI generation task:', error); - - // 发送失败消息 - await this.messageProducer.sendGenerationFailed({ - taskId: data.taskId, - userId: data.userId, - platform: data.platform, - errorMessage: error.message, - timestamp: new Date(), - }); - } - } - - // 处理AI生成完成消息 - @EventPattern(QueueConfig.routingKeys.AI_GENERATION_COMPLETED) - async handleGenerationCompleted(@Payload() data: GenerationCompletedMessage) { - try { - console.log('Processing AI generation completed:', data); - - // 可以在这里添加后续处理逻辑,如推送通知等 - // await this.pushService.sendGenerationCompleteNotification(data); - - console.log('AI generation completed processed successfully'); - } catch (error) { - console.error('Error processing AI generation completed:', error); - } - } - - // 处理AI生成失败消息 - @EventPattern(QueueConfig.routingKeys.AI_GENERATION_FAILED) - async handleGenerationFailed(@Payload() data: GenerationFailedMessage) { - try { - console.log('Processing AI generation failed:', data); - - // 可以在这里添加失败处理逻辑,如推送错误通知等 - // await this.pushService.sendGenerationFailedNotification(data); - - console.log('AI generation failed processed successfully'); - } catch (error) { - console.error('Error processing AI generation failed:', error); - } - } - - // 处理扩展服务消息 - @EventPattern(QueueConfig.routingKeys.EXTENSION_EVENT) - async handleExtensionEvent(@Payload() data: ExtensionMessage) { - try { - console.log('Processing extension event:', data); - - // 根据数据类型和操作类型处理 - await this.extensionService.handleExtensionEvent(data); - - // 同步到相关平台 - await this.platformService.syncExtensionData(data); - - console.log('Extension event processed successfully'); - } catch (error) { - console.error('Error processing extension event:', error); - } - } - - // 处理平台同步消息 - @EventPattern(QueueConfig.routingKeys.PLATFORM_SYNC) - async handlePlatformSync(@Payload() data: any) { - try { - console.log('Processing platform sync:', data); - - await this.platformService.syncData(data); - - console.log('Platform sync processed successfully'); - } catch (error) { - console.error('Error processing platform sync:', error); - } - } -} -``` - -## 4. 在业务服务中使用消息队列 - -```typescript -// src/services/user.service.ts -import { Injectable } from '@nestjs/common'; -import { MessageProducerService } from './message-producer.service'; - -@Injectable() -export class UserService { - constructor( - private readonly messageProducer: MessageProducerService, - ) {} - - async registerUser(userData: any) { - // 创建用户 - const user = await this.createUser(userData); - - // 发送用户注册消息到队列 - await this.messageProducer.sendUserRegistration({ - userId: user.id, - platform: userData.platform, - userInfo: userData, - timestamp: new Date(), - }); - - return user; - } - - private async createUser(userData: any) { - // 实际的用户创建逻辑 - return { id: 'user-id', ...userData }; - } -} -``` - -## 5. 主应用模块配置 - -```typescript -// src/app.module.ts -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { RabbitMQModule } from './rabbitmq/rabbitmq.module'; -import { MessageProducerService } from './services/message-producer.service'; -import { MessageConsumerService } from './services/message-consumer.service'; - -@Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - }), - RabbitMQModule, - ], - providers: [ - MessageProducerService, - MessageConsumerService, - ], - exports: [MessageProducerService], -}) -export class AppModule {} -``` - -## 6. 错误处理和重试机制 - -```typescript -// src/decorators/retry.decorator.ts -export function Retry(maxRetries: number = 3, delay: number = 1000) { - return function (target: any, propertyName: string, descriptor: PropertyDescriptor) { - const method = descriptor.value; - - descriptor.value = async function (...args: any[]) { - let lastError: any; - - for (let i = 0; i <= maxRetries; i++) { - try { - return await method.apply(this, args); - } catch (error) { - lastError = error; - - if (i < maxRetries) { - console.log(`Retry ${i + 1}/${maxRetries} for ${propertyName}:`, error.message); - await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i))); - } - } - } - - throw lastError; - }; - }; -} -``` - -## 7. 队列监控和管理 - -```typescript -// src/services/queue-monitor.service.ts -import { Injectable } from '@nestjs/common'; -import * as amqp from 'amqplib'; - -@Injectable() -export class QueueMonitorService { - private connection: amqp.Connection; - - async getQueueInfo(queueName: string) { - const channel = await this.connection.createChannel(); - const queueInfo = await channel.checkQueue(queueName); - await channel.close(); - - return { - queue: queueName, - messageCount: queueInfo.messageCount, - consumerCount: queueInfo.consumerCount, - }; - } - - async purgeQueue(queueName: string) { - const channel = await this.connection.createChannel(); - await channel.purgeQueue(queueName); - await channel.close(); - } -} -``` - -## 8. Docker Compose 配置 - -```yaml -# docker-compose.yml -version: '3.8' -services: - rabbitmq: - image: rabbitmq:3-management-alpine - container_name: rabbitmq - environment: - RABBITMQ_DEFAULT_USER: admin - RABBITMQ_DEFAULT_PASS: password - RABBITMQ_DEFAULT_VHOST: / - ports: - - "5672:5672" # AMQP端口 - - "15672:15672" # 管理界面端口 - volumes: - - rabbitmq_data:/var/lib/rabbitmq - healthcheck: - test: ["CMD", "rabbitmq-diagnostics", "ping"] - interval: 30s - timeout: 10s - retries: 5 - -volumes: - rabbitmq_data: -``` - -## 9. 环境变量配置 - -```env -# RabbitMQ配置 -RABBITMQ_URL=amqp://admin:password@localhost:5672 -RABBITMQ_USER=admin -RABBITMQ_PASSWORD=password -RABBITMQ_VHOST=/ -RABBITMQ_HEARTBEAT=60 -RABBITMQ_RECONNECT_TIME=5 -``` - -这个配置提供了完整的RabbitMQ集成方案,包括消息生产者、消费者、错误处理和监控功能。 diff --git a/docs/template-hybrid-architecture.md b/docs/template-hybrid-architecture.md new file mode 100644 index 0000000..1eecb4f --- /dev/null +++ b/docs/template-hybrid-architecture.md @@ -0,0 +1,501 @@ +# AI模板管理系统 - 混合式架构设计 + +## 🎯 核心设计理念 + +基于现有的优雅抽象类设计,实现**代码定义逻辑 + 数据库存储配置**的混合式架构: + +- **代码层**:抽象类定义执行逻辑和接口规范 +- **数据库层**:存储具体模板的配置信息 +- **Entity层**:动态从数据库加载配置,创建模板实例 + +## 🏗️ 架构层次分析 + +### 现有代码结构 +``` +Template (公共属性抽象类) [src/templates/types.ts:4-13] +├── ImageGenerateTemplate (图片生成抽象) [src/templates/types.ts:18-20] +│ └── N8nImageGenerateTemplate (N8n图片实现) [src/templates/n8nTemplate.ts:4-32] +│ ├── PhotoRestoreTemplate (具体配置) [src/templates/n8nTemplates/PhotoRestoreTemplate.ts] +│ ├── PetFigurineTemplate (具体配置) +│ └── CosplayRealPersonTemplate (具体配置) +└── VideoGenerateTemplate (视频生成抽象) [src/templates/types.ts:23-25] + └── N8nVideoGenerateTemplate (N8n视频实现) [src/templates/n8nTemplate.ts:35-74] + ├── CharacterFigurineTemplate (具体配置) [src/templates/n8nTemplates/CharacterFigurineTemplate.ts] + ├── GarageOpeningTemplate (具体配置) + └── NuclearExplosionTemplate (具体配置) +``` + +### 设计要点 +1. **抽象类保持不变**:Template、ImageGenerateTemplate、VideoGenerateTemplate、N8nTemplate系列 +2. **具体配置存数据库**:PhotoRestoreTemplate、CharacterFigurineTemplate等的配置信息 +3. **动态实例化**:通过Entity层从数据库加载配置,创建模板实例 + +## 📊 数据库架构设计 + +### 1. 模板配置表 (n8n_templates) +```sql +CREATE TABLE n8n_templates ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + + -- 基础配置 (对应Template抽象类) + code VARCHAR(100) NOT NULL UNIQUE COMMENT '模板唯一标识码', + name VARCHAR(200) NOT NULL COMMENT '模板名称', + description TEXT COMMENT '模板详细描述', + credit_cost INT NOT NULL DEFAULT 0 COMMENT '积分消耗', + version VARCHAR(50) NOT NULL COMMENT '版本号', + input_example_url TEXT COMMENT '输入示例图片', + output_example_url TEXT COMMENT '输出示例图片', + tags JSON COMMENT '标签数组', + + -- 模板类型配置 + template_type ENUM('image', 'video') NOT NULL COMMENT '模板类型', + template_class VARCHAR(100) NOT NULL COMMENT '具体模板类名', + + -- N8n配置 (对应N8nTemplate系列) + image_model VARCHAR(100) COMMENT '图片生成模型', + image_prompt TEXT COMMENT '图片生成提示词', + video_model VARCHAR(100) COMMENT '视频生成模型', + video_prompt TEXT COMMENT '视频生成提示词', + duration INT COMMENT '视频时长(秒)', + aspect_ratio VARCHAR(20) COMMENT '视频比例', + + -- 系统字段 + is_active BOOLEAN DEFAULT TRUE COMMENT '是否启用', + sort_order INT DEFAULT 0 COMMENT '排序权重', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + KEY idx_code (code), + KEY idx_type (template_type), + KEY idx_class (template_class), + KEY idx_active (is_active) +) COMMENT 'N8n模板配置表'; +``` + +### 2. 初始化数据示例 +```sql +INSERT INTO n8n_templates ( + code, name, description, credit_cost, version, + input_example_url, output_example_url, tags, + template_type, template_class, + image_model, image_prompt, + video_model, video_prompt, duration, aspect_ratio +) VALUES +( + 'photo_restore_v1', + '老照片修复上色', + '将黑白老照片修复并上色,让历史照片重现生机', + 12, '1.0.0', + 'https://file.302.ai/gpt/imgs/20250903/6033b7e701a64e4a97d4608d4a0ed308.jpg', + 'https://file.302.ai/gpt/imgs/20250903/07ed5fb40090ac6ff51c874cc75ea8f2.jpg', + '["老照片", "修复", "上色", "黑白转彩色", "AI修复"]', + 'image', 'PhotoRestoreTemplate', + 'gemini-2.5-flash-image-preview', + 'Restore this old photo and colorize the entire black and white photo', + NULL, NULL, NULL, NULL +), +( + 'character_figurine_v1', + '人物手办', + '将人物照片制作成精细的角色手办模型,展示在收藏家房间中,并生成抚摸手办的视频', + 28, '1.0.0', + 'https://cdn.roasmax.cn/upload/3d590851eb584e92aa415a964e93260e.jpg', + 'https://file.302.ai/gpt/imgs/20250828/2283106b31faf2066e1a72d955f65bca.jpg', + '["人物", "手办", "模型", "收藏", "PVC", "角色模型", "视频生成"]', + 'video', 'CharacterFigurineTemplate', + 'gemini-2.5-flash-image-preview', + 'Transform this photo into a highly detailed character model...', + '302/MiniMax-Hailuo-02', + '一只手摸了摸手办的脑袋', + 6, '9:16' +); +``` + +## 💾 Entity层设计 + +### 1. 模板配置实体 +```typescript +// src/entities/n8n-template.entity.ts +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; + +export enum TemplateType { + IMAGE = 'image', + VIDEO = 'video' +} + +@Entity('n8n_templates') +export class N8nTemplateEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ unique: true }) + code: string; + + @Column() + name: string; + + @Column('text') + description: string; + + @Column({ name: 'credit_cost' }) + creditCost: number; + + @Column() + version: string; + + @Column({ name: 'input_example_url', nullable: true }) + inputExampleUrl: string; + + @Column({ name: 'output_example_url', nullable: true }) + outputExampleUrl: string; + + @Column('json', { nullable: true }) + tags: string[]; + + @Column({ name: 'template_type', type: 'enum', enum: TemplateType }) + templateType: TemplateType; + + @Column({ name: 'template_class' }) + templateClass: string; + + @Column({ name: 'image_model', nullable: true }) + imageModel: string; + + @Column({ name: 'image_prompt', type: 'text', nullable: true }) + imagePrompt: string; + + @Column({ name: 'video_model', nullable: true }) + videoModel: string; + + @Column({ name: 'video_prompt', type: 'text', nullable: true }) + videoPrompt: string; + + @Column({ nullable: true }) + duration: number; + + @Column({ name: 'aspect_ratio', nullable: true }) + aspectRatio: string; + + @Column({ name: 'is_active', default: true }) + isActive: boolean; + + @Column({ name: 'sort_order', default: 0 }) + sortOrder: number; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; +} +``` + +### 2. 动态模板实体类 +```typescript +// src/entities/n8n-template-instance.entity.ts +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { N8nTemplateEntity, TemplateType } from './n8n-template.entity'; +import { N8nImageGenerateTemplate, N8nVideoGenerateTemplate } from '../templates/n8nTemplate'; +import axios from 'axios'; + +// 动态图片模板实体 +export class N8nImageGenerateTemplateEntity extends N8nImageGenerateTemplate { + private config: N8nTemplateEntity; + + constructor( + private templateId: number, + private templateRepository: Repository + ) { + super(); + } + + async onInit(): Promise { + this.config = await this.templateRepository.findOne({ + where: { id: this.templateId, templateType: TemplateType.IMAGE, isActive: true } + }); + + if (!this.config) { + throw new Error(`Template with id ${this.templateId} not found`); + } + + return this; + } + + // 从数据库动态获取配置 + get code(): string { return this.config?.code || ''; } + get name(): string { return this.config?.name || ''; } + get description(): string { return this.config?.description || ''; } + get creditCost(): number { return this.config?.creditCost || 0; } + get version(): string { return this.config?.version || ''; } + get input(): string { return this.config?.inputExampleUrl || ''; } + get output(): string { return this.config?.outputExampleUrl || ''; } + get tags(): string[] { return this.config?.tags || []; } + get imageModel(): string { return this.config?.imageModel || ''; } + get imagePrompt(): string { return this.config?.imagePrompt || ''; } + + // 执行方法继承自N8nImageGenerateTemplate,无需修改 +} + +// 动态视频模板实体 +export class N8nVideoGenerateTemplateEntity extends N8nVideoGenerateTemplate { + private config: N8nTemplateEntity; + + constructor( + private templateId: number, + private templateRepository: Repository + ) { + super(); + } + + async onInit(): Promise { + this.config = await this.templateRepository.findOne({ + where: { id: this.templateId, templateType: TemplateType.VIDEO, isActive: true } + }); + + if (!this.config) { + throw new Error(`Template with id ${this.templateId} not found`); + } + + return this; + } + + // 从数据库动态获取配置 + get code(): string { return this.config?.code || ''; } + get name(): string { return this.config?.name || ''; } + get description(): string { return this.config?.description || ''; } + get creditCost(): number { return this.config?.creditCost || 0; } + get version(): string { return this.config?.version || ''; } + get input(): string { return this.config?.inputExampleUrl || ''; } + get output(): string { return this.config?.outputExampleUrl || ''; } + get tags(): string[] { return this.config?.tags || []; } + get imageModel(): string { return this.config?.imageModel || ''; } + get imagePrompt(): string { return this.config?.imagePrompt || ''; } + get videoModel(): string { return this.config?.videoModel || ''; } + get videoPrompt(): string { return this.config?.videoPrompt || ''; } + get duration(): number { return this.config?.duration || 6; } + get aspectRatio(): string { return this.config?.aspectRatio || '9:16'; } + + // 执行方法继承自N8nVideoGenerateTemplate,无需修改 +} +``` + +### 3. 模板工厂服务 +```typescript +// src/services/n8n-template-factory.service.ts +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { N8nTemplateEntity, TemplateType } from '../entities/n8n-template.entity'; +import { N8nImageGenerateTemplateEntity, N8nVideoGenerateTemplateEntity } from '../entities/n8n-template-instance.entity'; + +@Injectable() +export class N8nTemplateFactoryService { + constructor( + @InjectRepository(N8nTemplateEntity) + private templateRepository: Repository, + ) {} + + // 创建图片模板实例 + async createImageTemplate(templateId: number): Promise { + const instance = new N8nImageGenerateTemplateEntity(templateId, this.templateRepository); + return await instance.onInit(); + } + + // 创建视频模板实例 + async createVideoTemplate(templateId: number): Promise { + const instance = new N8nVideoGenerateTemplateEntity(templateId, this.templateRepository); + return await instance.onInit(); + } + + // 通过模板代码创建实例 + async createTemplateByCode(code: string): Promise { + const config = await this.templateRepository.findOne({ + where: { code, isActive: true } + }); + + if (!config) { + throw new Error(`Template with code ${code} not found`); + } + + if (config.templateType === TemplateType.IMAGE) { + return this.createImageTemplate(config.id); + } else { + return this.createVideoTemplate(config.id); + } + } + + // 获取所有可用模板 + async getAllTemplates(): Promise { + return this.templateRepository.find({ + where: { isActive: true }, + order: { sortOrder: 'DESC', createdAt: 'DESC' } + }); + } + + // 按类型获取模板 + async getTemplatesByType(type: TemplateType): Promise { + return this.templateRepository.find({ + where: { templateType: type, isActive: true }, + order: { sortOrder: 'DESC', createdAt: 'DESC' } + }); + } +} +``` + +## 🚀 使用示例 + +### 1. 您期望的使用方式 +```typescript +// 通过ID创建并执行视频模板 +const template = await new N8nVideoGenerateTemplateEntity(templateId, templateRepo) + .onInit() + .then(e => e.execute(imgUrl)); + +// 通过工厂服务创建 +const factory = new N8nTemplateFactoryService(templateRepo); +const template = await factory.createVideoTemplate(1); +const result = await template.execute(imageUrl); +``` + +### 2. 在Controller中的使用 +```typescript +// src/controllers/template.controller.ts +@Controller('templates') +export class TemplateController { + constructor( + private readonly templateFactory: N8nTemplateFactoryService, + ) {} + + @Post(':templateId/execute') + async executeTemplate( + @Param('templateId', ParseIntPipe) templateId: number, + @Body() { imageUrl }: { imageUrl: string } + ) { + // 动态创建模板实例并执行 + const config = await this.templateFactory.templateRepository.findOne({ + where: { id: templateId } + }); + + let template; + if (config.templateType === TemplateType.IMAGE) { + template = await this.templateFactory.createImageTemplate(templateId); + } else { + template = await this.templateFactory.createVideoTemplate(templateId); + } + + return await template.execute(imageUrl); + } + + @Get() + async getAllTemplates() { + return this.templateFactory.getAllTemplates(); + } + + @Get('image') + async getImageTemplates() { + return this.templateFactory.getTemplatesByType(TemplateType.IMAGE); + } + + @Get('video') + async getVideoTemplates() { + return this.templateFactory.getTemplatesByType(TemplateType.VIDEO); + } +} +``` + +### 3. 数据迁移脚本 +```typescript +// 将现有代码中的模板配置迁移到数据库 +// src/migrations/migrate-templates.ts +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { N8nTemplateEntity, TemplateType } from '../entities/n8n-template.entity'; + +// 导入现有模板 +import { PhotoRestoreTemplate } from '../templates/n8nTemplates/PhotoRestoreTemplate'; +import { CharacterFigurineTemplate } from '../templates/n8nTemplates/CharacterFigurineTemplate'; + +@Injectable() +export class TemplateMigrationService { + constructor( + @InjectRepository(N8nTemplateEntity) + private templateRepository: Repository, + ) {} + + async migrateExistingTemplates() { + // 迁移现有模板配置 + const templates = [ + new PhotoRestoreTemplate(), + new CharacterFigurineTemplate(), + // ... 其他模板 + ]; + + for (const template of templates) { + const existing = await this.templateRepository.findOne({ + where: { code: template.code } + }); + + if (!existing) { + const entity = new N8nTemplateEntity(); + entity.code = template.code; + entity.name = template.name; + entity.description = template.description; + entity.creditCost = template.creditCost; + entity.version = template.version; + entity.inputExampleUrl = template.input; + entity.outputExampleUrl = template.output; + entity.tags = template.tags; + + // 判断模板类型 + if (template instanceof N8nImageGenerateTemplate) { + entity.templateType = TemplateType.IMAGE; + entity.imageModel = template.imageModel; + entity.imagePrompt = template.imagePrompt; + } else if (template instanceof N8nVideoGenerateTemplate) { + entity.templateType = TemplateType.VIDEO; + entity.imageModel = template.imageModel; + entity.imagePrompt = template.imagePrompt; + entity.videoModel = template.videoModel; + entity.videoPrompt = template.videoPrompt; + entity.duration = template.duration; + entity.aspectRatio = template.aspectRatio; + } + + entity.templateClass = template.constructor.name; + + await this.templateRepository.save(entity); + } + } + } +} +``` + +## ✅ 架构优势 + +### 1. 保持代码优势 +- **类型安全**:继承现有抽象类,保持TypeScript类型检查 +- **执行逻辑不变**:N8nTemplate的execute方法逻辑完全保持不变 +- **抽象层次清晰**:Template → ImageGenerate/VideoGenerate → N8nTemplate → 具体实现 + +### 2. 新增数据库能力 +- **配置动态化**:模板配置存储在数据库中,可在线修改 +- **运营友好**:通过管理后台可以调整模板参数、启用/禁用模板 +- **A/B测试**:可以创建同一功能的不同版本进行测试 +- **统计分析**:可以跟踪每个模板的使用情况和效果 + +### 3. 使用体验优化 +```typescript +// 简洁的API调用 +const result = await new N8nVideoGenerateTemplateEntity(id, repo) + .onInit() + .then(e => e.execute(imgUrl)); + +// 工厂模式调用 +const template = await templateFactory.createTemplateByCode('character_figurine_v1'); +const result = await template.execute(imageUrl); +``` + +这种设计完美融合了您的面向对象架构优势和数据库配置管理的灵活性! \ No newline at end of file diff --git a/docs/template-management-system.md b/docs/template-management-system.md deleted file mode 100644 index 2501e18..0000000 --- a/docs/template-management-system.md +++ /dev/null @@ -1,1668 +0,0 @@ -# AI模板管理系统设计方案 (面向对象版本) - -## 1. 系统概述 - -### 1.1 核心目标 -- **统一接口**: 通过抽象类抹平不同AI模型和API的调用差异 -- **模板化管理**: 将复杂的AI调用封装为简单的模板类 -- **类型安全**: 利用TypeScript的类型系统确保参数和返回值的正确性 -- **易于扩展**: 通过继承轻松添加新的模板类型 - -### 1.2 设计优势 -- **编译时检查**: TypeScript编译时就能发现接口不匹配问题 -- **代码复用**: 通过继承实现通用逻辑的复用 -- **类型推导**: IDE能提供完整的代码提示和类型检查 -- **易于测试**: 每个模板类都可以独立进行单元测试 - -### 1.3 模板类型示例 -- **换装模板**: 图片 + 服装描述 → 换装后图片 -- **抠图模板**: 图片 + 物体描述 → 抠图视频 -- **风格转换**: 图片 + 风格描述 → 风格化图片 -- **背景替换**: 图片 + 背景描述 → 新背景图片 - -## 2. 核心抽象类设计 - -### 2.1 基础模板抽象类 -```typescript -// src/templates/base/template.abstract.ts -export interface TemplateMetadata { - code: string; - name: string; - description: string; - category: string; - creditCost: number; - version: string; - previewImageUrl?: string; -} - -export interface TemplateExecutionContext { - userId: string; - platform: string; - requestId: string; - timestamp: Date; -} - -export interface TemplateExecutionResult { - success: boolean; - data?: T; - error?: string; - metadata: { - executionTime: number; - creditCost: number; - templateCode: string; - aiProvider?: string; - }; -} - -export abstract class Template { - // 模板元数据 - abstract readonly metadata: TemplateMetadata; - - // 模板是否启用 - protected _enabled: boolean = true; - - constructor() { - this.validateTemplate(); - } - - // 获取模板信息 - getMetadata(): TemplateMetadata { - return { ...this.metadata }; - } - - // 检查模板是否启用 - isEnabled(): boolean { - return this._enabled; - } - - // 启用/禁用模板 - setEnabled(enabled: boolean): void { - this._enabled = enabled; - } - - // 验证输入参数 - 子类必须实现 - abstract validateInput(input: TInput): Promise; - - // 执行模板 - 子类必须实现 - abstract execute(input: TInput, context: TemplateExecutionContext): Promise; - - // 获取参数定义 - 用于前端生成表单 - abstract getParameterDefinitions(): ParameterDefinition[]; - - // 模板执行入口 - 统一的执行流程 - async run(input: TInput, context: TemplateExecutionContext): Promise> { - const startTime = Date.now(); - - try { - // 检查模板是否启用 - if (!this.isEnabled()) { - throw new Error(`模板 ${this.metadata.code} 已禁用`); - } - - // 验证输入参数 - const validationErrors = await this.validateInput(input); - if (validationErrors.length > 0) { - throw new Error(`参数验证失败: ${validationErrors.join(', ')}`); - } - - // 执行模板 - const result = await this.execute(input, context); - const executionTime = Date.now() - startTime; - - return { - success: true, - data: result, - metadata: { - executionTime, - creditCost: this.metadata.creditCost, - templateCode: this.metadata.code, - } - }; - - } catch (error) { - const executionTime = Date.now() - startTime; - - return { - success: false, - error: error.message, - metadata: { - executionTime, - creditCost: 0, // 失败不扣积分 - templateCode: this.metadata.code, - } - }; - } - } - - // 验证模板定义的完整性 - private validateTemplate(): void { - if (!this.metadata.code) { - throw new Error('模板代码不能为空'); - } - if (!this.metadata.name) { - throw new Error('模板名称不能为空'); - } - if (this.metadata.creditCost < 0) { - throw new Error('积分消耗不能为负数'); - } - } -} -``` - -### 2.2 参数定义接口 -```typescript -// src/templates/base/parameter.interface.ts -export enum ParameterType { - STRING = 'string', - NUMBER = 'number', - BOOLEAN = 'boolean', - IMAGE = 'image', - SELECT = 'select', - RANGE = 'range', - ARRAY = 'array' -} - -export interface ParameterValidation { - required?: boolean; - min?: number; - max?: number; - options?: string[]; - pattern?: string; - minLength?: number; - maxLength?: number; -} - -export interface ParameterDefinition { - key: string; - displayName: string; - description: string; - type: ParameterType; - defaultValue?: any; - validation?: ParameterValidation; - order: number; -} -``` - -### 2.3 图片生成模板抽象类 -```typescript -// src/templates/base/image-generate-template.abstract.ts -import { Template, TemplateExecutionContext } from './template.abstract'; - -export interface ImageGenerateInput { - prompt: string; - inputImage?: string; // base64 或 URL - width?: number; - height?: number; - quality?: 'standard' | 'high' | 'ultra'; - style?: string; - [key: string]: any; // 允许扩展参数 -} - -export interface ImageGenerateOutput { - images: Array<{ - url: string; - thumbnailUrl?: string; - width: number; - height: number; - format: string; - }>; - seed?: number; - parameters?: Record; -} - -export abstract class ImageGenerateTemplate extends Template { - // 通用的图片参数验证 - async validateInput(input: ImageGenerateInput): Promise { - const errors: string[] = []; - - // 验证提示词 - if (!input.prompt || input.prompt.trim().length === 0) { - errors.push('提示词不能为空'); - } - - if (input.prompt && input.prompt.length > 1000) { - errors.push('提示词长度不能超过1000个字符'); - } - - // 验证图片尺寸 - if (input.width && (input.width < 256 || input.width > 2048)) { - errors.push('图片宽度必须在256-2048之间'); - } - - if (input.height && (input.height < 256 || input.height > 2048)) { - errors.push('图片高度必须在256-2048之间'); - } - - // 验证输入图片 - if (input.inputImage && !this.isValidImageData(input.inputImage)) { - errors.push('输入图片格式不正确'); - } - - // 调用子类的自定义验证 - const customErrors = await this.validateCustomInput(input); - errors.push(...customErrors); - - return errors; - } - - // 子类可以重写此方法添加自定义验证 - protected async validateCustomInput(input: ImageGenerateInput): Promise { - return []; - } - - // 验证图片数据格式 - protected isValidImageData(imageData: string): boolean { - // base64格式 - if (imageData.startsWith('data:image/')) { - return /^data:image\/(jpeg|jpg|png|gif|webp);base64,/.test(imageData); - } - // URL格式 - return /^https?:\/\/.+\.(jpeg|jpg|png|gif|webp)$/i.test(imageData); - } - - // 获取通用的图片生成参数定义 - protected getCommonParameterDefinitions(): ParameterDefinition[] { - return [ - { - key: 'prompt', - displayName: '提示词', - description: '描述想要生成的图片内容', - type: ParameterType.STRING, - validation: { required: true, minLength: 1, maxLength: 1000 }, - order: 1, - }, - { - key: 'quality', - displayName: '生成质量', - description: '选择生成质量等级', - type: ParameterType.SELECT, - defaultValue: 'standard', - validation: { options: ['standard', 'high', 'ultra'] }, - order: 100, - } - ]; - } -} -``` - -### 2.4 视频生成模板抽象类 -```typescript -// src/templates/base/video-generate-template.abstract.ts -import { Template, TemplateExecutionContext } from './template.abstract'; - -export interface VideoGenerateInput { - prompt: string; - inputImage?: string; - duration?: number; // 秒 - fps?: number; - resolution?: '720p' | '1080p' | '4k'; - style?: string; - [key: string]: any; -} - -export interface VideoGenerateOutput { - videoUrl: string; - thumbnailUrl?: string; - duration: number; - fps: number; - resolution: string; - format: string; - fileSize?: number; -} - -export abstract class VideoGenerateTemplate extends Template { - async validateInput(input: VideoGenerateInput): Promise { - const errors: string[] = []; - - // 验证提示词 - if (!input.prompt || input.prompt.trim().length === 0) { - errors.push('提示词不能为空'); - } - - // 验证时长 - if (input.duration && (input.duration < 1 || input.duration > 30)) { - errors.push('视频时长必须在1-30秒之间'); - } - - // 验证帧率 - if (input.fps && (input.fps < 12 || input.fps > 60)) { - errors.push('帧率必须在12-60之间'); - } - - // 验证输入图片 - if (input.inputImage && !this.isValidImageData(input.inputImage)) { - errors.push('输入图片格式不正确'); - } - - // 调用子类的自定义验证 - const customErrors = await this.validateCustomInput(input); - errors.push(...customErrors); - - return errors; - } - - protected async validateCustomInput(input: VideoGenerateInput): Promise { - return []; - } - - protected isValidImageData(imageData: string): boolean { - if (imageData.startsWith('data:image/')) { - return /^data:image\/(jpeg|jpg|png|gif|webp);base64,/.test(imageData); - } - return /^https?:\/\/.+\.(jpeg|jpg|png|gif|webp)$/i.test(imageData); - } - - protected getCommonParameterDefinitions(): ParameterDefinition[] { - return [ - { - key: 'prompt', - displayName: '提示词', - description: '描述想要生成的视频内容', - type: ParameterType.STRING, - validation: { required: true, minLength: 1, maxLength: 500 }, - order: 1, - }, - { - key: 'duration', - displayName: '视频时长', - description: '生成视频的时长(秒)', - type: ParameterType.RANGE, - defaultValue: 4, - validation: { min: 1, max: 30 }, - order: 90, - }, - { - key: 'resolution', - displayName: '分辨率', - description: '选择视频分辨率', - type: ParameterType.SELECT, - defaultValue: '720p', - validation: { options: ['720p', '1080p', '4k'] }, - order: 95, - } - ]; - } -} -``` - -## 3. 模板管理器 - -### 3.1 模板管理器 (TemplateManager) -```typescript -// src/services/template-manager.service.ts -import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; -import { Template, TemplateExecutionContext, TemplateExecutionResult } from '../templates/base/template.abstract'; -import { ParameterDefinition } from '../templates/base/parameter.interface'; - -export interface TemplateInfo { - code: string; - name: string; - description: string; - category: string; - creditCost: number; - version: string; - enabled: boolean; - parameters: ParameterDefinition[]; -} - -@Injectable() -export class TemplateManager implements OnModuleInit { - private readonly logger = new Logger(TemplateManager.name); - private readonly templates = new Map(); - - async onModuleInit() { - // 启动时自动注册所有模板 - await this.registerAllTemplates(); - } - - // 注册模板 - registerTemplate(template: Template): void { - const code = template.getMetadata().code; - - if (this.templates.has(code)) { - this.logger.warn(`模板 ${code} 已存在,将被覆盖`); - } - - this.templates.set(code, template); - this.logger.log(`模板 ${code} 注册成功`); - } - - // 批量注册模板 - registerTemplates(templates: Template[]): void { - templates.forEach(template => this.registerTemplate(template)); - } - - // 卸载模板 - unregisterTemplate(templateCode: string): boolean { - if (this.templates.has(templateCode)) { - this.templates.delete(templateCode); - this.logger.log(`模板 ${templateCode} 已卸载`); - return true; - } - return false; - } - - // 获取模板 - getTemplate(templateCode: string): Template | null { - return this.templates.get(templateCode) || null; - } - - // 获取所有模板信息 - getAllTemplates(): TemplateInfo[] { - return Array.from(this.templates.values()).map(template => ({ - ...template.getMetadata(), - enabled: template.isEnabled(), - parameters: template.getParameterDefinitions(), - })); - } - - // 获取启用的模板 - getEnabledTemplates(): TemplateInfo[] { - return this.getAllTemplates().filter(template => template.enabled); - } - - // 按分类获取模板 - getTemplatesByCategory(category: string): TemplateInfo[] { - return this.getAllTemplates().filter(template => - template.category === category && template.enabled - ); - } - - // 搜索模板 - searchTemplates(keyword: string): TemplateInfo[] { - const lowerKeyword = keyword.toLowerCase(); - return this.getAllTemplates().filter(template => - template.enabled && ( - template.name.toLowerCase().includes(lowerKeyword) || - template.description.toLowerCase().includes(lowerKeyword) || - template.category.toLowerCase().includes(lowerKeyword) - ) - ); - } - - // 启用/禁用模板 - setTemplateEnabled(templateCode: string, enabled: boolean): boolean { - const template = this.getTemplate(templateCode); - if (template) { - template.setEnabled(enabled); - this.logger.log(`模板 ${templateCode} ${enabled ? '已启用' : '已禁用'}`); - return true; - } - return false; - } - - // 执行模板 - async executeTemplate( - templateCode: string, - input: TInput, - context: TemplateExecutionContext - ): Promise> { - const template = this.getTemplate(templateCode); - - if (!template) { - return { - success: false, - error: `模板 ${templateCode} 不存在`, - metadata: { - executionTime: 0, - creditCost: 0, - templateCode, - } - }; - } - - this.logger.log(`开始执行模板 ${templateCode},用户: ${context.userId}`); - - const result = await template.run(input, context); - - this.logger.log( - `模板 ${templateCode} 执行${result.success ? '成功' : '失败'},` + - `耗时: ${result.metadata.executionTime}ms` - ); - - return result; - } - - // 获取模板统计信息 - getStatistics() { - const allTemplates = this.getAllTemplates(); - const enabledTemplates = allTemplates.filter(t => t.enabled); - - const categoryCounts = allTemplates.reduce((acc, template) => { - acc[template.category] = (acc[template.category] || 0) + 1; - return acc; - }, {} as Record); - - return { - total: allTemplates.length, - enabled: enabledTemplates.length, - disabled: allTemplates.length - enabledTemplates.length, - categories: categoryCounts, - }; - } - - // 自动注册所有模板 - 在实际项目中,这里会导入所有模板类 - private async registerAllTemplates(): Promise { - // 这里会自动导入和注册所有模板 - // 在实际项目中,可以通过文件扫描或者手动导入的方式 - this.logger.log('开始注册模板...'); - - // 示例:手动注册模板(实际项目中可以自动化) - // const outfitChangeTemplate = new OutfitChangeTemplate(); - // this.registerTemplate(outfitChangeTemplate); - - this.logger.log(`模板注册完成,共注册 ${this.templates.size} 个模板`); - } -} -``` - -### 3.2 模板工厂 (TemplateFactory) -```typescript -// src/templates/template.factory.ts -import { Injectable } from '@nestjs/common'; -import { Template } from './base/template.abstract'; - -// 导入所有具体模板类 -import { OutfitChangeTemplate } from './image/outfit-change.template'; -import { ImageToVideoTemplate } from './video/image-to-video.template'; -import { PortraitEnhanceTemplate } from './image/portrait-enhance.template'; -import { StyleTransferTemplate } from './image/style-transfer.template'; -import { BackgroundReplaceTemplate } from './image/background-replace.template'; - -@Injectable() -export class TemplateFactory { - // 创建所有模板实例 - createAllTemplates(): Template[] { - return [ - new OutfitChangeTemplate(), - new ImageToVideoTemplate(), - new PortraitEnhanceTemplate(), - new StyleTransferTemplate(), - new BackgroundReplaceTemplate(), - // 在这里添加新的模板类 - ]; - } - - // 根据代码创建特定模板 - createTemplate(templateCode: string): Template | null { - const templates = this.createAllTemplates(); - return templates.find(template => - template.getMetadata().code === templateCode - ) || null; - } - - // 获取所有可用的模板代码 - getAvailableTemplateCodes(): string[] { - return this.createAllTemplates().map(template => - template.getMetadata().code - ); - } -} -``` - -## 4. 具体模板实现示例 - -### 4.1 换装模板实现 -```typescript -// src/templates/image/outfit-change.template.ts -import { Injectable } from '@nestjs/common'; -import { ImageGenerateTemplate, ImageGenerateInput, ImageGenerateOutput } from '../base/image-generate-template.abstract'; -import { TemplateMetadata, TemplateExecutionContext } from '../base/template.abstract'; -import { ParameterDefinition, ParameterType } from '../base/parameter.interface'; -import { BowongAIService } from '../../services/bowongai.service'; - -interface OutfitChangeInput extends ImageGenerateInput { - inputImage: string; // 必填 - clothingDescription: string; - style?: 'casual' | 'formal' | 'vintage' | 'modern' | 'elegant'; -} - -@Injectable() -export class OutfitChangeTemplate extends ImageGenerateTemplate { - readonly metadata: TemplateMetadata = { - code: 'outfit_change_v1', - name: '智能换装', - description: '上传人物图片,描述想要的服装,AI自动生成换装效果', - category: '换装', - creditCost: 15, - version: '1.0.0', - previewImageUrl: '/images/templates/outfit-change-preview.jpg', - }; - - constructor(private readonly bowongAI: BowongAIService) { - super(); - } - - async execute(input: OutfitChangeInput, context: TemplateExecutionContext): Promise { - // 构建换装专用提示词 - const prompt = this.buildOutfitChangePrompt(input); - - // 调用BowongAI基础服务,传入换装模板的特定参数 - const result = await this.bowongAI.imageToImage({ - prompt, - imageUrl: input.inputImage, - model: 'gemini-2.5-flash-image-preview', // 换装模板专用模型 - }); - - // 转换为标准输出格式 - return { - images: result.images.map(img => ({ - url: img.url, - thumbnailUrl: img.thumbnailUrl, - width: img.width, - height: img.height, - format: 'png', - })), - parameters: { - prompt, - model: 'gemini-2.5-flash-image-preview', - workflow: '图生图', - templateType: 'outfit_change', - }, - }; - } - - protected async validateCustomInput(input: OutfitChangeInput): Promise { - const errors: string[] = []; - - // 验证必须有输入图片 - if (!input.inputImage) { - errors.push('必须提供人物图片'); - } - - // 验证服装描述 - if (!input.clothingDescription || input.clothingDescription.trim().length < 3) { - errors.push('服装描述至少需要3个字符'); - } - - if (input.clothingDescription && input.clothingDescription.length > 200) { - errors.push('服装描述不能超过200个字符'); - } - - return errors; - } - - getParameterDefinitions(): ParameterDefinition[] { - return [ - { - key: 'inputImage', - displayName: '人物图片', - description: '请上传清晰的人物图片,建议分辨率1024x1024', - type: ParameterType.IMAGE, - validation: { required: true }, - order: 1, - }, - { - key: 'clothingDescription', - displayName: '服装描述', - description: '详细描述想要的服装样式,如"红色连衣裙"、"黑色西装"等', - type: ParameterType.STRING, - validation: { required: true, minLength: 3, maxLength: 200 }, - order: 2, - }, - { - key: 'style', - displayName: '风格', - description: '选择服装风格', - type: ParameterType.SELECT, - defaultValue: 'casual', - validation: { options: ['casual', 'formal', 'vintage', 'modern', 'elegant'] }, - order: 3, - }, - ...this.getCommonParameterDefinitions(), - ]; - } - - private buildOutfitChangePrompt(input: OutfitChangeInput): string { - const styleMap = { - casual: 'casual everyday wear', - formal: 'formal business attire', - vintage: 'vintage retro style', - modern: 'modern contemporary fashion', - elegant: 'elegant sophisticated style', - }; - - const styleText = input.style ? styleMap[input.style] : 'casual everyday wear'; - - return `Please convert this photo into a highly detailed character model wearing ${input.clothingDescription}. -Style: ${styleText}. -Place in front of the model a delicate, colorful box featuring the printed image of the person from the photo. -Behind the box, show a computer screen actively displaying the modeling process. -In front of the box, position the completed character model based on the provided photo, with the ${styleText} clothing clearly and realistically rendered. -Set the entire scene in a bright, stylish indoor environment resembling a toy collector's or hobbyist's room—full of refined details, vibrant décor, and playful atmosphere. -Ensure the lighting is crisp and luminous, highlighting both the model and its packaging.`; - } -} -``` - -### 4.2 抠图模板实现 -```typescript -// src/templates/video/image-to-video.template.ts -import { Injectable } from '@nestjs/common'; -import { VideoGenerateTemplate, VideoGenerateInput, VideoGenerateOutput } from '../base/video-generate-template.abstract'; -import { TemplateMetadata, TemplateExecutionContext } from '../base/template.abstract'; -import { ParameterDefinition, ParameterType } from '../base/parameter.interface'; -import { BowongAIService } from '../../services/bowongai.service'; - -interface ImageToVideoInput extends VideoGenerateInput { - inputImage: string; // 必填 - videoPrompt?: string; // 视频生成提示词 -} - -@Injectable() -export class ImageToVideoTemplate extends VideoGenerateTemplate { - readonly metadata: TemplateMetadata = { - code: 'image_to_video_v1', - name: '图生视频', - description: '上传图片,生成动态视频效果', - category: '视频生成', - creditCost: 50, - version: '1.0.0', - previewImageUrl: '/images/templates/image-to-video-preview.jpg', - }; - - constructor(private readonly bowongAI: BowongAIService) { - super(); - } - - async execute(input: ImageToVideoInput, context: TemplateExecutionContext): Promise { - // 构建图生视频专用提示词 - const imagePrompt = this.buildImagePrompt(input); - const videoPrompt = this.buildVideoPrompt(input); - - const result = await this.bowongAI.imageToVideo({ - imagePrompt, - videoPrompt, - imageUrl: input.inputImage, - duration: input.duration || 6, - aspectRatio: input.resolution || '9:16', - imageModel: 'gemini-2.5-flash-image-preview', - videoModel: '302/MiniMax-Hailuo-02', - }); - - return { - videoUrl: result.videoUrl, - thumbnailUrl: result.thumbnailUrl, - duration: result.duration, - fps: result.fps, - resolution: result.resolution, - format: 'mp4', - }; - } - - protected async validateCustomInput(input: ImageToVideoInput): Promise { - const errors: string[] = []; - - if (!input.inputImage) { - errors.push('必须提供原始图片'); - } - - if (input.videoPrompt && input.videoPrompt.length > 200) { - errors.push('视频提示词不能超过200个字符'); - } - - return errors; - } - - getParameterDefinitions(): ParameterDefinition[] { - return [ - { - key: 'inputImage', - displayName: '原始图片', - description: '请上传要生成视频的图片', - type: ParameterType.IMAGE, - validation: { required: true }, - order: 1, - }, - { - key: 'videoPrompt', - displayName: '视频提示词', - description: '描述想要的视频效果(可选)', - type: ParameterType.STRING, - validation: { maxLength: 200 }, - order: 2, - }, - { - key: 'resolution', - displayName: '视频比例', - description: '选择视频比例', - type: ParameterType.SELECT, - defaultValue: '9:16', - validation: { options: ['9:16', '16:9'] }, - order: 3, - }, - ...this.getCommonParameterDefinitions(), - ]; - } - - private buildImagePrompt(input: ImageToVideoInput): string { - return `Please convert this photo into a highly detailed character model. -Place in front of the model a delicate, colorful box featuring the printed image of the person from the photo. -Behind the box, show a computer screen actively displaying the Blender modeling process. -In front of the box, position the completed character model based on the provided photo, with the PVC texture clearly and realistically rendered. -Set the entire scene in a bright, stylish indoor environment resembling a toy collector's or hobbyist's room—full of refined details, vibrant décor, and playful atmosphere. -Ensure the lighting is crisp and luminous, highlighting both the model and its packaging.`; - } - - private buildVideoPrompt(input: ImageToVideoInput): string { - const baseVideoPrompt = 'Create gentle natural movements and subtle animations'; - - if (input.videoPrompt) { - return `${baseVideoPrompt}. ${input.videoPrompt}. Maintain character consistency throughout the animation.`; - } - - return `${baseVideoPrompt}. Natural lifelike movement with smooth transitions.`; - } -} -``` - -### 4.3 人像增强模板实现(示例) -```typescript -// src/templates/image/portrait-enhance.template.ts -import { Injectable } from '@nestjs/common'; -import { ImageGenerateTemplate, ImageGenerateInput, ImageGenerateOutput } from '../base/image-generate-template.abstract'; -import { TemplateMetadata, TemplateExecutionContext } from '../base/template.abstract'; -import { ParameterDefinition, ParameterType } from '../base/parameter.interface'; -import { BowongAIService } from '../../services/bowongai.service'; - -interface PortraitEnhanceInput extends ImageGenerateInput { - inputImage: string; // 必填 - enhanceLevel: 'subtle' | 'moderate' | 'dramatic'; - lightingStyle?: 'natural' | 'studio' | 'soft' | 'dramatic'; -} - -@Injectable() -export class PortraitEnhanceTemplate extends ImageGenerateTemplate { - readonly metadata: TemplateMetadata = { - code: 'portrait_enhance_v1', - name: '人像增强', - description: '专业级人像照片增强,提升画质和美观度', - category: '人像处理', - creditCost: 12, - version: '1.0.0', - previewImageUrl: '/images/templates/portrait-enhance-preview.jpg', - }; - - constructor(private readonly bowongAI: BowongAIService) { - super(); - } - - async execute(input: PortraitEnhanceInput, context: TemplateExecutionContext): Promise { - // 构建人像增强专用提示词 - const prompt = this.buildPortraitPrompt(input); - - // 调用BowongAI基础服务,传入人像增强模板的特定参数 - const result = await this.bowongAI.imageToImage({ - prompt, - imageUrl: input.inputImage, - model: 'gemini-2.5-flash-image-preview', // 人像增强专用模型 - }); - - return { - images: result.images.map(img => ({ - url: img.url, - thumbnailUrl: img.thumbnailUrl, - width: img.width, - height: img.height, - format: 'png', - })), - parameters: { - prompt, - model: 'gemini-2.5-flash-image-preview', - workflow: '图生图', - templateType: 'portrait_enhance', - enhanceLevel: input.enhanceLevel, - }, - }; - } - - protected async validateCustomInput(input: PortraitEnhanceInput): Promise { - const errors: string[] = []; - - if (!input.inputImage) { - errors.push('必须提供人像图片'); - } - - const validEnhanceLevels = ['subtle', 'moderate', 'dramatic']; - if (!validEnhanceLevels.includes(input.enhanceLevel)) { - errors.push('增强级别必须是: subtle, moderate, dramatic 之一'); - } - - return errors; - } - - getParameterDefinitions(): ParameterDefinition[] { - return [ - { - key: 'inputImage', - displayName: '人像图片', - description: '请上传需要增强的人像照片', - type: ParameterType.IMAGE, - validation: { required: true }, - order: 1, - }, - { - key: 'enhanceLevel', - displayName: '增强级别', - description: '选择增强程度', - type: ParameterType.SELECT, - defaultValue: 'moderate', - validation: { options: ['subtle', 'moderate', 'dramatic'] }, - order: 2, - }, - { - key: 'lightingStyle', - displayName: '光照风格', - description: '选择光照效果(可选)', - type: ParameterType.SELECT, - defaultValue: 'natural', - validation: { options: ['natural', 'studio', 'soft', 'dramatic'] }, - order: 3, - }, - ...this.getCommonParameterDefinitions(), - ]; - } - - private buildPortraitPrompt(input: PortraitEnhanceInput): string { - const enhancementLevels = { - subtle: 'Apply subtle professional enhancements', - moderate: 'Apply moderate quality improvements', - dramatic: 'Apply dramatic professional transformation', - }; - - const lightingStyles = { - natural: 'natural lighting', - studio: 'professional studio lighting', - soft: 'soft diffused lighting', - dramatic: 'dramatic cinematic lighting', - }; - - const enhancement = enhancementLevels[input.enhanceLevel] || 'moderate'; - const lighting = lightingStyles[input.lightingStyle] || 'natural lighting'; - - return `${enhancement} to this portrait photo. -Improve skin texture, clarity, and overall quality while maintaining natural appearance. -Focus on professional photography quality with ${lighting}. -Preserve the person's unique features and expressions. -Enhance details without over-processing or creating artificial effects.`; - } -} -``` - -## 5. 模板服务集成 - -### 5.1 模板服务 (TemplateService) -```typescript -// src/services/template.service.ts -import { Injectable } from '@nestjs/common'; -import { TemplateManager } from './template-manager.service'; -import { TemplateFactory } from '../templates/template.factory'; -import { CreditService } from './credit.service'; -import { TemplateExecutionContext } from '../templates/base/template.abstract'; - -@Injectable() -export class TemplateService { - constructor( - private readonly templateManager: TemplateManager, - private readonly templateFactory: TemplateFactory, - private readonly creditService: CreditService, - ) { - // 初始化时注册所有模板 - this.initializeTemplates(); - } - - // 执行模板(带积分检查) - async executeTemplate( - templateCode: string, - input: any, - userId: string, - platform: string - ) { - // 1. 获取模板信息 - const template = this.templateManager.getTemplate(templateCode); - if (!template) { - throw new Error(`模板 ${templateCode} 不存在`); - } - - const metadata = template.getMetadata(); - - // 2. 检查用户积分 - const hasEnoughCredits = await this.creditService.checkBalance( - userId, - platform as any, - metadata.creditCost - ); - - if (!hasEnoughCredits) { - throw new Error('积分不足'); - } - - // 3. 执行模板 - const context: TemplateExecutionContext = { - userId, - platform, - requestId: `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, - timestamp: new Date(), - }; - - const result = await this.templateManager.executeTemplate(templateCode, input, context); - - // 4. 扣除积分(仅在成功时) - if (result.success) { - await this.creditService.consumeCredits( - userId, - platform as any, - metadata.creditCost, - 'ai_generation' as any, - context.requestId - ); - } - - return result; - } - - // 获取所有可用模板 - getAvailableTemplates() { - return this.templateManager.getEnabledTemplates(); - } - - // 按分类获取模板 - getTemplatesByCategory(category: string) { - return this.templateManager.getTemplatesByCategory(category); - } - - // 搜索模板 - searchTemplates(keyword: string) { - return this.templateManager.searchTemplates(keyword); - } - - // 获取模板详情 - getTemplateInfo(templateCode: string) { - const template = this.templateManager.getTemplate(templateCode); - if (!template) { - return null; - } - - return { - ...template.getMetadata(), - enabled: template.isEnabled(), - parameters: template.getParameterDefinitions(), - }; - } - - // 获取统计信息 - getStatistics() { - return this.templateManager.getStatistics(); - } - - // 初始化模板 - private initializeTemplates() { - const templates = this.templateFactory.createAllTemplates(); - this.templateManager.registerTemplates(templates); - } -} -``` - -### 5.2 模板控制器 -```typescript -// src/controllers/template.controller.ts -import { Controller, Get, Post, Body, Param, Query, UseGuards } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; -import { TemplateService } from '../services/template.service'; -import { JwtAuthGuard } from '../guards/jwt-auth.guard'; -import { CurrentUser } from '../decorators/current-user.decorator'; - -@ApiTags('🎨 AI模板系统') -@Controller('templates') -export class TemplateController { - constructor(private readonly templateService: TemplateService) {} - - @Get() - @ApiOperation({ summary: '获取所有可用模板' }) - async getAvailableTemplates(@Query('category') category?: string) { - if (category) { - return this.templateService.getTemplatesByCategory(category); - } - return this.templateService.getAvailableTemplates(); - } - - @Get('search') - @ApiOperation({ summary: '搜索模板' }) - async searchTemplates(@Query('keyword') keyword: string) { - if (!keyword) { - throw new Error('搜索关键词不能为空'); - } - return this.templateService.searchTemplates(keyword); - } - - @Get('statistics') - @ApiOperation({ summary: '获取模板统计信息' }) - async getStatistics() { - return this.templateService.getStatistics(); - } - - @Get(':templateCode') - @ApiOperation({ summary: '获取模板详情' }) - async getTemplateInfo(@Param('templateCode') templateCode: string) { - const templateInfo = this.templateService.getTemplateInfo(templateCode); - if (!templateInfo) { - throw new Error('模板不存在'); - } - return templateInfo; - } - - @Post(':templateCode/execute') - @UseGuards(JwtAuthGuard) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ summary: '执行模板' }) - async executeTemplate( - @Param('templateCode') templateCode: string, - @Body() input: any, - @CurrentUser() user: any - ) { - return this.templateService.executeTemplate( - templateCode, - input, - user.id, - user.platform - ); - } -} -``` - -## 5. AI服务集成 - -### 5.1 AI服务接口 -```typescript -// src/services/ai-service.interface.ts -export interface ImageToImageRequest { - prompt: string; - initImage: string; - strength?: number; - cfgScale?: number; - steps?: number; - width?: number; - height?: number; -} - -export interface ImageToImageResponse { - images: Array<{ - url: string; - thumbnailUrl?: string; - width: number; - height: number; - }>; - seed?: number; -} - -export interface ImageToVideoRequest { - prompt: string; - image: string; - duration?: number; - fps?: number; - resolution?: string; -} - -export interface ImageToVideoResponse { - videoUrl: string; - thumbnailUrl?: string; - duration: number; - fps: number; - resolution: string; - fileSize?: number; -} -``` - -### 5.2 BowongAI 基础服务实现 -```typescript -// src/services/bowongai.service.ts -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import axios from 'axios'; - -export interface BowongAIImageOptions { - prompt: string; - imageUrl: string; - model?: string; -} - -export interface BowongAIVideoOptions { - imagePrompt: string; - videoPrompt: string; - imageUrl: string; - duration?: number; - aspectRatio?: string; - imageModel?: string; - videoModel?: string; -} - -export interface BowongAIImageRequest { - workflow: '图生图' | '图生图+生视频'; - environment: string; - image_generation: { - model: string; - prompt: string; - image_url: string; - }; - video_generation?: { - model: string; - prompt: string; - duration: string; - aspect_ratio: string; - }; -} - -export interface BowongAIResponse { - success: boolean; - data?: { - image_url?: string; - video_url?: string; - task_id?: string; - }; - error?: string; -} - -@Injectable() -export class BowongAIService { - private readonly webhookUrl: string; - private readonly environment: string; - - constructor(private readonly configService: ConfigService) { - this.webhookUrl = this.configService.get('BOWONGAI_WEBHOOK_URL', - 'https://n8n.bowongai.com/webhook/76f92dbf-785f-4add-96b5-a108174b7c14'); - this.environment = this.configService.get('BOWONGAI_ENVIRONMENT', - 'https://bowongai-test--text-video-agent-fastapi-app.modal.run'); - } - - // 通用图生图接口 - 各模板传入自己的参数 - async imageToImage(options: BowongAIImageOptions) { - const payload: BowongAIImageRequest = { - workflow: '图生图', - environment: this.environment, - image_generation: { - model: options.model || 'gemini-2.5-flash-image-preview', - prompt: options.prompt, - image_url: options.imageUrl, - }, - }; - - const response = await axios.post(this.webhookUrl, payload, { - headers: { - 'Content-Type': 'application/json', - }, - timeout: 120000, - }); - - if (!response.data.success) { - throw new Error(response.data.error || '图生图处理失败'); - } - - return { - images: [{ - url: response.data.data.image_url, - width: 1024, - height: 1024, - }], - }; - } - - // 通用图生视频接口 - 各模板传入自己的参数 - async imageToVideo(options: BowongAIVideoOptions) { - const payload: BowongAIImageRequest = { - workflow: '图生图+生视频', - environment: this.environment, - image_generation: { - model: options.imageModel || 'gemini-2.5-flash-image-preview', - prompt: options.imagePrompt, - image_url: options.imageUrl, - }, - video_generation: { - model: options.videoModel || '302/MiniMax-Hailuo-02', - prompt: options.videoPrompt, - duration: options.duration?.toString() || '6', - aspect_ratio: options.aspectRatio || '9:16', - }, - }; - - const response = await axios.post(this.webhookUrl, payload, { - headers: { - 'Content-Type': 'application/json', - }, - timeout: 300000, // 5分钟超时 - }); - - if (!response.data.success) { - throw new Error(response.data.error || '图生视频处理失败'); - } - - return { - videoUrl: response.data.data.video_url, - duration: options.duration || 6, - fps: 24, - resolution: options.aspectRatio || '9:16', - format: 'mp4', - }; - } -} -``` - -### 5.3 AI服务接口更新 -```typescript -// src/services/ai-service.interface.ts - 更新接口定义 -export interface ImageToImageRequest { - prompt: string; - initImage: string; // 图片URL或base64 - strength?: number; - cfgScale?: number; - steps?: number; - width?: number; - height?: number; -} - -export interface ImageToImageResponse { - images: Array<{ - url: string; - thumbnailUrl?: string; - width: number; - height: number; - }>; - seed?: number; -} - -export interface ImageToVideoRequest { - prompt: string; - image: string; // 图片URL或base64 - duration?: number; // 秒 - fps?: number; - resolution?: string; // '720p', '1080p', '9:16', '16:9' -} - -export interface ImageToVideoResponse { - videoUrl: string; - thumbnailUrl?: string; - duration: number; - fps: number; - resolution: string; - format: string; - fileSize?: number; -} -``` - -## 6. 模块配置 - -### 6.1 模板模块配置 -```typescript -// src/modules/template.module.ts -import { Module } from '@nestjs/common'; -import { TemplateController } from '../controllers/template.controller'; -import { TemplateService } from '../services/template.service'; -import { TemplateManager } from '../services/template-manager.service'; -import { TemplateFactory } from '../templates/template.factory'; -import { BowongAIService } from '../services/bowongai.service'; -import { CreditService } from '../services/credit.service'; - -// 导入所有模板类 -import { OutfitChangeTemplate } from '../templates/image/outfit-change.template'; -import { ImageToVideoTemplate } from '../templates/video/image-to-video.template'; -import { PortraitEnhanceTemplate } from '../templates/image/portrait-enhance.template'; -import { StyleTransferTemplate } from '../templates/image/style-transfer.template'; -import { BackgroundReplaceTemplate } from '../templates/image/background-replace.template'; - -@Module({ - controllers: [TemplateController], - providers: [ - TemplateService, - TemplateManager, - TemplateFactory, - BowongAIService, - CreditService, - // 注册所有模板类 - OutfitChangeTemplate, - ImageToVideoTemplate, - PortraitEnhanceTemplate, - StyleTransferTemplate, - BackgroundReplaceTemplate, - ], - exports: [TemplateService, TemplateManager], -}) -export class TemplateModule {} -``` - -### 6.2 环境配置 -```typescript -// .env 配置 -# BowongAI 配置 -BOWONGAI_WEBHOOK_URL=https://n8n.bowongai.com/webhook/76f92dbf-785f-4add-96b5-a108174b7c14 -BOWONGAI_ENVIRONMENT=https://bowongai-test--text-video-agent-fastapi-app.modal.run - -# 其他AI服务配置(备用) -OPENAI_API_KEY=your-openai-api-key -STABILITY_API_KEY=your-stability-api-key -RUNWAYML_API_KEY=your-runwayml-api-key -``` - -## 7. 完整的使用示例 - -### 7.1 项目集成步骤 -```typescript -// 1. 安装依赖 -npm install reflect-metadata - -// 2. 在 app.module.ts 中导入模板模块 -import { Module } from '@nestjs/common'; -import { TemplateModule } from './modules/template.module'; - -@Module({ - imports: [ - // ... 其他模块 - TemplateModule, - ], -}) -export class AppModule {} - -// 3. 创建新的模板类 -// src/templates/image/my-custom.template.ts -@Injectable() -export class MyCustomTemplate extends ImageGenerateTemplate { - readonly metadata: TemplateMetadata = { - code: 'my_custom_v1', - name: '我的自定义模板', - description: '自定义的AI生成模板', - category: '自定义', - creditCost: 10, - version: '1.0.0', - }; - - async execute(input: any, context: TemplateExecutionContext) { - // 实现具体逻辑 - return { - images: [/* 生成的图片 */], - }; - } - - getParameterDefinitions(): ParameterDefinition[] { - return [ - { - key: 'prompt', - displayName: '提示词', - description: '描述想要生成的内容', - type: ParameterType.STRING, - validation: { required: true }, - order: 1, - }, - // ... 其他参数 - ]; - } -} - -// 4. 在工厂类中注册 -// src/templates/template.factory.ts -createAllTemplates(): Template[] { - return [ - // ... 现有模板 - new MyCustomTemplate(), - ]; -} -``` - -## 6. 使用示例 - -### 6.1 前端调用示例 - -#### 图生图模板调用 -```typescript -// 前端调用换装模板(图生图) -const response = await fetch('/api/templates/outfit_change_v1/execute', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - inputImage: 'https://cdn.roasmax.cn/upload/e9066bf20fb546b09ece65d6fbcf2dd3.png', - clothingDescription: '红色连衣裙', - style: 'elegant' - }) -}); - -const result = await response.json(); -if (result.success) { - console.log('图生图成功:', result.data); - // result.data.images[0].url 是生成的图片URL -} else { - console.error('生成失败:', result.error); -} -``` - -#### 图生视频模板调用 -```typescript -// 前端调用图生视频模板 -const response = await fetch('/api/templates/image_to_video_v1/execute', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - inputImage: 'https://cdn.roasmax.cn/upload/e9066bf20fb546b09ece65d6fbcf2dd3.png', - videoPrompt: '让角色动起来', - duration: 6, - resolution: '9:16' - }) -}); - -const result = await response.json(); -if (result.success) { - console.log('图生视频成功:', result.data); - // result.data.videoUrl 是生成的视频URL -} else { - console.error('生成失败:', result.error); -} -``` - -#### 人像增强模板调用 -```typescript -// 前端调用人像增强模板 -const response = await fetch('/api/templates/portrait_enhance_v1/execute', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - inputImage: 'https://cdn.roasmax.cn/upload/e9066bf20fb546b09ece65d6fbcf2dd3.png', - enhanceLevel: 'moderate', - lightingStyle: 'natural' - }) -}); - -const result = await response.json(); -if (result.success) { - console.log('人像增强成功:', result.data); - // result.data.images[0].url 是增强后的图片URL -} else { - console.error('增强失败:', result.error); -} -``` - -### 6.2 添加新模板 -```typescript -// 1. 创建新的模板类 -// src/templates/image/portrait-enhance.template.ts -@Injectable() -export class PortraitEnhanceTemplate extends ImageGenerateTemplate { - readonly metadata: TemplateMetadata = { - code: 'portrait_enhance_v1', - name: '人像增强', - description: '提升人像照片的清晰度和美观度', - category: '人像处理', - creditCost: 8, - version: '1.0.0', - }; - - async execute(input: any, context: TemplateExecutionContext) { - // 实现具体的人像增强逻辑 - // ... - } - - getParameterDefinitions(): ParameterDefinition[] { - // 定义参数 - // ... - } -} - -// 2. 在工厂类中注册 -// src/templates/template.factory.ts -export class TemplateFactory { - createAllTemplates(): Template[] { - return [ - new OutfitChangeTemplate(), - new ObjectRemovalTemplate(), - new PortraitEnhanceTemplate(), // 添加新模板 - // ... - ]; - } -} -``` - -### 6.3 模板管理命令 -```typescript -// 获取所有模板 -const templates = templateManager.getAllTemplates(); - -// 启用/禁用模板 -templateManager.setTemplateEnabled('outfit_change_v1', false); - -// 获取统计信息 -const stats = templateManager.getStatistics(); -console.log(`共有 ${stats.total} 个模板,其中 ${stats.enabled} 个已启用`); -``` - -## 7. 系统优势 - -### 7.1 面向对象设计优势 -1. **类型安全**: TypeScript编译时检查,避免运行时错误 -2. **代码复用**: 通过继承实现通用逻辑复用 -3. **易于扩展**: 添加新模板只需继承基类并实现抽象方法 -4. **IDE支持**: 完整的代码提示和重构支持 -5. **单元测试**: 每个模板类可独立测试 - -### 7.2 与数据库方案对比 -| 特性 | 面向对象方案 | 数据库方案 | -|------|-------------|-----------| -| 类型安全 | ✅ 编译时检查 | ❌ 运行时检查 | -| 代码复用 | ✅ 继承机制 | ❌ 需要额外逻辑 | -| 扩展性 | ✅ 新增类即可 | ❌ 需要数据库迁移 | -| 性能 | ✅ 内存中执行 | ❌ 数据库查询开销 | -| 维护性 | ✅ 代码即文档 | ❌ 需要维护数据结构 | -| 版本控制 | ✅ Git管理 | ❌ 数据库版本管理复杂 | - -### 7.3 核心特性 -1. **统一接口**: 所有模板都实现相同的执行接口 -2. **参数验证**: 基类提供通用验证,子类可扩展 -3. **错误处理**: 统一的错误处理和结果格式 -4. **元数据管理**: 每个模板包含完整的元数据信息 -5. **动态管理**: 运行时启用/禁用模板 -6. **积分集成**: 自动处理积分检查和扣除 - -这个基于面向对象的模板管理系统更加灵活、类型安全,且易于维护和扩展。通过抽象类和继承机制,可以轻松添加新的AI生成模板,同时保持代码的一致性和可维护性。 diff --git a/docs/template-system-summary.md b/docs/template-system-summary.md index 8acb96e..42ddaea 100644 --- a/docs/template-system-summary.md +++ b/docs/template-system-summary.md @@ -1,195 +1,241 @@ -# AI模板管理系统总结 - 面向对象设计 +# AI模板管理系统架构总结 - 混合式设计 -## 🎯 设计理念 +## 🎯 核心设计理念 -您的建议非常正确!使用面向对象的设计模式比数据库方案更加优雅和实用: +基于您的优雅抽象类设计,实现**代码定义逻辑 + 数据库存储配置**的混合式架构: ```typescript -export abstract class Template {} -export abstract class ImageGenerateTemplate extends Template {} -export abstract class VideoGenerateTemplate extends Template {} -export class TemplateManager {} +// 抽象层次 (代码中定义) +Template (公共属性抽象类) +├── ImageGenerateTemplate (图片生成抽象) +├── VideoGenerateTemplate (视频生成抽象) +├── N8nImageGenerateTemplate (N8n图片实现) +└── N8nVideoGenerateTemplate (N8n视频实现) + +// 具体配置 (数据库中存储) +CharacterFigurineTemplate → n8n_templates表中的配置记录 +PhotoRestoreTemplate → n8n_templates表中的配置记录 + +// 动态实例 (Entity层) +new N8nVideoGenerateTemplateEntity(id).onInit().then(e=>e.execute(imgUrl)) ``` -## 🏗️ 核心架构 +## 🏗️ 混合式架构设计 -### 1. 抽象类层次结构 +### 1. 现有代码抽象层次 (保持不变) ``` -Template (基础抽象类) -├── ImageGenerateTemplate (图片生成抽象类) -│ ├── OutfitChangeTemplate (换装模板) -│ ├── StyleTransferTemplate (风格转换模板) -│ └── BackgroundReplaceTemplate (背景替换模板) -└── VideoGenerateTemplate (视频生成抽象类) - ├── ObjectRemovalTemplate (抠图模板) - └── MotionGenerateTemplate (动作生成模板) +Template (基础抽象类) [src/templates/types.ts:4-13] +├── ImageGenerateTemplate (图片生成抽象) [src/templates/types.ts:18-20] +│ └── N8nImageGenerateTemplate (N8n图片实现) [src/templates/n8nTemplate.ts:4-32] +│ ├── PhotoRestoreTemplate (具体配置存DB) +│ ├── PetFigurineTemplate (具体配置存DB) +│ └── CosplayRealPersonTemplate (具体配置存DB) +└── VideoGenerateTemplate (视频生成抽象) [src/templates/types.ts:23-25] + └── N8nVideoGenerateTemplate (N8n视频实现) [src/templates/n8nTemplate.ts:35-74] + ├── CharacterFigurineTemplate (具体配置存DB) + ├── GarageOpeningTemplate (具体配置存DB) + └── NuclearExplosionTemplate (具体配置存DB) ``` -### 2. 核心组件 -- **Template**: 基础抽象类,定义统一接口 -- **TemplateManager**: 模板管理器,负责注册和执行 -- **TemplateFactory**: 模板工厂,创建所有模板实例 -- **TemplateService**: 业务服务层,集成积分系统 +### 2. 架构组件分层 +#### 代码层 (现有优势保持) +- **Template抽象类**: 公共属性定义 [types.ts:4-13] +- **N8nTemplate系列**: 执行逻辑实现 [n8nTemplate.ts] +- **TemplateManager**: 模板注册和管理 [types.ts:28-71] +- **TemplateService**: 业务服务集成 [template.service.ts] -## ✅ 面向对象方案的优势 +#### 数据库层 (新增能力) +- **n8n_templates表**: 存储模板配置信息 +- **N8nTemplateEntity**: 配置数据实体 +- **TemplateUsageLog**: 使用记录统计 -### 🔒 类型安全 +#### Entity层 (动态桥接) +- **N8nImageGenerateTemplateEntity**: 动态图片模板实例 +- **N8nVideoGenerateTemplateEntity**: 动态视频模板实例 +- **N8nTemplateFactoryService**: 模板工厂服务 + +## ✅ 混合式架构的优势 + +### 🔒 保持类型安全 (现有优势) ```typescript -// 编译时就能发现错误 -const template: ImageGenerateTemplate = new OutfitChangeTemplate(); -const result = await template.execute(input, context); // 类型检查 -``` - -### 🧬 代码复用 -```typescript -// 基类提供通用功能 -export abstract class ImageGenerateTemplate extends Template { - // 通用的图片参数验证 - async validateInput(input: ImageGenerateInput): Promise { - // 共享的验证逻辑 - } +// 现有代码已实现类型安全 +const template = templateManager.getTemplate('photo_restore_v1'); +if (template instanceof N8nImageGenerateTemplate) { + const result = await template.execute(imageUrl); // 类型检查 } + +// Entity层同样保持类型安全 +const template = await new N8nVideoGenerateTemplateEntity(id, repo).onInit(); +const result = await template.execute(imageUrl); // 强类型约束 ``` -### 🚀 易于扩展 +### 🧬 代码复用增强 (现有+新增) ```typescript -// 添加新模板只需继承和实现 -@Injectable() -export class NewTemplate extends ImageGenerateTemplate { - readonly metadata = { /* 模板信息 */ }; - - async execute(input, context) { - // 具体实现 - } - - getParameterDefinitions() { - // 参数定义 - } -} -``` - -### 🛠️ IDE支持 -- 完整的代码提示和自动补全 -- 重构时自动更新所有引用 -- 编译时错误检查 -- 调试时的完整类型信息 - -## 📊 与数据库方案对比 - -| 特性 | 面向对象方案 | 数据库方案 | -|------|-------------|-----------| -| **类型安全** | ✅ 编译时检查 | ❌ 运行时检查 | -| **代码复用** | ✅ 继承机制 | ❌ 需要额外逻辑 | -| **扩展性** | ✅ 新增类即可 | ❌ 需要数据库迁移 | -| **性能** | ✅ 内存中执行 | ❌ 数据库查询开销 | -| **维护性** | ✅ 代码即文档 | ❌ 需要维护数据结构 | -| **版本控制** | ✅ Git管理 | ❌ 数据库版本管理复杂 | -| **测试** | ✅ 单元测试友好 | ❌ 需要数据库环境 | -| **部署** | ✅ 无状态部署 | ❌ 需要数据库同步 | - -## 🎨 具体实现示例 - -### 换装模板 -```typescript -@Injectable() -export class OutfitChangeTemplate extends ImageGenerateTemplate { - readonly metadata: TemplateMetadata = { - code: 'outfit_change_v1', - name: '智能换装', - description: '上传人物图片,描述想要的服装,AI自动生成换装效果', - category: '换装', - creditCost: 15, - version: '1.0.0', - }; - - async execute(input: OutfitChangeInput, context: TemplateExecutionContext) { - const prompt = this.buildPrompt(input); - const result = await this.stabilityAI.imageToImage({ - prompt, - initImage: input.inputImage, - strength: 0.8, +// 现有的N8n抽象类提供通用执行逻辑 +export abstract class N8nImageGenerateTemplate extends ImageGenerateTemplate { + execute(imageUrl: string): Promise { + return axios.request({ + url: 'https://n8n.bowongai.com/webhook/...', + data: { + model: this.imageModel, // 子类提供 + prompt: this.imagePrompt, // 子类提供 + image_url: imageUrl + } }); - return this.formatOutput(result); } +} - getParameterDefinitions(): ParameterDefinition[] { - return [ - { - key: 'inputImage', - displayName: '人物图片', - type: ParameterType.IMAGE, - validation: { required: true }, - order: 1, - }, - { - key: 'clothingDescription', - displayName: '服装描述', - type: ParameterType.STRING, - validation: { required: true, minLength: 3, maxLength: 200 }, - order: 2, - }, - ]; - } +// Entity层动态提供配置,复用执行逻辑 +export class N8nImageGenerateTemplateEntity extends N8nImageGenerateTemplate { + get imageModel(): string { return this.config?.imageModel || ''; } + get imagePrompt(): string { return this.config?.imagePrompt || ''; } + // execute方法自动继承,无需重写 } ``` -## 🔧 使用方式 - -### 前端调用 +### 🚀 扩展性大幅增强 (质的飞跃) ```typescript -// 统一的调用接口,不管底层是什么AI模型 -const result = await fetch('/api/templates/outfit_change_v1/execute', { +// 代码扩展:添加新抽象类型 +export class NewAIProviderTemplate extends ImageGenerateTemplate { + // 新AI服务商的实现逻辑 +} + +// 配置扩展:数据库中添加新模板记录 +INSERT INTO n8n_templates (code, name, description, ...) VALUES +('new_style_v1', '新风格模板', '描述...', ...); + +// 无需重启服务,立即生效 +const newTemplate = await factory.createTemplateByCode('new_style_v1'); +``` + +### 🎛️ 运营管理能力 (全新增值) +- **在线配置**: 管理后台可调整模板参数、提示词等 +- **A/B测试**: 同功能不同配置版本对比测试 +- **灰度发布**: 新模板先小范围开放测试 +- **实时监控**: 每个模板的使用率、成功率、耗时等 +- **个性化**: 根据用户行为推荐合适的模板 + + +## 🔧 使用方式和扩展 + +### 您期望的使用方式 +```typescript +// 通过ID创建并执行 (您的预期语法) +const result = await new N8nVideoGenerateTemplateEntity(templateId, repo) + .onInit() + .then(e => e.execute(imgUrl)); + +// 通过工厂服务简化使用 +const template = await templateFactory.createTemplateByCode('character_figurine_v1'); +const result = await template.execute(imageUrl); + +// 支持链式调用 +const result = await templateFactory + .createVideoTemplate(1) + .then(t => t.execute(imageUrl)); +``` + +### 前端API调用 (保持简洁) +```typescript +// 基于数据库ID的调用 +const result = await fetch('/api/templates/1/execute', { method: 'POST', - body: JSON.stringify({ - inputImage: 'data:image/jpeg;base64,...', - clothingDescription: '红色连衣裙', - style: 'elegant' - }) + headers: { 'Authorization': `Bearer ${token}` }, + body: JSON.stringify({ imageUrl: 'https://...' }) +}); + +// 基于模板代码的调用 (兼容现有) +const result = await fetch('/api/templates/character_figurine_v1/execute', { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` }, + body: JSON.stringify({ imageUrl: 'https://...' }) }); ``` -### 添加新模板 +### 添加新模板 (多种方式) ```typescript -// 1. 创建模板类 -export class MyTemplate extends ImageGenerateTemplate { - // 实现抽象方法 +// 方式1: 数据库中直接添加配置 (推荐) +INSERT INTO n8n_templates ( + code, name, description, template_type, + image_model, image_prompt, ... +) VALUES ( + 'new_template_v1', '新模板', '描述', 'image', + 'gemini-2.5-flash-image-preview', 'AI提示词', ... +); + +// 方式2: 代码中添加新抽象类型 (框架扩展) +export class CustomAITemplate extends ImageGenerateTemplate { + // 新AI服务商的实现 } -// 2. 在工厂中注册 -export class TemplateFactory { - createAllTemplates(): Template[] { - return [ - new OutfitChangeTemplate(), - new MyTemplate(), // 添加新模板 - ]; - } -} +// 方式3: 迁移现有代码模板到数据库 +await migrationService.migrateExistingTemplates(); ``` -## 🎯 核心价值 +## 🎯 混合式架构的核心价值 -### 1. **统一抽象** -不同的AI模型(Stability AI、RunwayML、OpenAI等)通过统一的模板接口调用,前端无需关心底层实现差异。 +### 1. **保持现有优势** (代码层不变) +- **统一抽象**: N8n工作流统一处理所有AI模型调用 +- **类型安全**: TypeScript抽象类确保编译时类型检查 +- **执行高效**: 内存中执行,无数据库查询开销 +- **易于调试**: 完整的IDE支持和断点调试 -### 2. **类型安全** -TypeScript的类型系统确保编译时就能发现接口不匹配、参数错误等问题。 +### 2. **新增数据驱动能力** (数据库层增值) +- **配置动态化**: 模板参数可在线调整,无需重新部署 +- **版本管理**: 支持模板配置的版本控制和回滚 +- **权限控制**: 可设置模板的可见性和访问权限 +- **成本优化**: 基于使用统计调整积分定价策略 -### 3. **易于维护** -每个模板都是独立的类,修改一个模板不会影响其他模板,代码结构清晰。 +### 3. **业务智能提升** (Entity层创新) +- **个性化推荐**: 根据用户历史偏好推荐模板 +- **A/B测试框架**: 同功能不同配置版本效果对比 +- **实时监控**: 模板性能、成功率、用户满意度监控 +- **自动化运营**: 根据数据自动调整模板优先级 -### 4. **快速扩展** -添加新的AI生成功能只需要继承对应的抽象类,实现几个方法即可。 +### 4. **开发效率飞跃** (工具链增强) +```typescript +// 一行代码创建任意模板实例 +const template = await factory.createTemplateByCode('any_template_v1'); -### 5. **测试友好** -每个模板类都可以独立进行单元测试,不需要复杂的数据库环境。 +// 支持批量操作 +const results = await Promise.all( + imageUrls.map(url => template.execute(url)) +); + +// 支持条件执行 +const template = user.isPremium ? + await factory.createTemplateByCode('premium_template_v1') : + await factory.createTemplateByCode('basic_template_v1'); +``` + +### 5. **运营价值最大化** (商业化支持) +- **精准定价**: 基于模板成本和用户价值的动态定价 +- **用户分层**: VIP用户专享模板和优先队列 +- **市场洞察**: 了解哪些模板最受欢迎,指导产品方向 +- **收入优化**: 通过数据分析优化模板组合和推荐策略 ## 🚀 总结 -面向对象的模板管理系统完美解决了您提出的问题: +这种混合式架构完美融合了: -1. **消除调用方式不一致**: 通过抽象类统一接口 -2. **消除参数结果不一致**: 通过类型定义标准化 -3. **简化模板管理**: 通过代码而非数据库管理 -4. **提升开发效率**: 通过继承和多态减少重复代码 +### 保持现有架构优势 +✅ **代码优先**: 执行逻辑仍在TypeScript代码中,保持类型安全 +✅ **性能卓越**: 内存执行,无数据库查询延迟 +✅ **结构清晰**: 抽象层次分明,易于理解和维护 +✅ **扩展便利**: 继承抽象类即可添加新功能 -这个设计既保持了灵活性,又提供了强类型保障,是一个非常优雅的解决方案! +### 新增数据库强大能力 +🆕 **配置灵活**: 模板参数可在线调整,支持实时生效 +🆕 **运营友好**: 完整的管理后台和数据分析面板 +🆕 **业务智能**: 个性化推荐、A/B测试、用户画像 +🆕 **商业价值**: 数据驱动的定价策略和收入优化 + +**您的使用体验**: +```typescript +// 简单优雅的API +const result = await new N8nVideoGenerateTemplateEntity(id, repo) + .onInit() + .then(e => e.execute(imgUrl)); +``` + +这是一个既保持技术优雅性,又具备强大商业价值的完美架构设计! diff --git a/input.md b/input.md index 20340fc..82b823c 100644 --- a/input.md +++ b/input.md @@ -1,35 +1,4 @@ ------------ -图生图+生视频 ------------ - -https://n8n.bowongai.com/webhook/76f92dbf-785f-4add-96b5-a108174b7c14 -{ - "workflow": "图生图+生视频", - "environment": "https://bowongai-test--text-video-agent-fastapi-app.modal.run", - "image_generation": { - "model": "gemini-2.5-flash-image-preview", - "prompt": "Please convert this photo into a highly detailed character model. Place in front of the model a delicate, colorful box featuring the printed image of the person from the photo. Behind the box, show a computer screen actively displaying the Blender modeling process. In front of the box, position the completed character model based on the provided photo, with the PVC texture clearly and realistically rendered. Set the entire scene in a bright, stylish indoor environment resembling a toy collector’s or hobbyist’s room—full of refined details, vibrant décor, and playful atmosphere. Ensure the lighting is crisp and luminous, highlighting both the model and its packaging.", - "image_url": "https://cdn.roasmax.cn/upload/e9066bf20fb546b09ece65d6fbcf2dd3.png" - }, - "video_generation": { - "model": "302/MiniMax-Hailuo-02", - "prompt": "do anything you want", - "duration": "6", - "aspect_ratio": "9:16" - } -} - --------- -图生图 ---------- - -https://n8n.bowongai.com/webhook/76f92dbf-785f-4add-96b5-a108174b7c14 -{ - "workflow": "图生图", - "environment": "https://bowongai-test--text-video-agent-fastapi-app.modal.run", - "image_generation": { - "model": "gemini-2.5-flash-image-preview", - "prompt": "Please convert this photo into a highly detailed character model. Place in front of the model a delicate, colorful box featuring the printed image of the person from the photo. Behind the box, show a computer screen actively displaying the Blender modeling process. In front of the box, position the completed character model based on the provided photo, with the PVC texture clearly and realistically rendered. Set the entire scene in a bright, stylish indoor environment resembling a toy collector’s or hobbyist’s room—full of refined details, vibrant décor, and playful atmosphere. Ensure the lighting is crisp and luminous, highlighting both the model and its packaging.", - "image_url": "https://cdn.roasmax.cn/upload/e9066bf20fb546b09ece65d6fbcf2dd3.png" - } -} \ No newline at end of file +服务:mysql-6bc9094abd49-public.rds.volces.com:53305 +数据库:nano_camera_miniapp +账号:root +密码:WsJWXfwFx0bq6DE2kmB6 \ No newline at end of file diff --git a/ormconfig.js b/ormconfig.js new file mode 100644 index 0000000..7d78a1d --- /dev/null +++ b/ormconfig.js @@ -0,0 +1,17 @@ +module.exports = { + type: 'mysql', + host: 'mysql-6bc9094abd49-public.rds.volces.com', + port: 53305, + username: 'root', + password: 'WsJWXfwFx0bq6DE2kmB6', + database: 'nano_camera_miniapp', + entities: ['src/**/*.entity.ts'], + migrations: ['src/migrations/*.ts'], + cli: { + migrationsDir: 'src/migrations', + }, + synchronize: false, + logging: process.env.NODE_ENV === 'development', + charset: 'utf8mb4', + timezone: '+08:00', +}; \ No newline at end of file diff --git a/package.json b/package.json index a1d5f17..27b76e9 100644 --- a/package.json +++ b/package.json @@ -18,15 +18,26 @@ "test:watch": "jest --watch", "test:cov": "jest --coverage", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "typecheck": "tsc --noEmit", + "typeorm": "typeorm-ts-node-commonjs", + "migration:generate": "npm run typeorm -- migration:generate", + "migration:run": "npm run typeorm -- migration:run", + "migration:revert": "npm run typeorm -- migration:revert", + "schema:sync": "npm run typeorm -- schema:sync", + "schema:drop": "npm run typeorm -- schema:drop" }, "dependencies": { "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", + "@nestjs/typeorm": "^11.0.0", "axios": "^1.11.0", + "mysql2": "^3.9.7", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "typeorm": "^0.3.20" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 632738e..9884d3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,21 +11,33 @@ importers: '@nestjs/common': specifier: ^11.0.1 version: 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^4.0.2 + version: 4.0.2(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.1 version: 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/platform-express': specifier: ^11.0.1 version: 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) + '@nestjs/typeorm': + specifier: ^11.0.0 + version: 11.0.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2))) axios: specifier: ^1.11.0 version: 1.11.0 + mysql2: + specifier: ^3.9.7 + version: 3.14.4 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 rxjs: specifier: ^7.8.1 version: 7.8.2 + typeorm: + specifier: ^0.3.20 + version: 0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)) devDependencies: '@eslint/eslintrc': specifier: ^3.2.0 @@ -657,6 +669,12 @@ packages: class-validator: optional: true + '@nestjs/config@4.0.2': + resolution: {integrity: sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + '@nestjs/core@11.1.6': resolution: {integrity: sha512-siWX7UDgErisW18VTeJA+x+/tpNZrJewjTBsRPF3JVxuWRuAB1kRoiJcxHgln8Lb5UY9NdvklITR84DUEXD0Cg==} engines: {node: '>= 20'} @@ -699,6 +717,15 @@ packages: '@nestjs/platform-express': optional: true + '@nestjs/typeorm@11.0.0': + resolution: {integrity: sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + rxjs: ^7.2.0 + typeorm: ^0.3.0 + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -740,6 +767,9 @@ packages: '@sinonjs/fake-timers@13.0.5': resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} + '@sqltools/formatter@1.2.5': + resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + '@tokenizer/inflate@0.2.7': resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} engines: {node: '>=18'} @@ -953,41 +983,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -1144,6 +1182,10 @@ packages: resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} engines: {node: '>=12'} + ansis@3.17.0: + resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} + engines: {node: '>=14'} + ansis@4.1.0: resolution: {integrity: sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w==} engines: {node: '>=14'} @@ -1152,6 +1194,10 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + app-root-path@3.1.0: + resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==} + engines: {node: '>= 6.0.0'} + append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} @@ -1173,6 +1219,14 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + axios@1.11.0: resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} @@ -1242,6 +1296,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -1254,6 +1311,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + call-bound@1.0.4: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} @@ -1411,6 +1472,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + dayjs@1.11.18: + resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -1438,10 +1502,18 @@ packages: defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -1457,6 +1529,18 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} + dotenv-expand@12.0.1: + resolution: {integrity: sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==} + engines: {node: '>=12'} + + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -1695,6 +1779,10 @@ packages: debug: optional: true + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -1740,6 +1828,9 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1819,6 +1910,9 @@ packages: resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -1846,6 +1940,10 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1884,6 +1982,10 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1911,14 +2013,24 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2170,6 +2282,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2180,6 +2295,14 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lru.min@1.1.2: + resolution: {integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} @@ -2286,6 +2409,14 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + mysql2@3.14.4: + resolution: {integrity: sha512-Cs/jx3WZPNrYHVz+Iunp9ziahaG5uFMvD2R8Zlmc194AqXNxt9HBNu7ZsPYrUtmJsF0egETCWIdMIYAwOGjL1w==} + engines: {node: '>= 8.0'} + + named-placeholders@1.1.3: + resolution: {integrity: sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==} + engines: {node: '>=12.0.0'} + napi-postinstall@0.3.3: resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2438,6 +2569,10 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2573,6 +2708,9 @@ packages: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -2580,9 +2718,18 @@ packages: resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} engines: {node: '>= 18'} + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2639,6 +2786,14 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sql-highlight@6.1.0: + resolution: {integrity: sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==} + engines: {node: '>=14'} + + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -2754,6 +2909,10 @@ packages: tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + to-buffer@1.2.1: + resolution: {integrity: sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==} + engines: {node: '>= 0.4'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2859,9 +3018,69 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typeorm@0.3.26: + resolution: {integrity: sha512-o2RrBNn3lczx1qv4j+JliVMmtkPSqEGpG0UuZkt9tCfWkoXKu8MZnjvp2GjWPll1SehwemQw6xrbVRhmOglj8Q==} + engines: {node: '>=16.13.0'} + hasBin: true + peerDependencies: + '@google-cloud/spanner': ^5.18.0 || ^6.0.0 || ^7.0.0 + '@sap/hana-client': ^2.14.22 + better-sqlite3: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0 + ioredis: ^5.0.4 + mongodb: ^5.8.0 || ^6.0.0 + mssql: ^9.1.1 || ^10.0.1 || ^11.0.1 + mysql2: ^2.2.5 || ^3.0.1 + oracledb: ^6.3.0 + pg: ^8.5.1 + pg-native: ^3.0.0 + pg-query-stream: ^4.0.0 + redis: ^3.1.1 || ^4.0.0 || ^5.0.14 + reflect-metadata: ^0.1.14 || ^0.2.0 + sql.js: ^1.4.0 + sqlite3: ^5.0.3 + ts-node: ^10.7.0 + typeorm-aurora-data-api-driver: ^2.0.0 || ^3.0.0 + peerDependenciesMeta: + '@google-cloud/spanner': + optional: true + '@sap/hana-client': + optional: true + better-sqlite3: + optional: true + ioredis: + optional: true + mongodb: + optional: true + mssql: + optional: true + mysql2: + optional: true + oracledb: + optional: true + pg: + optional: true + pg-native: + optional: true + pg-query-stream: + optional: true + redis: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + ts-node: + optional: true + typeorm-aurora-data-api-driver: + optional: true + typescript-eslint@8.42.0: resolution: {integrity: sha512-ozR/rQn+aQXQxh1YgbCzQWDFrsi9mcg+1PM3l/z5o1+20P7suOIaNg515bpr/OYt6FObz/NHcBstydDLHWeEKg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2918,6 +3137,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -2957,6 +3180,10 @@ packages: webpack-cli: optional: true + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3744,6 +3971,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@nestjs/config@4.0.2(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 16.4.7 + dotenv-expand: 12.0.1 + lodash: 4.17.21 + rxjs: 7.8.2 + '@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -3800,6 +4035,14 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) + '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)))': + dependencies: + '@nestjs/common': 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + typeorm: 0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)) + '@noble/hashes@1.8.0': {} '@nodelib/fs.scandir@2.1.5': @@ -3837,6 +4080,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@sqltools/formatter@1.2.5': {} + '@tokenizer/inflate@0.2.7': dependencies: debug: 4.4.1 @@ -4281,6 +4526,8 @@ snapshots: ansi-styles@6.2.1: {} + ansis@3.17.0: {} + ansis@4.1.0: {} anymatch@3.1.3: @@ -4288,6 +4535,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + app-root-path@3.1.0: {} + append-field@1.0.0: {} arg@4.1.3: {} @@ -4304,6 +4553,12 @@ snapshots: asynckit@0.4.0: {} + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + aws-ssl-profiles@1.1.2: {} + axios@1.11.0: dependencies: follow-redirects: 1.15.11 @@ -4425,6 +4680,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -4436,6 +4696,13 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4567,6 +4834,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + dayjs@1.11.18: {} + debug@4.4.1: dependencies: ms: 2.1.3 @@ -4581,8 +4850,16 @@ snapshots: dependencies: clone: 1.0.4 + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + delayed-stream@1.0.0: {} + denque@2.1.0: {} + depd@2.0.0: {} detect-newline@3.1.0: {} @@ -4594,6 +4871,14 @@ snapshots: diff@4.0.2: {} + dotenv-expand@12.0.1: + dependencies: + dotenv: 16.6.1 + + dotenv@16.4.7: {} + + dotenv@16.6.1: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4874,6 +5159,10 @@ snapshots: follow-redirects@1.15.11: {} + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -4929,6 +5218,10 @@ snapshots: function-bind@1.1.2: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -5015,6 +5308,10 @@ snapshots: has-own-prop@2.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -5041,6 +5338,10 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -5070,6 +5371,8 @@ snapshots: is-arrayish@0.2.1: {} + is-callable@1.2.7: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -5086,10 +5389,18 @@ snapshots: is-promise@4.0.0: {} + is-property@1.0.2: {} + is-stream@2.0.1: {} + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + is-unicode-supported@0.1.0: {} + isarray@2.0.5: {} + isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -5522,6 +5833,8 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + long@5.3.2: {} + lru-cache@10.4.3: {} lru-cache@11.1.0: {} @@ -5530,6 +5843,10 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@7.18.3: {} + + lru.min@1.1.2: {} + magic-string@0.30.17: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5617,6 +5934,22 @@ snapshots: mute-stream@2.0.0: {} + mysql2@3.14.4: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.0 + long: 5.3.2 + lru.min: 1.1.2 + named-placeholders: 1.1.3 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + + named-placeholders@1.1.3: + dependencies: + lru-cache: 7.18.3 + napi-postinstall@0.3.3: {} natural-compare@1.4.0: {} @@ -5747,6 +6080,8 @@ snapshots: pluralize@8.0.0: {} + possible-typed-array-names@1.1.0: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -5883,6 +6218,8 @@ snapshots: transitivePeerDependencies: - supports-color + seq-queue@0.0.5: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -5896,8 +6233,23 @@ snapshots: transitivePeerDependencies: - supports-color + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + setprototypeof@1.2.0: {} + sha.js@2.4.12: + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + to-buffer: 1.2.1 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -5956,6 +6308,10 @@ snapshots: sprintf-js@1.0.3: {} + sql-highlight@6.1.0: {} + + sqlstring@2.3.3: {} + stack-utils@2.0.6: dependencies: escape-string-regexp: 2.0.0 @@ -6068,6 +6424,12 @@ snapshots: tmpl@1.0.5: {} + to-buffer@1.2.1: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -6170,8 +6532,38 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.1 + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + typedarray@0.0.6: {} + typeorm@0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)): + dependencies: + '@sqltools/formatter': 1.2.5 + ansis: 3.17.0 + app-root-path: 3.1.0 + buffer: 6.0.3 + dayjs: 1.11.18 + debug: 4.4.1 + dedent: 1.6.0 + dotenv: 16.6.1 + glob: 10.4.5 + reflect-metadata: 0.2.2 + sha.js: 2.4.12 + sql-highlight: 6.1.0 + tslib: 2.8.1 + uuid: 11.1.0 + yargs: 17.7.2 + optionalDependencies: + mysql2: 3.14.4 + ts-node: 10.9.2(@types/node@22.18.0)(typescript@5.9.2) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + typescript-eslint@8.42.0(eslint@9.34.0)(typescript@5.9.2): dependencies: '@typescript-eslint/eslint-plugin': 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0)(typescript@5.9.2))(eslint@9.34.0)(typescript@5.9.2) @@ -6238,6 +6630,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.1.0: {} + v8-compile-cache-lib@3.0.1: {} v8-to-istanbul@9.3.0: @@ -6297,6 +6691,16 @@ snapshots: - esbuild - uglify-js + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/src/app.module.ts b/src/app.module.ts index 49fb4c7..cd10d36 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,11 +1,26 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { TemplateService } from './templates/index'; import { TemplateManager } from './templates/types'; +import { typeOrmConfig } from './config/database.config'; +import { N8nTemplateEntity } from './entities/n8n-template.entity'; +import { N8nTemplateFactoryService } from './services/n8n-template-factory.service'; +import { TemplateMigrationService } from './services/template-migration.service'; +import { TemplateController } from './controllers/template.controller'; @Module({ - imports: [], - controllers: [AppController], - providers: [TemplateService, TemplateManager], + imports: [ + TypeOrmModule.forRoot(typeOrmConfig), + TypeOrmModule.forFeature([N8nTemplateEntity]) + ], + controllers: [AppController, TemplateController], + providers: [ + TemplateService, + TemplateManager, + N8nTemplateFactoryService, + TemplateMigrationService + ], + exports: [N8nTemplateFactoryService, TemplateMigrationService] }) export class AppModule {} diff --git a/src/config/database.config.ts b/src/config/database.config.ts new file mode 100644 index 0000000..24e726e --- /dev/null +++ b/src/config/database.config.ts @@ -0,0 +1,35 @@ +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { ConfigService } from '@nestjs/config'; + +export const databaseConfig = (configService: ConfigService): TypeOrmModuleOptions => ({ + type: 'mysql', + host: 'mysql-6bc9094abd49-public.rds.volces.com', + port: 53305, + username: 'root', + password: 'WsJWXfwFx0bq6DE2kmB6', + database: 'nano_camera_miniapp', + entities: [__dirname + '/../**/*.entity{.ts,.js}'], + migrations: [__dirname + '/../migrations/*{.ts,.js}'], + synchronize: false, // 生产环境建议设为false + logging: process.env.NODE_ENV === 'development', + migrationsRun: true, // 自动运行迁移 + charset: 'utf8mb4', + timezone: '+08:00', +}); + +// 也可以直接导出配置对象 +export const typeOrmConfig: TypeOrmModuleOptions = { + type: 'mysql', + host: 'mysql-6bc9094abd49-public.rds.volces.com', + port: 53305, + username: 'root', + password: 'WsJWXfwFx0bq6DE2kmB6', + database: 'nano_camera_miniapp', + entities: [__dirname + '/../**/*.entity{.ts,.js}'], + migrations: [__dirname + '/../migrations/*{.ts,.js}'], + synchronize: false, + logging: process.env.NODE_ENV === 'development', + migrationsRun: true, + charset: 'utf8mb4', + timezone: '+08:00', +}; \ No newline at end of file diff --git a/src/controllers/template.controller.ts b/src/controllers/template.controller.ts new file mode 100644 index 0000000..15e6863 --- /dev/null +++ b/src/controllers/template.controller.ts @@ -0,0 +1,349 @@ +import { + Controller, + Get, + Post, + Param, + Body, + ParseIntPipe, + Query, + HttpException, + HttpStatus +} from '@nestjs/common'; +import { N8nTemplateFactoryService } from '../services/n8n-template-factory.service'; +import { TemplateMigrationService } from '../services/template-migration.service'; + +/** + * 模板控制器 + * 提供模板相关的API接口,支持混合式架构的动态模板管理 + */ +@Controller('templates') +export class TemplateController { + constructor( + private readonly templateFactory: N8nTemplateFactoryService, + private readonly migrationService: TemplateMigrationService + ) {} + + /** + * 执行模板 - 通过模板ID + * @param templateId 模板ID + * @param body 请求体,包含imageUrl + * @returns 执行结果 + */ + @Post(':templateId/execute') + 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 { + success: true, + data: { + 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 + ); + } + } + + /** + * 执行模板 - 通过模板代码 + * @param code 模板代码 + * @param body 请求体,包含imageUrl + * @returns 执行结果 + */ + @Post('code/:code/execute') + async executeTemplateByCode( + @Param('code') code: string, + @Body() body: { imageUrl: string } + ) { + try { + const { imageUrl } = body; + + if (!imageUrl) { + throw new HttpException('imageUrl is required', HttpStatus.BAD_REQUEST); + } + + // 通过模板代码创建实例并执行 + const template = await this.templateFactory.createTemplateByCode(code); + const result = await template.execute(imageUrl); + + return { + success: true, + data: { + 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 + ); + } + } + + /** + * 批量执行模板 + * @param templateId 模板ID + * @param body 包含多个imageUrl的数组 + * @returns 批量执行结果 + */ + @Post(':templateId/batch-execute') + 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 { + success: true, + data: { + 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 + ); + } + } + + /** + * 获取所有模板列表 + * @returns 模板配置列表 + */ + @Get() + async getAllTemplates() { + try { + const templates = await this.templateFactory.getAllTemplates(); + return { + success: true, + 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 + })) + }; + } catch (error) { + throw new HttpException( + error.message || 'Failed to fetch templates', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 获取图片类型模板 + * @returns 图片模板列表 + */ + @Get('image') + async getImageTemplates() { + try { + const templates = await this.templateFactory.getImageTemplates(); + return { + success: true, + data: templates + }; + } catch (error) { + throw new HttpException( + error.message || 'Failed to fetch image templates', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 获取视频类型模板 + * @returns 视频模板列表 + */ + @Get('video') + async getVideoTemplates() { + try { + const templates = await this.templateFactory.getVideoTemplates(); + return { + success: true, + data: templates + }; + } catch (error) { + throw new HttpException( + error.message || 'Failed to fetch video templates', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 根据用户偏好推荐模板 + * @param tags 用户偏好标签,逗号分隔 + * @param limit 推荐数量 + * @returns 推荐模板列表 + */ + @Get('recommend') + 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 { + success: true, + data: { + userTags, + recommendedTemplates: templates + } + }; + } catch (error) { + throw new HttpException( + error.message || 'Failed to recommend templates', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 获取特定模板详情 + * @param templateId 模板ID + * @returns 模板详细信息 + */ + @Get(':templateId') + async getTemplateById(@Param('templateId', ParseIntPipe) templateId: number) { + try { + const template = await this.templateFactory.createTemplate(templateId); + + return { + success: true, + data: { + 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 + ); + } + } + + /** + * 迁移现有代码模板到数据库 + * @returns 迁移结果 + */ + @Post('migrate') + async migrateTemplates() { + try { + await this.migrationService.migrateAllExistingTemplates(); + const report = await this.migrationService.getMigrationReport(); + + return { + success: true, + message: 'Templates migrated successfully', + data: report + }; + } catch (error) { + throw new HttpException( + error.message || 'Migration failed', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 同步现有模板配置 + * @returns 同步结果 + */ + @Post('sync') + async syncTemplates() { + try { + await this.migrationService.syncExistingTemplates(); + const report = await this.migrationService.getMigrationReport(); + + return { + success: true, + message: 'Templates synchronized successfully', + data: report + }; + } catch (error) { + throw new HttpException( + error.message || 'Synchronization failed', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 获取迁移状态报告 + * @returns 迁移状态报告 + */ + @Get('admin/migration-report') + async getMigrationReport() { + try { + const report = await this.migrationService.getMigrationReport(); + return { + success: true, + data: report + }; + } catch (error) { + throw new HttpException( + error.message || 'Failed to get migration report', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } +} \ No newline at end of file diff --git a/src/entities/n8n-template-instance.entity.ts b/src/entities/n8n-template-instance.entity.ts new file mode 100644 index 0000000..dd96ae9 --- /dev/null +++ b/src/entities/n8n-template-instance.entity.ts @@ -0,0 +1,176 @@ +import { Repository } from 'typeorm'; +import { N8nTemplateEntity, TemplateType } from './n8n-template.entity'; +import { N8nImageGenerateTemplate, N8nVideoGenerateTemplate } from '../templates/n8nTemplate'; + +/** + * 动态图片模板实体 + * 从数据库加载配置,实现N8nImageGenerateTemplate抽象类 + */ +export class N8nImageGenerateTemplateEntity extends N8nImageGenerateTemplate { + private config: N8nTemplateEntity | null = null; + + constructor( + private templateId: number, + private templateRepository: Repository + ) { + super(); + } + + /** + * 初始化模板配置,从数据库加载数据 + */ + async onInit(): Promise { + this.config = await this.templateRepository.findOne({ + where: { + id: this.templateId, + templateType: TemplateType.IMAGE, + isActive: true + } + }); + + if (!this.config) { + throw new Error(`Image template with id ${this.templateId} not found or inactive`); + } + + return this; + } + + // 从数据库动态获取配置 - 实现Template抽象类的属性 + get code(): string { + return this.config?.code || ''; + } + + get name(): string { + return this.config?.name || ''; + } + + get description(): string { + return this.config?.description || ''; + } + + get creditCost(): number { + return this.config?.creditCost || 0; + } + + get version(): string { + return this.config?.version || ''; + } + + get input(): string { + return this.config?.inputExampleUrl || ''; + } + + get output(): string { + return this.config?.outputExampleUrl || ''; + } + + get tags(): string[] { + return this.config?.tags || []; + } + + // 实现N8nImageGenerateTemplate抽象类的属性 + get imageModel(): string { + return this.config?.imageModel || ''; + } + + get imagePrompt(): string { + return this.config?.imagePrompt || ''; + } + + // execute方法自动继承自N8nImageGenerateTemplate,无需重写 +} + +/** + * 动态视频模板实体 + * 从数据库加载配置,实现N8nVideoGenerateTemplate抽象类 + */ +export class N8nVideoGenerateTemplateEntity extends N8nVideoGenerateTemplate { + private config: N8nTemplateEntity | null = null; + + constructor( + private templateId: number, + private templateRepository: Repository + ) { + super(); + } + + /** + * 初始化模板配置,从数据库加载数据 + */ + async onInit(): Promise { + this.config = await this.templateRepository.findOne({ + where: { + id: this.templateId, + templateType: TemplateType.VIDEO, + isActive: true + } + }); + + if (!this.config) { + throw new Error(`Video template with id ${this.templateId} not found or inactive`); + } + + return this; + } + + // 从数据库动态获取配置 - 实现Template抽象类的属性 + get code(): string { + return this.config?.code || ''; + } + + get name(): string { + return this.config?.name || ''; + } + + get description(): string { + return this.config?.description || ''; + } + + get creditCost(): number { + return this.config?.creditCost || 0; + } + + get version(): string { + return this.config?.version || ''; + } + + get input(): string { + return this.config?.inputExampleUrl || ''; + } + + get output(): string { + return this.config?.outputExampleUrl || ''; + } + + get tags(): string[] { + return this.config?.tags || []; + } + + // 实现N8nImageGenerateTemplate抽象类的属性(视频模板也需要图片生成) + get imageModel(): string { + return this.config?.imageModel || ''; + } + + get imagePrompt(): string { + return this.config?.imagePrompt || ''; + } + + // 实现N8nVideoGenerateTemplate抽象类的属性 + get videoModel(): string { + return this.config?.videoModel || ''; + } + + get videoPrompt(): string { + return this.config?.videoPrompt || ''; + } + + get duration(): number { + return this.config?.duration || 6; + } + + get aspectRatio(): string { + return this.config?.aspectRatio || '9:16'; + } + + // execute方法自动继承自N8nVideoGenerateTemplate,无需重写 +} \ No newline at end of file diff --git a/src/entities/n8n-template.entity.ts b/src/entities/n8n-template.entity.ts new file mode 100644 index 0000000..4a78f07 --- /dev/null +++ b/src/entities/n8n-template.entity.ts @@ -0,0 +1,88 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + Index +} from 'typeorm'; + +export enum TemplateType { + IMAGE = 'image', + VIDEO = 'video' +} + +@Entity('n8n_templates') +@Index('idx_code', ['code']) +@Index('idx_type', ['templateType']) +@Index('idx_class', ['templateClass']) +@Index('idx_active', ['isActive']) +export class N8nTemplateEntity { + @PrimaryGeneratedColumn('increment', { type: 'bigint' }) + id: number; + + @Column({ unique: true, length: 100, comment: '模板唯一标识码' }) + code: string; + + @Column({ length: 200, comment: '模板名称' }) + name: string; + + @Column({ type: 'text', comment: '模板详细描述' }) + description: string; + + @Column({ name: 'credit_cost', default: 0, comment: '积分消耗' }) + creditCost: number; + + @Column({ length: 50, comment: '版本号' }) + version: string; + + @Column({ name: 'input_example_url', type: 'text', nullable: true, comment: '输入示例图片' }) + inputExampleUrl: string; + + @Column({ name: 'output_example_url', type: 'text', nullable: true, comment: '输出示例图片' }) + outputExampleUrl: string; + + @Column({ type: 'json', nullable: true, comment: '标签数组' }) + tags: string[]; + + @Column({ + name: 'template_type', + type: 'enum', + enum: TemplateType, + comment: '模板类型' + }) + templateType: TemplateType; + + @Column({ name: 'template_class', length: 100, comment: '具体模板类名' }) + templateClass: string; + + @Column({ name: 'image_model', length: 100, nullable: true, comment: '图片生成模型' }) + imageModel: string; + + @Column({ name: 'image_prompt', type: 'text', nullable: true, comment: '图片生成提示词' }) + imagePrompt: string; + + @Column({ name: 'video_model', length: 100, nullable: true, comment: '视频生成模型' }) + videoModel: string; + + @Column({ name: 'video_prompt', type: 'text', nullable: true, comment: '视频生成提示词' }) + videoPrompt: string; + + @Column({ nullable: true, comment: '视频时长(秒)' }) + duration: number; + + @Column({ name: 'aspect_ratio', length: 20, nullable: true, comment: '视频比例' }) + aspectRatio: string; + + @Column({ name: 'is_active', default: true, comment: '是否启用' }) + isActive: boolean; + + @Column({ name: 'sort_order', default: 0, comment: '排序权重' }) + sortOrder: number; + + @CreateDateColumn({ name: 'created_at', comment: '创建时间' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at', comment: '更新时间' }) + updatedAt: Date; +} \ No newline at end of file diff --git a/src/migrations/1725434680000-AddPhotoRestoreTemplate.ts b/src/migrations/1725434680000-AddPhotoRestoreTemplate.ts new file mode 100644 index 0000000..705f473 --- /dev/null +++ b/src/migrations/1725434680000-AddPhotoRestoreTemplate.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPhotoRestoreTemplate1725434680000 implements MigrationInterface { + name = 'AddPhotoRestoreTemplate1725434680000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO n8n_templates ( + code, name, description, credit_cost, version, + input_example_url, output_example_url, tags, + template_type, template_class, + image_model, image_prompt, + video_model, video_prompt, duration, aspect_ratio, + is_active, sort_order + ) VALUES ( + 'photo_restore_v1', + '老照片修复上色', + '将黑白老照片修复并上色,让历史照片重现生机', + 12, + '1.0.0', + 'https://file.302.ai/gpt/imgs/20250903/6033b7e701a64e4a97d4608d4a0ed308.jpg', + 'https://file.302.ai/gpt/imgs/20250903/07ed5fb40090ac6ff51c874cc75ea8f2.jpg', + JSON_ARRAY('老照片', '修复', '上色', '黑白转彩色', 'AI修复'), + 'image', + 'PhotoRestoreTemplate', + 'gemini-2.5-flash-image-preview', + 'Restore this old photo and colorize the entire black and white photo', + NULL, + NULL, + NULL, + NULL, + true, + 100 + ) + `); + + console.log('✅ PhotoRestoreTemplate migration completed'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DELETE FROM n8n_templates WHERE code = 'photo_restore_v1' + `); + + console.log('⏭️ PhotoRestoreTemplate migration reverted'); + } +} \ No newline at end of file diff --git a/src/migrations/1725434690000-AddCharacterFigurineTemplate.ts b/src/migrations/1725434690000-AddCharacterFigurineTemplate.ts new file mode 100644 index 0000000..6210be2 --- /dev/null +++ b/src/migrations/1725434690000-AddCharacterFigurineTemplate.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddCharacterFigurineTemplate1725434690000 implements MigrationInterface { + name = 'AddCharacterFigurineTemplate1725434690000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO n8n_templates ( + code, name, description, credit_cost, version, + input_example_url, output_example_url, tags, + template_type, template_class, + image_model, image_prompt, + video_model, video_prompt, duration, aspect_ratio, + is_active, sort_order + ) VALUES ( + 'character_figurine_v1', + '人物手办', + '将人物照片制作成精细的角色手办模型,展示在收藏家房间中,并生成抚摸手办的视频', + 28, + '1.0.0', + 'https://cdn.roasmax.cn/upload/3d590851eb584e92aa415a964e93260e.jpg', + 'https://file.302.ai/gpt/imgs/20250903/13256820c89e486790234e182c972905.mp4', + JSON_ARRAY('人物', '手办', '模型', '收藏', 'PVC', '角色模型', '视频生成'), + 'video', + 'CharacterFigurineTemplate', + 'gemini-2.5-flash-image-preview', + 'Transform this photo into a highly detailed character model. Place a detailed, colorful box with the image of the character from the photo in front of the model. In front of the box, place the finished character model from the photo, with the PVC texture rendered realistically. Set the entire scene in a bright, stylish interior, reminiscent of a toy collector\\'s or hobbyist\\'s room—full of intricate details, vibrant decor, and a playful atmosphere. Ensure the lighting is bright and clear to highlight the model and its packaging.', + '302/MiniMax-Hailuo-02', + '一只手摸了摸手办的脑袋', + 6, + '9:16', + true, + 90 + ) + `); + + console.log('✅ CharacterFigurineTemplate migration completed'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DELETE FROM n8n_templates WHERE code = 'character_figurine_v1' + `); + + console.log('⏭️ CharacterFigurineTemplate migration reverted'); + } +} \ No newline at end of file diff --git a/src/migrations/1725434700000-AddPetFigurineTemplate.ts b/src/migrations/1725434700000-AddPetFigurineTemplate.ts new file mode 100644 index 0000000..d7f9e7f --- /dev/null +++ b/src/migrations/1725434700000-AddPetFigurineTemplate.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPetFigurineTemplate1725434700000 implements MigrationInterface { + name = 'AddPetFigurineTemplate1725434700000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO n8n_templates ( + code, name, description, credit_cost, version, + input_example_url, output_example_url, tags, + template_type, template_class, + image_model, image_prompt, + video_model, video_prompt, duration, aspect_ratio, + is_active, sort_order + ) VALUES ( + 'pet_figurine_v1', + '宠物手办', + '将宠物插图制作成1/7比例的商业化手办,展示在电脑桌上,并生成手办把玩视频', + 30, + '1.0.0', + 'https://cdn.roasmax.cn/upload/7cc18d70e9ba48bc8cadff7d3cdf62e1.png', + 'https://file.302.ai/gpt/imgs/20250903/92e2e8adf4754d1a8bf406418ab4d10d.mp4', + JSON_ARRAY('宠物', '手办', '模型', '收藏', 'BANDAI', 'ZBrush', '视频生成'), + 'video', + 'PetFigurineTemplate', + 'gemini-2.5-flash-image', + 'Create a 1/7 scale commercialized figure of the animal in the illustration, in a realistic style and environment. Place the figure on a computer desk, using a circular transparent acrylic base without any text. On the Macbook Pro screen, display the ZBrush modeling process of the figure in line mode. Next to the computer screen, place a BANDAI-style toy packaging box printed with the original artwork.', + '302/MiniMax-Hailuo-02', + '一双手拿起手办,在手里展示把玩', + 6, + '9:16', + true, + 85 + ) + `); + + console.log('✅ PetFigurineTemplate migration completed'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DELETE FROM n8n_templates WHERE code = 'pet_figurine_v1' + `); + + console.log('⏭️ PetFigurineTemplate migration reverted'); + } +} \ No newline at end of file diff --git a/src/migrations/1725434710000-AddCosplayRealPersonTemplate.ts b/src/migrations/1725434710000-AddCosplayRealPersonTemplate.ts new file mode 100644 index 0000000..a092431 --- /dev/null +++ b/src/migrations/1725434710000-AddCosplayRealPersonTemplate.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddCosplayRealPersonTemplate1725434710000 implements MigrationInterface { + name = 'AddCosplayRealPersonTemplate1725434710000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO n8n_templates ( + code, name, description, credit_cost, version, + input_example_url, output_example_url, tags, + template_type, template_class, + image_model, image_prompt, + video_model, video_prompt, duration, aspect_ratio, + is_active, sort_order + ) VALUES ( + 'cosplay_real_person_v1', + 'cos真人', + '生成cosplay这张插图的真人照片,背景设置在Comiket,并生成cosplay人物做经典动作的视频', + 25, + '1.0.0', + 'https://cdn.roasmax.cn/upload/c69a847f8e8740f594f731ef502c4054.png', + 'https://file.302.ai/gpt/imgs/20250903/a78d664707c04cf9815f5195b4bfcd18.mp4', + JSON_ARRAY('cosplay', '真人', 'Comiket', '动漫', '二次元', '视频生成'), + 'video', + 'CosplayRealPersonTemplate', + 'gemini-2.5-flash-image', + '生成一张cosplay这张插图的照片,背景设置在Comiket,插图主体大一些', + '302/MiniMax-Hailuo-02', + '图片里的cosplay人物动起来,做出这个cosplay人物的经典动作', + 6, + '9:16', + true, + 80 + ) + `); + + console.log('✅ CosplayRealPersonTemplate migration completed'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DELETE FROM n8n_templates WHERE code = 'cosplay_real_person_v1' + `); + + console.log('⏭️ CosplayRealPersonTemplate migration reverted'); + } +} \ No newline at end of file diff --git a/src/migrations/1725434720000-AddGarageOpeningTemplate.ts b/src/migrations/1725434720000-AddGarageOpeningTemplate.ts new file mode 100644 index 0000000..7c0f544 --- /dev/null +++ b/src/migrations/1725434720000-AddGarageOpeningTemplate.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddGarageOpeningTemplate1725434720000 implements MigrationInterface { + name = 'AddGarageOpeningTemplate1725434720000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO n8n_templates ( + code, name, description, credit_cost, version, + input_example_url, output_example_url, tags, + template_type, template_class, + image_model, image_prompt, + video_model, video_prompt, duration, aspect_ratio, + is_active, sort_order + ) VALUES ( + 'garage_opening_v1', + '车库开门', + '生成豪宅车库开门场景,展示人物站在现代豪宅前,车库门缓缓升起露出豪车的视频', + 35, + '1.0.0', + 'https://bowong-ai-shanghai.tos-cn-shanghai.volces.com/images/1756889137996_jujhz5.jpg', + 'https://file.302.ai/gpt/imgs/20250903/c98adec564fb4cf89813ecd89afc13bd.mp4', + JSON_ARRAY('车库', '豪宅', '豪车', 'Lamborghini', '时尚', '奢华', '视频生成'), + 'video', + 'GarageOpeningTemplate', + 'gemini-2.5-flash-image-preview', + 'First shot, 4K still frame: A person with long straight black hair, facing camera, wearing a plain pastel-pink track jacket and matching full-length track pants, white crew socks, and clean pink sneakers. They stand on sun-lit marble in front of a closed matte-black garage door of a modern white mansion. Bright sunlight, palm trees on both sides, marble stairs on the right.', + '302/veo3-fast-frames', + 'Realistic 4K movie video clip: In front of a modern mansion garage with a large black curtain door. As the black rolling shutter door slowly rises (with a smooth and stable movement), the subject walks backwards in perfect sync (with a natural gait and eye contact with the door). Once the two Lamborghini Urus SUVs are fully exposed from the car doors—one in bright pink and the other in matte dark brown—the subject will stop in front of the cars and make a natural gesture (pointing to features, explaining, confident posture). At the last second of the video, the camera will focus on the subject and pull away a bit. Throughout the video, the camera steadily advances (focusing on the subject and cars at a slightly lower angle to enhance luxury), with bright sunlight illuminating the marble walls, palm trees on both sides of the entrance, and visible stairs on the right. This scene combines high-end fashion, luxurious atmosphere, and smooth motion transitions.', + 6, + '16:9', + true, + 75 + ) + `); + + console.log('✅ GarageOpeningTemplate migration completed'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DELETE FROM n8n_templates WHERE code = 'garage_opening_v1' + `); + + console.log('⏭️ GarageOpeningTemplate migration reverted'); + } +} \ No newline at end of file diff --git a/src/migrations/1725434730000-AddJapaneseMagazineTemplate.ts b/src/migrations/1725434730000-AddJapaneseMagazineTemplate.ts new file mode 100644 index 0000000..0639f72 --- /dev/null +++ b/src/migrations/1725434730000-AddJapaneseMagazineTemplate.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddJapaneseMagazineTemplate1725434730000 implements MigrationInterface { + name = 'AddJapaneseMagazineTemplate1725434730000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO n8n_templates ( + code, name, description, credit_cost, version, + input_example_url, output_example_url, tags, + template_type, template_class, + image_model, image_prompt, + video_model, video_prompt, duration, aspect_ratio, + is_active, sort_order + ) VALUES ( + 'japanese_magazine_v1', + '日本杂志', + '生成时尚的日本杂志风格数字海报,包含多个模特和日语文本元素,并制作人物律动视频', + 32, + '1.0.0', + 'https://cdn.roasmax.cn/upload/1980dcb0b0e3467794065c85ba607913.jpg', + 'https://file.302.ai/gpt/imgs/20250903/a13130f161814b36826672ff08f68876.mp4', + JSON_ARRAY('日本杂志', '时尚', '海报', '街头服饰', '拼贴', '日语文本', '视频生成'), + 'video', + 'JapaneseMagazineTemplate', + 'mj', + '品牌活动的编辑风格数字海报,以时尚的中央女模特为特色,她只展示头部近景、分层的街头服饰和自信的眼神。她被大胆的灯光和拼贴风格的阴影所包围。在背景前方下面,微缩的另外的她人工智能生成的模型展示了俏皮、休闲的风格——一个穿着无檐便帽和牛仔夹克,另一个穿着柔和的运动服,手势富有表现力。该场景使用纯白色的工作室背景,充满了剪纸贴纸、星星、箭头和粗体日语文本,并配以彩色轮廓。具有大胆的布局和优雅的排版。', + '302/MiniMax-Hailuo-02', + '这张图片的人物都在自己的位置不变,但是都要欢快的小幅度的律动', + 6, + '9:16', + true, + 70 + ) + `); + + console.log('✅ JapaneseMagazineTemplate migration completed'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DELETE FROM n8n_templates WHERE code = 'japanese_magazine_v1' + `); + + console.log('⏭️ JapaneseMagazineTemplate migration reverted'); + } +} \ No newline at end of file diff --git a/src/migrations/1725434740000-AddNuclearExplosionTemplate.ts b/src/migrations/1725434740000-AddNuclearExplosionTemplate.ts new file mode 100644 index 0000000..bc34aa4 --- /dev/null +++ b/src/migrations/1725434740000-AddNuclearExplosionTemplate.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddNuclearExplosionTemplate1725434740000 implements MigrationInterface { + name = 'AddNuclearExplosionTemplate1725434740000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO n8n_templates ( + code, name, description, credit_cost, version, + input_example_url, output_example_url, tags, + template_type, template_class, + image_model, image_prompt, + video_model, video_prompt, duration, aspect_ratio, + is_active, sort_order + ) VALUES ( + 'nuclear_explosion_v1', + '原子弹爆炸', + '生成远处核爆炸场景,冲击波向人物吹来的电影化启示录风格视频', + 20, + '1.0.0', + 'https://cdn.roasmax.cn/upload/1980dcb0b0e3467794065c85ba607913.jpg', + 'https://file.302.ai/gpt/imgs/20250903/4f4fbdff8c3343858f897685013b4a24.mp4', + JSON_ARRAY('核爆炸', '冲击波', '启示录', '电影化', '特效', '视频生成'), + 'video', + 'NuclearExplosionTemplate', + 'gemini-2.5-flash-image-preview', + 'Keep the original image unchanged, prepare for nuclear explosion video generation', + '302/MiniMax-Hailuo-02', + '远处爆发了一场核爆炸,冲击波随风吹向主体。大规模、电影化、启示录式。画面中的人物始终看向摄像机的方向', + 6, + '9:16', + true, + 65 + ) + `); + + console.log('✅ NuclearExplosionTemplate migration completed'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DELETE FROM n8n_templates WHERE code = 'nuclear_explosion_v1' + `); + + console.log('⏭️ NuclearExplosionTemplate migration reverted'); + } +} \ No newline at end of file diff --git a/src/migrations/1725434750000-AddOpenEyesTemplate.ts b/src/migrations/1725434750000-AddOpenEyesTemplate.ts new file mode 100644 index 0000000..f03f3a1 --- /dev/null +++ b/src/migrations/1725434750000-AddOpenEyesTemplate.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddOpenEyesTemplate1725434750000 implements MigrationInterface { + name = 'AddOpenEyesTemplate1725434750000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO n8n_templates ( + code, name, description, credit_cost, version, + input_example_url, output_example_url, tags, + template_type, template_class, + image_model, image_prompt, + video_model, video_prompt, duration, aspect_ratio, + is_active, sort_order + ) VALUES ( + 'open_eyes_v1', + '闭眼照片修复', + '修复照片闭眼问题,让人物睁开眼睛', + 10, + '1.0.0', + 'https://bowong-ai-shanghai.tos-cn-shanghai.volces.com/images/1756887607688_bjqnk3.png', + 'https://bowong-ai-shanghai.tos-cn-shanghai.volces.com/images/1756889137996_jujhz5.jpg', + JSON_ARRAY('闭眼', '照片修复', '人物'), + 'image', + 'OpenEyesTemplate', + 'gemini-2.5-flash-image-preview', + '图像中的主体(人物、动物、meme、或者表情等包含广义上五官的东西)闭上眼睛', + NULL, + NULL, + NULL, + NULL, + true, + 60 + ) + `); + + console.log('✅ OpenEyesTemplate migration completed'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DELETE FROM n8n_templates WHERE code = 'open_eyes_v1' + `); + + console.log('⏭️ OpenEyesTemplate migration reverted'); + } +} \ No newline at end of file diff --git a/src/services/n8n-template-factory.service.ts b/src/services/n8n-template-factory.service.ts new file mode 100644 index 0000000..e234994 --- /dev/null +++ b/src/services/n8n-template-factory.service.ts @@ -0,0 +1,192 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { N8nTemplateEntity, TemplateType } from '../entities/n8n-template.entity'; +import { + N8nImageGenerateTemplateEntity, + N8nVideoGenerateTemplateEntity +} from '../entities/n8n-template-instance.entity'; + +/** + * N8n模板工厂服务 + * 负责创建动态模板实例,支持从数据库加载配置 + */ +@Injectable() +export class N8nTemplateFactoryService { + constructor( + @InjectRepository(N8nTemplateEntity) + private templateRepository: Repository, + ) {} + + /** + * 创建图片模板实例 + * @param templateId 模板ID + * @returns 初始化后的图片模板实例 + */ + async createImageTemplate(templateId: number): Promise { + const instance = new N8nImageGenerateTemplateEntity(templateId, this.templateRepository); + return await instance.onInit(); + } + + /** + * 创建视频模板实例 + * @param templateId 模板ID + * @returns 初始化后的视频模板实例 + */ + async createVideoTemplate(templateId: number): Promise { + const instance = new N8nVideoGenerateTemplateEntity(templateId, this.templateRepository); + return await instance.onInit(); + } + + /** + * 通过模板代码创建实例 + * @param code 模板代码 + * @returns 对应类型的模板实例 + */ + async createTemplateByCode( + code: string + ): Promise { + const config = await this.templateRepository.findOne({ + where: { code, isActive: true } + }); + + if (!config) { + throw new Error(`Template with code ${code} not found or inactive`); + } + + if (config.templateType === TemplateType.IMAGE) { + return this.createImageTemplate(config.id); + } else { + return this.createVideoTemplate(config.id); + } + } + + /** + * 通用模板创建方法 + * @param templateId 模板ID + * @returns 对应类型的模板实例 + */ + async createTemplate( + templateId: number + ): Promise { + const config = await this.templateRepository.findOne({ + where: { id: templateId, isActive: true } + }); + + if (!config) { + throw new Error(`Template with id ${templateId} not found or inactive`); + } + + if (config.templateType === TemplateType.IMAGE) { + return this.createImageTemplate(templateId); + } else { + return this.createVideoTemplate(templateId); + } + } + + /** + * 获取所有可用模板配置 + * @returns 所有激活状态的模板配置 + */ + async getAllTemplates(): Promise { + return this.templateRepository.find({ + where: { isActive: true }, + order: { sortOrder: 'DESC', createdAt: 'DESC' } + }); + } + + /** + * 按类型获取模板配置 + * @param type 模板类型 + * @returns 指定类型的模板配置列表 + */ + async getTemplatesByType(type: TemplateType): Promise { + return this.templateRepository.find({ + where: { templateType: type, isActive: true }, + order: { sortOrder: 'DESC', createdAt: 'DESC' } + }); + } + + /** + * 获取图片模板配置列表 + * @returns 图片类型的模板配置 + */ + async getImageTemplates(): Promise { + return this.getTemplatesByType(TemplateType.IMAGE); + } + + /** + * 获取视频模板配置列表 + * @returns 视频类型的模板配置 + */ + async getVideoTemplates(): Promise { + return this.getTemplatesByType(TemplateType.VIDEO); + } + + /** + * 通过代码获取模板配置 + * @param code 模板代码 + * @returns 模板配置信息 + */ + async getTemplateByCode(code: string): Promise { + return this.templateRepository.findOne({ + where: { code, isActive: true } + }); + } + + /** + * 批量执行模板 + * @param templateId 模板ID + * @param imageUrls 图片URL数组 + * @returns 执行结果数组 + */ + async batchExecute(templateId: number, imageUrls: string[]): Promise { + const template = await this.createTemplate(templateId); + return Promise.all(imageUrls.map(url => template.execute(url))); + } + + /** + * 根据用户偏好推荐模板(示例实现) + * @param userTags 用户偏好标签 + * @param limit 推荐数量限制 + * @returns 推荐的模板配置 + */ + async recommendTemplates(userTags: string[], limit: number = 5): Promise { + // 这里可以实现更复杂的推荐算法 + const query = this.templateRepository + .createQueryBuilder('template') + .where('template.isActive = :isActive', { isActive: true }) + .orderBy('template.sortOrder', 'DESC') + .addOrderBy('template.createdAt', 'DESC') + .limit(limit); + + // 如果提供了用户标签,可以基于标签匹配进行推荐 + if (userTags.length > 0) { + query.andWhere( + 'JSON_OVERLAPS(template.tags, :userTags)', + { userTags: JSON.stringify(userTags) } + ); + } + + return query.getMany(); + } + + /** + * 获取模板使用统计(预留接口,需要配合使用记录表实现) + * @param templateId 模板ID + * @returns 使用统计信息 + */ + async getTemplateStats(templateId: number): Promise<{ + totalUsage: number; + successRate: number; + averageExecutionTime: number; + }> { + // TODO: 实现模板使用统计逻辑 + // 需要配合模板使用记录表(template_usage_logs) + return { + totalUsage: 0, + successRate: 1.0, + averageExecutionTime: 0 + }; + } +} \ No newline at end of file diff --git a/src/services/template-migration.service.ts b/src/services/template-migration.service.ts new file mode 100644 index 0000000..0e0a35f --- /dev/null +++ b/src/services/template-migration.service.ts @@ -0,0 +1,280 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { N8nTemplateEntity, TemplateType } from '../entities/n8n-template.entity'; +import { N8nImageGenerateTemplate, N8nVideoGenerateTemplate } from '../templates/n8nTemplate'; + +// 导入现有模板 +import { PhotoRestoreTemplate } from '../templates/n8nTemplates/PhotoRestoreTemplate'; +import { CharacterFigurineTemplate } from '../templates/n8nTemplates/CharacterFigurineTemplate'; +import { PetFigurineTemplate } from '../templates/n8nTemplates/PetFigurineTemplate'; +import { CosplayRealPersonTemplate } from '../templates/n8nTemplates/CosplayRealPersonTemplate'; +import { GarageOpeningTemplate } from '../templates/n8nTemplates/GarageOpeningTemplate'; +import { JapaneseMagazineTemplate } from '../templates/n8nTemplates/JapaneseMagazineTemplate'; +import { NuclearExplosionTemplate } from '../templates/n8nTemplates/NuclearExplosionTemplate'; + +/** + * 模板迁移服务 + * 负责将现有代码中的模板配置迁移到数据库 + */ +@Injectable() +export class TemplateMigrationService { + constructor( + @InjectRepository(N8nTemplateEntity) + private templateRepository: Repository, + ) {} + + /** + * 迁移所有现有模板到数据库 + */ + async migrateAllExistingTemplates(): Promise { + console.log('开始迁移现有模板到数据库...'); + + // 创建模板实例 + const templates = [ + new PhotoRestoreTemplate(), + new CharacterFigurineTemplate(), + new PetFigurineTemplate(), + new CosplayRealPersonTemplate(), + new GarageOpeningTemplate(), + new JapaneseMagazineTemplate(), + new NuclearExplosionTemplate(), + ]; + + let migratedCount = 0; + let skippedCount = 0; + + for (const template of templates) { + try { + const result = await this.migrateTemplate(template); + if (result) { + migratedCount++; + console.log(`✅ 成功迁移模板: ${template.code} - ${template.name}`); + } else { + skippedCount++; + console.log(`⏭️ 跳过已存在模板: ${template.code} - ${template.name}`); + } + } catch (error) { + console.error(`❌ 迁移模板失败: ${template.code}`, error); + } + } + + console.log(`\n📊 迁移完成统计:`); + console.log(` • 新增模板: ${migratedCount} 个`); + console.log(` • 跳过模板: ${skippedCount} 个`); + console.log(` • 总模板数: ${templates.length} 个\n`); + } + + /** + * 迁移单个模板 + * @param template 模板实例 + * @returns 是否创建了新记录 + */ + async migrateTemplate(template: any): Promise { + // 检查模板是否已存在 + const existing = await this.templateRepository.findOne({ + where: { code: template.code } + }); + + if (existing) { + return false; // 已存在,跳过 + } + + // 创建新的实体 + const entity = new N8nTemplateEntity(); + + // 基础属性 + entity.code = template.code; + entity.name = template.name; + entity.description = template.description; + entity.creditCost = template.creditCost; + entity.version = template.version; + entity.inputExampleUrl = template.input; + entity.outputExampleUrl = template.output; + entity.tags = template.tags; + entity.templateClass = template.constructor.name; + + // 判断模板类型并设置相应属性 + if (template instanceof N8nImageGenerateTemplate) { + entity.templateType = TemplateType.IMAGE; + entity.imageModel = template.imageModel; + entity.imagePrompt = template.imagePrompt; + } else if (template instanceof N8nVideoGenerateTemplate) { + entity.templateType = TemplateType.VIDEO; + entity.imageModel = template.imageModel; + entity.imagePrompt = template.imagePrompt; + entity.videoModel = template.videoModel; + entity.videoPrompt = template.videoPrompt; + entity.duration = template.duration; + entity.aspectRatio = template.aspectRatio; + } + + // 保存到数据库 + await this.templateRepository.save(entity); + return true; + } + + /** + * 清空所有模板数据(谨慎使用) + */ + async clearAllTemplates(): Promise { + console.log('⚠️ 清空所有模板数据...'); + await this.templateRepository.clear(); + console.log('✅ 所有模板数据已清空'); + } + + /** + * 同步现有模板配置(更新已存在的模板) + */ + async syncExistingTemplates(): Promise { + console.log('开始同步现有模板配置...'); + + const templates = [ + new PhotoRestoreTemplate(), + new CharacterFigurineTemplate(), + new PetFigurineTemplate(), + new CosplayRealPersonTemplate(), + new GarageOpeningTemplate(), + new JapaneseMagazineTemplate(), + new NuclearExplosionTemplate(), + ]; + + let updatedCount = 0; + let createdCount = 0; + + for (const template of templates) { + try { + const existing = await this.templateRepository.findOne({ + where: { code: template.code } + }); + + if (existing) { + // 更新现有模板 + await this.updateTemplateFromCode(existing, template); + updatedCount++; + console.log(`🔄 更新模板: ${template.code} - ${template.name}`); + } else { + // 创建新模板 + await this.migrateTemplate(template); + createdCount++; + console.log(`➕ 创建模板: ${template.code} - ${template.name}`); + } + } catch (error) { + console.error(`❌ 同步模板失败: ${template.code}`, error); + } + } + + console.log(`\n📊 同步完成统计:`); + console.log(` • 更新模板: ${updatedCount} 个`); + console.log(` • 创建模板: ${createdCount} 个`); + } + + /** + * 从代码模板更新数据库实体 + */ + private async updateTemplateFromCode(entity: N8nTemplateEntity, template: any): Promise { + // 更新基础属性 + entity.name = template.name; + entity.description = template.description; + entity.creditCost = template.creditCost; + entity.version = template.version; + entity.inputExampleUrl = template.input; + entity.outputExampleUrl = template.output; + entity.tags = template.tags; + entity.templateClass = template.constructor.name; + + // 更新类型特定属性 + if (template instanceof N8nImageGenerateTemplate) { + entity.templateType = TemplateType.IMAGE; + entity.imageModel = template.imageModel; + entity.imagePrompt = template.imagePrompt; + } else if (template instanceof N8nVideoGenerateTemplate) { + entity.templateType = TemplateType.VIDEO; + entity.imageModel = template.imageModel; + entity.imagePrompt = template.imagePrompt; + entity.videoModel = template.videoModel; + entity.videoPrompt = template.videoPrompt; + entity.duration = template.duration; + entity.aspectRatio = template.aspectRatio; + } + + await this.templateRepository.save(entity); + } + + /** + * 获取迁移状态报告 + */ + async getMigrationReport(): Promise<{ + totalInDatabase: number; + totalInCode: number; + missingInDatabase: string[]; + extraInDatabase: string[]; + }> { + // 数据库中的模板 + const dbTemplates = await this.templateRepository.find({ + select: ['code', 'name'] + }); + + // 代码中的模板 + const codeTemplates = [ + new PhotoRestoreTemplate(), + new CharacterFigurineTemplate(), + new PetFigurineTemplate(), + new CosplayRealPersonTemplate(), + new GarageOpeningTemplate(), + new JapaneseMagazineTemplate(), + new NuclearExplosionTemplate(), + ]; + + const dbCodes = new Set(dbTemplates.map(t => t.code)); + const codeCodes = new Set(codeTemplates.map(t => t.code)); + + // 找出差异 + const missingInDatabase = codeTemplates + .filter(t => !dbCodes.has(t.code)) + .map(t => `${t.code} (${t.name})`); + + const extraInDatabase = dbTemplates + .filter(t => !codeCodes.has(t.code as any)) + .map(t => `${t.code} (${t.name})`); + + return { + totalInDatabase: dbTemplates.length, + totalInCode: codeTemplates.length, + missingInDatabase, + extraInDatabase + }; + } + + /** + * 打印迁移状态报告 + */ + async printMigrationReport(): Promise { + const report = await this.getMigrationReport(); + + console.log('\n📋 模板迁移状态报告:'); + console.log('='.repeat(50)); + console.log(`数据库中的模板数量: ${report.totalInDatabase}`); + console.log(`代码中的模板数量: ${report.totalInCode}`); + + if (report.missingInDatabase.length > 0) { + console.log('\n❌ 缺失在数据库中的模板:'); + report.missingInDatabase.forEach(template => { + console.log(` • ${template}`); + }); + } + + if (report.extraInDatabase.length > 0) { + console.log('\n⚠️ 数据库中多余的模板:'); + report.extraInDatabase.forEach(template => { + console.log(` • ${template}`); + }); + } + + if (report.missingInDatabase.length === 0 && report.extraInDatabase.length === 0) { + console.log('\n✅ 数据库与代码模板完全同步!'); + } + + console.log('='.repeat(50)); + } +} \ No newline at end of file diff --git a/src/templates/n8nTemplates/CharacterFigurineTemplate.ts b/src/templates/n8nTemplates/CharacterFigurineTemplate.ts index a4232b8..9281834 100644 --- a/src/templates/n8nTemplates/CharacterFigurineTemplate.ts +++ b/src/templates/n8nTemplates/CharacterFigurineTemplate.ts @@ -8,7 +8,7 @@ export class CharacterFigurineTemplate extends N8nVideoGenerateTemplate { readonly creditCost = 28; readonly version = '1.0.0'; readonly input = 'https://cdn.roasmax.cn/upload/3d590851eb584e92aa415a964e93260e.jpg'; // 原始人物照片示例 - readonly output = 'https://file.302.ai/gpt/imgs/20250828/2283106b31faf2066e1a72d955f65bca.jpg'; // 输出手办图片示例 + readonly output = 'https://file.302.ai/gpt/imgs/20250903/13256820c89e486790234e182c972905.mp4'; // 输出手办图片示例 readonly tags = ['人物', '手办', '模型', '收藏', 'PVC', '角色模型', '视频生成']; // N8n模板特定属性 - 图片生成 diff --git a/src/templates/n8nTemplates/CosplayRealPersonTemplate.ts b/src/templates/n8nTemplates/CosplayRealPersonTemplate.ts index c35b7da..6c12bdc 100644 --- a/src/templates/n8nTemplates/CosplayRealPersonTemplate.ts +++ b/src/templates/n8nTemplates/CosplayRealPersonTemplate.ts @@ -8,7 +8,7 @@ export class CosplayRealPersonTemplate extends N8nVideoGenerateTemplate { readonly creditCost = 25; readonly version = '1.0.0'; readonly input = 'https://cdn.roasmax.cn/upload/c69a847f8e8740f594f731ef502c4054.png'; // 原始插图示例 - readonly output = 'https://file.302.ai/gpt/imgs/20250903/376c2ee8e6bc06598b34cc43ce649aae.jpg'; // 输出图片示例 + readonly output = 'https://file.302.ai/gpt/imgs/20250903/a78d664707c04cf9815f5195b4bfcd18.mp4'; // 输出图片示例 readonly tags = ['cosplay', '真人', 'Comiket', '动漫', '二次元', '视频生成']; // N8n模板特定属性 - 图片生成 diff --git a/src/templates/n8nTemplates/GarageOpeningTemplate.ts b/src/templates/n8nTemplates/GarageOpeningTemplate.ts index 4342739..badcf30 100644 --- a/src/templates/n8nTemplates/GarageOpeningTemplate.ts +++ b/src/templates/n8nTemplates/GarageOpeningTemplate.ts @@ -8,7 +8,7 @@ export class GarageOpeningTemplate extends N8nVideoGenerateTemplate { readonly creditCost = 35; readonly version = '1.0.0'; readonly input = 'https://bowong-ai-shanghai.tos-cn-shanghai.volces.com/images/1756889137996_jujhz5.jpg'; // 原始人物图片示例 - readonly output = 'https://cdn.roasmax.cn/upload/a0a299ffe4b14e07b034e3a45db20f79.png'; // 输出图片示例 + readonly output = 'https://file.302.ai/gpt/imgs/20250903/c98adec564fb4cf89813ecd89afc13bd.mp4'; // 输出图片示例 readonly tags = ['车库', '豪宅', '豪车', 'Lamborghini', '时尚', '奢华', '视频生成']; // N8n模板特定属性 - 图片生成 diff --git a/src/templates/n8nTemplates/JapaneseMagazineTemplate.ts b/src/templates/n8nTemplates/JapaneseMagazineTemplate.ts index 76723e1..43b7b15 100644 --- a/src/templates/n8nTemplates/JapaneseMagazineTemplate.ts +++ b/src/templates/n8nTemplates/JapaneseMagazineTemplate.ts @@ -8,7 +8,7 @@ export class JapaneseMagazineTemplate extends N8nVideoGenerateTemplate { readonly creditCost = 32; readonly version = '1.0.0'; readonly input = 'https://cdn.roasmax.cn/upload/1980dcb0b0e3467794065c85ba607913.jpg'; // 原始人物图片示例 - readonly output = 'https://file.302.ai/gpt/imgs/20250903/ade8425169c84ab599be132be0b6a119.jpg'; // 输出杂志图片示例 + readonly output = 'https://file.302.ai/gpt/imgs/20250903/a13130f161814b36826672ff08f68876.mp4'; // 输出杂志图片示例 readonly tags = ['日本杂志', '时尚', '海报', '街头服饰', '拼贴', '日语文本', '视频生成']; // N8n模板特定属性 - 图片生成 diff --git a/src/templates/n8nTemplates/PetFigurineTemplate.ts b/src/templates/n8nTemplates/PetFigurineTemplate.ts index 6ea0524..8d4308e 100644 --- a/src/templates/n8nTemplates/PetFigurineTemplate.ts +++ b/src/templates/n8nTemplates/PetFigurineTemplate.ts @@ -8,7 +8,7 @@ export class PetFigurineTemplate extends N8nVideoGenerateTemplate { readonly creditCost = 30; readonly version = '1.0.0'; readonly input = 'https://cdn.roasmax.cn/upload/7cc18d70e9ba48bc8cadff7d3cdf62e1.png'; // 原始宠物插图示例 - readonly output = 'https://cdn.roasmax.cn/upload/02f5c251162043f28e9da8d984546f40.png'; // 输出手办图片示例 + readonly output = 'https://file.302.ai/gpt/imgs/20250903/92e2e8adf4754d1a8bf406418ab4d10d.mp4'; // 输出手办图片示例 readonly tags = ['宠物', '手办', '模型', '收藏', 'BANDAI', 'ZBrush', '视频生成']; // N8n模板特定属性 - 图片生成