Files
bw-mini-app-server/docs/template-hybrid-architecture.md
imeepos f83859b518 feat: 实现数据库配置和模板迁移系统
- 配置MySQL数据库连接和TypeORM集成
- 创建N8nTemplate实体和动态实例实体
- 实现混合架构:代码逻辑+数据库配置
- 添加模板工厂服务和迁移服务
- 创建RESTful API控制器用于模板管理
- 将所有n8nTemplates模板整理为独立migration文件
- 添加完整的数据库迁移指南和使用文档
- 更新app.module.ts集成新服务和配置
2025-09-04 15:20:33 +08:00

17 KiB
Raw Blame History

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)

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. 初始化数据示例

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. 模板配置实体

// 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. 动态模板实体类

// 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<N8nTemplateEntity>
  ) {
    super();
  }

  async onInit(): Promise<N8nImageGenerateTemplateEntity> {
    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<N8nTemplateEntity>
  ) {
    super();
  }

  async onInit(): Promise<N8nVideoGenerateTemplateEntity> {
    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. 模板工厂服务

// 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<N8nTemplateEntity>,
  ) {}

  // 创建图片模板实例
  async createImageTemplate(templateId: number): Promise<N8nImageGenerateTemplateEntity> {
    const instance = new N8nImageGenerateTemplateEntity(templateId, this.templateRepository);
    return await instance.onInit();
  }

  // 创建视频模板实例  
  async createVideoTemplate(templateId: number): Promise<N8nVideoGenerateTemplateEntity> {
    const instance = new N8nVideoGenerateTemplateEntity(templateId, this.templateRepository);
    return await instance.onInit();
  }

  // 通过模板代码创建实例
  async createTemplateByCode(code: string): Promise<N8nImageGenerateTemplateEntity | N8nVideoGenerateTemplateEntity> {
    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<N8nTemplateEntity[]> {
    return this.templateRepository.find({
      where: { isActive: true },
      order: { sortOrder: 'DESC', createdAt: 'DESC' }
    });
  }

  // 按类型获取模板
  async getTemplatesByType(type: TemplateType): Promise<N8nTemplateEntity[]> {
    return this.templateRepository.find({
      where: { templateType: type, isActive: true },
      order: { sortOrder: 'DESC', createdAt: 'DESC' }
    });
  }
}

🚀 使用示例

1. 您期望的使用方式

// 通过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中的使用

// 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. 数据迁移脚本

// 将现有代码中的模板配置迁移到数据库
// 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<N8nTemplateEntity>,
  ) {}

  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. 使用体验优化

// 简洁的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);

这种设计完美融合了您的面向对象架构优势和数据库配置管理的灵活性!