diff --git a/docs/multi-platform-integration-solution.md b/docs/multi-platform-integration-solution.md index 754da57..ecec5f5 100644 --- a/docs/multi-platform-integration-solution.md +++ b/docs/multi-platform-integration-solution.md @@ -198,10 +198,9 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateCol import { PlatformUser } from './platform-user.entity'; import { ExtensionData } from './extension-data.entity'; import { UserCredit } from './user-credit.entity'; -import { GenerationTask } from './generation-task.entity'; +import { TemplateExecutionEntity } from './template-execution.entity'; import { UserSubscription } from './user-subscription.entity'; import { AdWatch } from './ad-watch.entity'; -// import { TemplateExecution } from './template-execution.entity'; // 已移除,使用面向对象模板系统 @Entity('users') export class User { @@ -241,8 +240,8 @@ export class User { @OneToMany(() => UserCredit, userCredit => userCredit.user) credits: UserCredit[]; - @OneToMany(() => GenerationTask, generationTask => generationTask.user) - generationTasks: GenerationTask[]; + @OneToMany(() => TemplateExecutionEntity, execution => execution.userId) + templateExecutions: TemplateExecutionEntity[]; @OneToMany(() => UserSubscription, userSubscription => userSubscription.user) subscriptions: UserSubscription[]; @@ -250,7 +249,7 @@ export class User { @OneToMany(() => AdWatch, adWatch => adWatch.user) adWatches: AdWatch[]; - // 注意: 模板执行记录通过面向对象方式管理,不存储在数据库中 + // 注意: 现在使用 TemplateExecutionEntity 实体管理模板执行记录 } ``` @@ -675,37 +674,51 @@ export class AdWatch { } ``` -### 8. AI生成任务与模板系统集成说明 +### 8. 模板执行记录系统说明 -**重要说明**: 本项目采用面向对象的模板管理系统,不使用数据库存储模板定义和执行记录。 +**重要说明**: 本项目采用混合式模板管理系统,使用数据库存储模板配置和执行记录。 #### 8.1 模板系统架构 -- **模板定义**: 通过TypeScript类定义,编译时类型检查 -- **模板管理**: 通过TemplateManager内存管理,支持动态注册/卸载 -- **执行记录**: 通过GenerationTask表记录AI生成任务,模板信息作为metadata存储 +- **模板定义**: 通过 N8nTemplateEntity 存储模板配置 +- **模板管理**: 通过 N8nTemplateFactoryService 管理模板实例化 +- **执行记录**: 通过 TemplateExecutionEntity 表记录AI生成任务的完整过程 -#### 8.2 与GenerationTask的关系 +#### 8.2 TemplateExecutionEntity 实体结构 ```typescript -// GenerationTask实体中存储模板相关信息 -@Entity('generation_tasks') -export class GenerationTask { - // ... 其他字段 +// 现在使用专门的模板执行记录实体 +@Entity('template_executions') +export class TemplateExecutionEntity { + @PrimaryGeneratedColumn() + id: number; - @Column({ type: 'json', nullable: true }) - parameters: any; // 存储模板执行参数 + @Column({ name: 'template_id' }) + templateId: number; - @Column({ type: 'json', nullable: true }) - metadata: any; // 存储模板代码、版本等信息 + @Column({ name: 'user_id' }) + userId: string; + + @Column({ type: 'enum', enum: PlatformType }) + platform: PlatformType; + + @Column({ type: 'enum', enum: ExecutionType }) + type: ExecutionType; + + @Column({ type: 'text' }) + prompt: string; + + @Column({ name: 'output_url', length: 500, nullable: true }) + outputUrl: string; + + @Column({ type: 'enum', enum: ExecutionStatus, default: ExecutionStatus.PENDING }) + status: ExecutionStatus; + + @Column({ name: 'credit_cost', default: 0 }) + creditCost: number; + + @ManyToOne(() => N8nTemplateEntity) + @JoinColumn({ name: 'template_id' }) + template: N8nTemplateEntity; } - -// 使用示例 -const task = new GenerationTask(); -task.metadata = { - templateCode: 'outfit_change_v1', - templateName: '智能换装', - templateVersion: '1.0.0', - executionId: 'exec_123456789' -}; ``` ### 4. TypeORM配置 @@ -923,8 +936,72 @@ export class CreateInitialTables1703001000000 implements MigrationInterface { ) `); - // 注意: AI模板系统采用面向对象设计,不需要数据库表 - // 模板定义通过TypeScript类管理,执行记录通过GenerationTask表的metadata字段存储 + // 创建N8N模板配置表 + await queryRunner.query(` + CREATE TABLE "n8n_templates" ( + "id" bigint NOT NULL AUTO_INCREMENT, + "code" varchar(100) NOT NULL UNIQUE, + "name" varchar(200) NOT NULL, + "description" text, + "creditCost" int NOT NULL DEFAULT 0, + "version" varchar(50) NOT NULL, + "inputExampleUrl" text, + "outputExampleUrl" text, + "tags" json, + "templateType" enum('image', 'video') NOT NULL, + "templateClass" varchar(100) NOT NULL, + "imageModel" varchar(100), + "imagePrompt" text, + "videoModel" varchar(100), + "videoPrompt" text, + "duration" int, + "aspectRatio" varchar(20), + "isActive" boolean DEFAULT TRUE, + "sortOrder" int DEFAULT 0, + "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT "PK_n8n_templates_id" PRIMARY KEY ("id") + ) + `); + + // 创建模板执行记录表 + await queryRunner.query(` + CREATE TYPE "execution_status" AS ENUM('pending', 'processing', 'completed', 'failed', 'cancelled') + `); + + await queryRunner.query(` + CREATE TYPE "execution_type" AS ENUM('image', 'video') + `); + + await queryRunner.query(` + CREATE TABLE "template_executions" ( + "id" bigint NOT NULL AUTO_INCREMENT, + "templateId" bigint NOT NULL, + "userId" uuid NOT NULL, + "platform" "platform_type" NOT NULL, + "type" "execution_type" NOT NULL, + "prompt" text NOT NULL, + "inputImageUrl" varchar(500), + "outputUrl" varchar(500), + "thumbnailUrl" varchar(500), + "status" "execution_status" DEFAULT 'pending', + "progress" int DEFAULT 0, + "errorMessage" text, + "executionResult" json, + "creditCost" int DEFAULT 0, + "creditTransactionId" varchar(100), + "inputParams" json, + "executionConfig" json, + "startedAt" TIMESTAMP NULL, + "completedAt" TIMESTAMP NULL, + "executionDuration" int, + "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT "PK_template_executions_id" PRIMARY KEY ("id"), + CONSTRAINT "FK_template_executions_templateId" FOREIGN KEY ("templateId") REFERENCES "n8n_templates"("id") ON DELETE CASCADE, + CONSTRAINT "FK_template_executions_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE + ) + `); // 创建索引 await queryRunner.query(`CREATE INDEX "IDX_platform_users_platform" ON "platform_users" ("platform")`); @@ -963,23 +1040,36 @@ export class CreateInitialTables1703001000000 implements MigrationInterface { await queryRunner.query(`CREATE INDEX "IDX_ad_watches_status" ON "ad_watches" ("status")`); await queryRunner.query(`CREATE INDEX "IDX_ad_watches_createdAt" ON "ad_watches" ("createdAt")`); - // 注意: 模板系统索引已移除,使用面向对象管理 + // N8n模板表索引 + await queryRunner.query(`CREATE INDEX "IDX_n8n_templates_code" ON "n8n_templates" ("code")`); + await queryRunner.query(`CREATE INDEX "IDX_n8n_templates_templateType" ON "n8n_templates" ("templateType")`); + await queryRunner.query(`CREATE INDEX "IDX_n8n_templates_templateClass" ON "n8n_templates" ("templateClass")`); + await queryRunner.query(`CREATE INDEX "IDX_n8n_templates_isActive" ON "n8n_templates" ("isActive")`); + + // 模板执行记录表索引 + await queryRunner.query(`CREATE INDEX "IDX_template_executions_templateId" ON "template_executions" ("templateId")`); + await queryRunner.query(`CREATE INDEX "IDX_template_executions_userId" ON "template_executions" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_template_executions_platform" ON "template_executions" ("platform")`); + await queryRunner.query(`CREATE INDEX "IDX_template_executions_status" ON "template_executions" ("status")`); + await queryRunner.query(`CREATE INDEX "IDX_template_executions_type" ON "template_executions" ("type")`); + await queryRunner.query(`CREATE INDEX "IDX_template_executions_creditTransactionId" ON "template_executions" ("creditTransactionId")`); } public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "template_executions"`); + await queryRunner.query(`DROP TABLE "n8n_templates"`); await queryRunner.query(`DROP TABLE "ad_watches"`); await queryRunner.query(`DROP TABLE "user_subscriptions"`); - await queryRunner.query(`DROP TABLE "generation_tasks"`); await queryRunner.query(`DROP TABLE "user_credits"`); await queryRunner.query(`DROP TABLE "extension_data"`); await queryRunner.query(`DROP TABLE "platform_users"`); await queryRunner.query(`DROP TABLE "users"`); + await queryRunner.query(`DROP TYPE "execution_type"`); + await queryRunner.query(`DROP TYPE "execution_status"`); await queryRunner.query(`DROP TYPE "ad_status"`); await queryRunner.query(`DROP TYPE "ad_type"`); await queryRunner.query(`DROP TYPE "subscription_status"`); await queryRunner.query(`DROP TYPE "subscription_plan"`); - await queryRunner.query(`DROP TYPE "task_status"`); - await queryRunner.query(`DROP TYPE "generation_type"`); await queryRunner.query(`DROP TYPE "credit_source"`); await queryRunner.query(`DROP TYPE "credit_type"`); await queryRunner.query(`DROP TYPE "platform_type"`); diff --git a/docs/template-hybrid-architecture.md b/docs/template-hybrid-architecture.md index 46edc07..15a01be 100644 --- a/docs/template-hybrid-architecture.md +++ b/docs/template-hybrid-architecture.md @@ -201,8 +201,8 @@ import { N8nTemplateEntity, TemplateType } from './n8n-template.entity'; import { N8nImageGenerateTemplate, N8nVideoGenerateTemplate } from '../templates/n8nTemplate'; import axios from 'axios'; -// 动态图片模板实体 -export class N8nImageGenerateTemplateEntity extends N8nImageGenerateTemplate { +// 动态图片模板实现 +export class DynamicN8nImageTemplate extends N8nImageGenerateTemplate { private config: N8nTemplateEntity; constructor( @@ -212,7 +212,7 @@ export class N8nImageGenerateTemplateEntity extends N8nImageGenerateTemplate { super(); } - async onInit(): Promise { + async onInit(): Promise { this.config = await this.templateRepository.findOne({ where: { id: this.templateId, templateType: TemplateType.IMAGE, isActive: true } }); @@ -239,8 +239,8 @@ export class N8nImageGenerateTemplateEntity extends N8nImageGenerateTemplate { // 执行方法继承自N8nImageGenerateTemplate,无需修改 } -// 动态视频模板实体 -export class N8nVideoGenerateTemplateEntity extends N8nVideoGenerateTemplate { +// 动态视频模板实现 +export class DynamicN8nVideoTemplate extends N8nVideoGenerateTemplate { private config: N8nTemplateEntity; constructor( @@ -250,7 +250,7 @@ export class N8nVideoGenerateTemplateEntity extends N8nVideoGenerateTemplate { super(); } - async onInit(): Promise { + async onInit(): Promise { this.config = await this.templateRepository.findOne({ where: { id: this.templateId, templateType: TemplateType.VIDEO, isActive: true } }); @@ -282,36 +282,194 @@ export class N8nVideoGenerateTemplateEntity extends N8nVideoGenerateTemplate { } ``` -### 3. 模板工厂服务 +### 3. 模板执行记录表 (template_executions) +```sql +CREATE TABLE template_executions ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + + -- 关联字段 + template_id BIGINT NOT NULL COMMENT '模板ID,关联n8n_templates.id', + user_id VARCHAR(100) NOT NULL COMMENT '用户ID,关联users.id', + platform ENUM('wechat', 'alipay', 'baidu', 'bytedance', 'jd', 'qq', 'feishu', 'kuaishou', 'h5', 'rn') NOT NULL COMMENT '执行平台', + + -- 执行内容 + type ENUM('image', 'video') NOT NULL COMMENT '生成类型', + prompt TEXT NOT NULL COMMENT '用户输入提示词', + input_image_url VARCHAR(500) COMMENT '输入图片URL', + output_url VARCHAR(500) COMMENT '生成结果URL', + thumbnail_url VARCHAR(500) COMMENT '缩略图URL', + + -- 执行状态和结果 + status ENUM('pending', 'processing', 'completed', 'failed', 'cancelled') NOT NULL DEFAULT 'pending' COMMENT '执行状态', + progress INT DEFAULT 0 COMMENT '执行进度(0-100)', + error_message TEXT COMMENT '错误信息', + execution_result JSON COMMENT '执行结果详情', + + -- 积分和计费 + credit_cost INT NOT NULL DEFAULT 0 COMMENT '消耗积分数', + credit_transaction_id VARCHAR(100) COMMENT '积分交易ID,关联user_credits.id', + + -- 执行参数和配置 + input_params JSON COMMENT '输入参数', + execution_config JSON COMMENT '执行时的配置快照', + + -- 性能指标 + started_at TIMESTAMP NULL COMMENT '开始执行时间', + completed_at TIMESTAMP NULL COMMENT '完成时间', + execution_duration INT COMMENT '执行耗时(秒)', + + -- 系统字段 + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + KEY idx_template_id (template_id), + KEY idx_user_id (user_id), + KEY idx_platform (platform), + KEY idx_status (status), + KEY idx_type (type), + KEY idx_created_at (created_at), + KEY idx_credit_transaction_id (credit_transaction_id), + + FOREIGN KEY (template_id) REFERENCES n8n_templates(id) ON DELETE CASCADE +) COMMENT '模板执行记录表'; +``` + +### 4. 模板执行记录实体 +```typescript +// src/entities/template-execution.entity.ts +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { N8nTemplateEntity } from './n8n-template.entity'; +import { PlatformType } from './platform-user.entity'; + +export enum ExecutionStatus { + PENDING = 'pending', + PROCESSING = 'processing', + COMPLETED = 'completed', + FAILED = 'failed', + CANCELLED = 'cancelled' +} + +export enum ExecutionType { + IMAGE = 'image', + VIDEO = 'video' +} + +@Entity('template_executions') +@Index(['templateId']) +@Index(['userId']) +@Index(['platform']) +@Index(['status']) +@Index(['type']) +@Index(['creditTransactionId']) +export class TemplateExecutionEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ name: 'template_id' }) + templateId: number; + + @Column({ name: 'user_id' }) + userId: string; + + @Column({ type: 'enum', enum: PlatformType }) + platform: PlatformType; + + @Column({ type: 'enum', enum: ExecutionType }) + type: ExecutionType; + + @Column({ type: 'text' }) + prompt: string; + + @Column({ name: 'input_image_url', length: 500, nullable: true }) + inputImageUrl: string; + + @Column({ name: 'output_url', length: 500, nullable: true }) + outputUrl: string; + + @Column({ name: 'thumbnail_url', length: 500, nullable: true }) + thumbnailUrl: string; + + @Column({ type: 'enum', enum: ExecutionStatus, default: ExecutionStatus.PENDING }) + status: ExecutionStatus; + + @Column({ default: 0 }) + progress: number; + + @Column({ name: 'error_message', type: 'text', nullable: true }) + errorMessage: string; + + @Column({ name: 'execution_result', type: 'json', nullable: true }) + executionResult: any; + + // 积分和计费相关 + @Column({ name: 'credit_cost', default: 0 }) + creditCost: number; + + @Column({ name: 'credit_transaction_id', length: 100, nullable: true }) + creditTransactionId: string; + + // 执行参数和配置 + @Column({ name: 'input_params', type: 'json', nullable: true }) + inputParams: any; + + @Column({ name: 'execution_config', type: 'json', nullable: true }) + executionConfig: any; + + // 性能指标 + @Column({ name: 'started_at', nullable: true }) + startedAt: Date; + + @Column({ name: 'completed_at', nullable: true }) + completedAt: Date; + + @Column({ name: 'execution_duration', nullable: true }) + executionDuration: number; + + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; + + @ManyToOne(() => N8nTemplateEntity) + @JoinColumn({ name: 'template_id' }) + template: N8nTemplateEntity; +} +``` + +### 5. 模板工厂服务 ```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'; +import { TemplateExecutionEntity, ExecutionStatus } from '../entities/template-execution.entity'; +import { DynamicN8nImageTemplate, DynamicN8nVideoTemplate } from '../templates/n8n-dynamic-template'; @Injectable() export class N8nTemplateFactoryService { constructor( @InjectRepository(N8nTemplateEntity) private templateRepository: Repository, + @InjectRepository(TemplateExecutionEntity) + private executionRepository: Repository, ) {} // 创建图片模板实例 - async createImageTemplate(templateId: number): Promise { - const instance = new N8nImageGenerateTemplateEntity(templateId, this.templateRepository); + async createImageTemplate(templateId: number): Promise { + const instance = new DynamicN8nImageTemplate(templateId, this.templateRepository); return await instance.onInit(); } // 创建视频模板实例 - async createVideoTemplate(templateId: number): Promise { - const instance = new N8nVideoGenerateTemplateEntity(templateId, this.templateRepository); + async createVideoTemplate(templateId: number): Promise { + const instance = new DynamicN8nVideoTemplate(templateId, this.templateRepository); return await instance.onInit(); } // 通过模板代码创建实例 - async createTemplateByCode(code: string): Promise { + async createTemplateByCode(code: string): Promise { const config = await this.templateRepository.findOne({ where: { code, isActive: true } }); @@ -327,6 +485,42 @@ export class N8nTemplateFactoryService { } } + // 创建执行记录 + async createExecution(data: Partial): Promise { + const execution = this.executionRepository.create(data); + return this.executionRepository.save(execution); + } + + // 更新执行记录 + async updateExecution(id: number, data: Partial): Promise { + await this.executionRepository.update(id, data); + return this.executionRepository.findOne({ where: { id } }); + } + + // 获取用户执行记录 + async getUserExecutions(userId: string, limit = 20): Promise { + return this.executionRepository.find({ + where: { userId }, + relations: ['template'], + order: { createdAt: 'DESC' }, + take: limit + }); + } + + // 获取模板执行统计 + async getTemplateExecutionStats(templateId: number): Promise { + const stats = await this.executionRepository + .createQueryBuilder('execution') + .select('execution.status', 'status') + .addSelect('COUNT(*)', 'count') + .addSelect('AVG(execution.executionDuration)', 'avgDuration') + .where('execution.templateId = :templateId', { templateId }) + .groupBy('execution.status') + .getRawMany(); + + return stats; + } + // 获取所有可用模板 async getAllTemplates(): Promise { return this.templateRepository.find({ @@ -347,22 +541,9 @@ export class N8nTemplateFactoryService { ## 🚀 使用示例 -### 1. 您期望的使用方式 +### 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中使用,自动记录执行过程 @Controller('templates') export class TemplateController { constructor( @@ -372,11 +553,69 @@ export class TemplateController { @Post(':templateId/execute') async executeTemplate( @Param('templateId', ParseIntPipe) templateId: number, - @Body() { imageUrl }: { imageUrl: string } + @Body() { imageUrl, taskId }: { imageUrl: string, taskId: string }, + @CurrentUser() user: any ) { - // 通过工厂服务创建模板实例并执行 - const template = await this.templateFactory.createTemplate(templateId); - return await template.execute(imageUrl); + // 1. 创建执行记录 + const execution = await this.templateFactory.createExecution({ + taskId, + templateId, + userId: user.id, + status: ExecutionStatus.PENDING, + inputParams: { imageUrl }, + progress: 0 + }); + + try { + // 2. 更新状态为处理中 + await this.templateFactory.updateExecution(execution.id, { + status: ExecutionStatus.PROCESSING, + startedAt: new Date(), + progress: 10 + }); + + // 3. 创建模板实例并执行 + const template = await this.templateFactory.createTemplateByCode(templateId); + + // 4. 更新进度 + await this.templateFactory.updateExecution(execution.id, { progress: 50 }); + + // 5. 执行模板 + const result = await template.execute(imageUrl); + + // 6. 记录执行完成 + await this.templateFactory.updateExecution(execution.id, { + status: ExecutionStatus.COMPLETED, + completedAt: new Date(), + progress: 100, + executionResult: result, + executionDuration: Math.floor((Date.now() - execution.startedAt.getTime()) / 1000) + }); + + return { success: true, data: result, executionId: execution.id }; + + } catch (error) { + // 7. 记录执行失败 + await this.templateFactory.updateExecution(execution.id, { + status: ExecutionStatus.FAILED, + completedAt: new Date(), + errorMessage: error.message, + executionDuration: execution.startedAt ? + Math.floor((Date.now() - execution.startedAt.getTime()) / 1000) : 0 + }); + + throw error; + } + } + + @Get('executions/:userId') + async getUserExecutions(@Param('userId') userId: string) { + return this.templateFactory.getUserExecutions(userId); + } + + @Get(':templateId/stats') + async getTemplateStats(@Param('templateId', ParseIntPipe) templateId: number) { + return this.templateFactory.getTemplateExecutionStats(templateId); } @Get() @@ -386,16 +625,36 @@ export class TemplateController { @Get('image') async getImageTemplates() { - return this.templateFactory.getImageTemplates(); + return this.templateFactory.getTemplatesByType(TemplateType.IMAGE); } @Get('video') async getVideoTemplates() { - return this.templateFactory.getVideoTemplates(); + return this.templateFactory.getTemplatesByType(TemplateType.VIDEO); } } ``` +### 2. 执行记录查询示例 +```typescript +// 查询用户最近的执行记录 +const userExecutions = await templateFactory.getUserExecutions('user123', 10); + +// 查询特定模板的执行统计 +const stats = await templateFactory.getTemplateExecutionStats(1); +// 返回格式: [ +// { status: 'completed', count: '85', avgDuration: '12.5' }, +// { status: 'failed', count: '5', avgDuration: '8.2' }, +// { status: 'processing', count: '2', avgDuration: null } +// ] + +// 根据执行ID查询详细信息 +const execution = await executionRepository.findOne({ + where: { id: executionId }, + relations: ['template'] +}); +``` + ## ✅ 架构优势 @@ -409,17 +668,44 @@ export class TemplateController { - **运营友好**:通过管理后台可以调整模板参数、启用/禁用模板 - **A/B测试**:可以创建同一功能的不同版本进行测试 - **统计分析**:可以跟踪每个模板的使用情况和效果 +- **执行跟踪**:完整记录每次模板执行的过程、结果和性能指标 +- **故障诊断**:详细的错误记录和执行时长,便于问题排查 ### 3. 使用体验优化 ```typescript -// 简洁的API调用 -const result = await new N8nVideoGenerateTemplateEntity(id, repo) - .onInit() - .then(e => e.execute(imgUrl)); +// 带执行记录的模板调用 +const execution = await templateFactory.createExecution({ + taskId: 'task_123', + templateId: 1, + userId: 'user_456', + inputParams: { imageUrl: 'https://...' } +}); -// 工厂模式调用 const template = await templateFactory.createTemplateByCode('character_figurine_v1'); const result = await template.execute(imageUrl); + +// 查询执行历史 +const history = await templateFactory.getUserExecutions('user_456'); + +// 查看模板统计 +const stats = await templateFactory.getTemplateExecutionStats(1); ``` +## 📈 执行记录的业务价值 + +### 1. 运营数据支持 +- **使用频率分析**:哪些模板最受欢迎 +- **成功率监控**:模板执行的成功率和失败原因 +- **性能监控**:平均执行时长和性能瓶颈 + +### 2. 用户体验优化 +- **执行历史查看**:用户可以查看自己的执行记录 +- **进度显示**:实时显示模板执行进度 +- **错误反馈**:清晰的错误信息和重试建议 + +### 3. 系统稳定性 +- **故障排查**:详细的执行日志便于问题定位 +- **性能优化**:基于执行数据优化模板参数 +- **容量规划**:基于使用数据进行系统容量规划 + 这种设计完美融合了您的面向对象架构优势和数据库配置管理的灵活性! \ No newline at end of file diff --git a/src/controllers/template.controller.ts b/src/controllers/template.controller.ts index 6e716a7..c459f25 100644 --- a/src/controllers/template.controller.ts +++ b/src/controllers/template.controller.ts @@ -2,6 +2,8 @@ import { Controller, Get, Post, + Put, + Delete, Param, Body, ParseIntPipe, @@ -10,6 +12,10 @@ import { HttpStatus } from '@nestjs/common'; import { N8nTemplateFactoryService } from '../services/n8n-template-factory.service'; +import { N8nTemplateEntity, TemplateType } from '../entities/n8n-template.entity'; +import { CreateTemplateDto, UpdateTemplateDto } from '../dto/template.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; /** * 模板控制器 @@ -18,7 +24,9 @@ import { N8nTemplateFactoryService } from '../services/n8n-template-factory.serv @Controller('templates') export class TemplateController { constructor( - private readonly templateFactory: N8nTemplateFactoryService + private readonly templateFactory: N8nTemplateFactoryService, + @InjectRepository(N8nTemplateEntity) + private readonly templateRepository: Repository ) {} /** @@ -279,4 +287,206 @@ export class TemplateController { } } + /** + * 创建新模板 + * @param createTemplateDto 创建模板的数据传输对象 + * @returns 创建的模板信息 + */ + @Post() + async createTemplate(@Body() createTemplateDto: CreateTemplateDto) { + try { + const existingTemplate = await this.templateRepository.findOne({ + where: { code: createTemplateDto.code } + }); + + if (existingTemplate) { + throw new HttpException( + `Template with code '${createTemplateDto.code}' already exists`, + HttpStatus.CONFLICT + ); + } + + const template = this.templateRepository.create(createTemplateDto); + const savedTemplate = await this.templateRepository.save(template); + + return { + success: true, + data: savedTemplate + }; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + error.message || 'Failed to create template', + HttpStatus.BAD_REQUEST + ); + } + } + + /** + * 更新模板 + * @param templateId 模板ID + * @param updateTemplateDto 更新模板的数据传输对象 + * @returns 更新后的模板信息 + */ + @Put(':templateId') + async updateTemplate( + @Param('templateId', ParseIntPipe) templateId: number, + @Body() updateTemplateDto: UpdateTemplateDto + ) { + try { + const existingTemplate = await this.templateRepository.findOne({ + where: { id: templateId } + }); + + if (!existingTemplate) { + throw new HttpException('Template not found', HttpStatus.NOT_FOUND); + } + + if (updateTemplateDto.code && updateTemplateDto.code !== existingTemplate.code) { + const codeConflict = await this.templateRepository.findOne({ + where: { code: updateTemplateDto.code } + }); + + if (codeConflict) { + throw new HttpException( + `Template with code '${updateTemplateDto.code}' already exists`, + HttpStatus.CONFLICT + ); + } + } + + await this.templateRepository.update(templateId, updateTemplateDto); + const updatedTemplate = await this.templateRepository.findOne({ + where: { id: templateId } + }); + + return { + success: true, + data: updatedTemplate + }; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + error.message || 'Failed to update template', + HttpStatus.BAD_REQUEST + ); + } + } + + /** + * 删除模板 + * @param templateId 模板ID + * @returns 删除结果 + */ + @Delete(':templateId') + async deleteTemplate(@Param('templateId', ParseIntPipe) templateId: number) { + try { + const template = await this.templateRepository.findOne({ + where: { id: templateId } + }); + + if (!template) { + throw new HttpException('Template not found', HttpStatus.NOT_FOUND); + } + + await this.templateRepository.delete(templateId); + + return { + success: true, + message: 'Template deleted successfully' + }; + } catch (error) { + throw new HttpException( + error.message || 'Failed to delete template', + HttpStatus.BAD_REQUEST + ); + } + } + + /** + * 获取模板详情(管理后台专用) + * @param templateId 模板ID + * @returns 模板完整详情(包含非激活状态的模板) + */ + @Get('admin/:templateId') + async getTemplateForAdmin(@Param('templateId', ParseIntPipe) templateId: number) { + try { + const template = await this.templateRepository.findOne({ + where: { id: templateId } + }); + + if (!template) { + throw new HttpException('Template not found', HttpStatus.NOT_FOUND); + } + + return { + success: true, + data: template + }; + } catch (error) { + throw new HttpException( + error.message || 'Template not found', + HttpStatus.NOT_FOUND + ); + } + } + + /** + * 获取所有模板列表(管理后台专用) + * @param page 页码 + * @param limit 每页数量 + * @param type 模板类型筛选 + * @param isActive 激活状态筛选 + * @returns 分页的模板列表(包含非激活状态的模板) + */ + @Get('admin') + async getAllTemplatesForAdmin( + @Query('page') page: number = 1, + @Query('limit') limit: number = 10, + @Query('type') type?: TemplateType, + @Query('isActive') isActive?: boolean + ) { + try { + const queryBuilder = this.templateRepository.createQueryBuilder('template'); + + if (type) { + queryBuilder.andWhere('template.templateType = :type', { type }); + } + + if (isActive !== undefined) { + queryBuilder.andWhere('template.isActive = :isActive', { isActive }); + } + + queryBuilder + .orderBy('template.sortOrder', 'DESC') + .addOrderBy('template.createdAt', 'DESC') + .skip((page - 1) * limit) + .take(limit); + + const [templates, total] = await queryBuilder.getManyAndCount(); + + return { + success: true, + data: { + templates, + pagination: { + page, + limit, + total, + totalPages: Math.ceil(total / limit) + } + } + }; + } catch (error) { + throw new HttpException( + error.message || 'Failed to fetch templates', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + } \ No newline at end of file diff --git a/src/dto/template.dto.ts b/src/dto/template.dto.ts new file mode 100644 index 0000000..d8ee538 --- /dev/null +++ b/src/dto/template.dto.ts @@ -0,0 +1,149 @@ +import { IsString, IsNumber, IsEnum, IsBoolean, IsOptional, IsArray, Min, Max } from 'class-validator'; +import { TemplateType } from '../entities/n8n-template.entity'; + +export class CreateTemplateDto { + @IsString() + code: string; + + @IsString() + name: string; + + @IsString() + description: string; + + @IsNumber() + @Min(0) + creditCost: number; + + @IsString() + version: string; + + @IsString() + @IsOptional() + inputExampleUrl?: string; + + @IsString() + @IsOptional() + outputExampleUrl?: string; + + @IsArray() + @IsOptional() + tags?: string[]; + + @IsEnum(TemplateType) + templateType: TemplateType; + + @IsString() + templateClass: string; + + @IsString() + @IsOptional() + imageModel?: string; + + @IsString() + @IsOptional() + imagePrompt?: string; + + @IsString() + @IsOptional() + videoModel?: string; + + @IsString() + @IsOptional() + videoPrompt?: string; + + @IsNumber() + @IsOptional() + @Min(1) + @Max(300) + duration?: number; + + @IsString() + @IsOptional() + aspectRatio?: string; + + @IsBoolean() + @IsOptional() + isActive?: boolean; + + @IsNumber() + @IsOptional() + sortOrder?: number; +} + +export class UpdateTemplateDto { + @IsString() + @IsOptional() + code?: string; + + @IsString() + @IsOptional() + name?: string; + + @IsString() + @IsOptional() + description?: string; + + @IsNumber() + @Min(0) + @IsOptional() + creditCost?: number; + + @IsString() + @IsOptional() + version?: string; + + @IsString() + @IsOptional() + inputExampleUrl?: string; + + @IsString() + @IsOptional() + outputExampleUrl?: string; + + @IsArray() + @IsOptional() + tags?: string[]; + + @IsEnum(TemplateType) + @IsOptional() + templateType?: TemplateType; + + @IsString() + @IsOptional() + templateClass?: string; + + @IsString() + @IsOptional() + imageModel?: string; + + @IsString() + @IsOptional() + imagePrompt?: string; + + @IsString() + @IsOptional() + videoModel?: string; + + @IsString() + @IsOptional() + videoPrompt?: string; + + @IsNumber() + @IsOptional() + @Min(1) + @Max(300) + duration?: number; + + @IsString() + @IsOptional() + aspectRatio?: string; + + @IsBoolean() + @IsOptional() + isActive?: boolean; + + @IsNumber() + @IsOptional() + sortOrder?: number; +} \ No newline at end of file diff --git a/src/entities/ad-watch.entity.ts b/src/entities/ad-watch.entity.ts new file mode 100644 index 0000000..3e4758c --- /dev/null +++ b/src/entities/ad-watch.entity.ts @@ -0,0 +1,95 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { User } from './user.entity'; +import { PlatformType } from './platform-user.entity'; + +/** + * 广告类型枚举 + * 定义了小程序支持的各种广告形式 + */ +export enum AdType { + BANNER = 'banner', // 横幅广告 - 显示在页面顶部或底部的条状广告 + INTERSTITIAL = 'interstitial', // 插屏广告 - 在应用界面切换时显示的全屏广告 + REWARDED = 'rewarded', // 激励视频广告 - 用户观看完整视频后给予奖励的广告 + NATIVE = 'native' // 原生广告 - 与应用内容自然融合的广告形式 +} + +/** + * 广告观看状态枚举 + * 记录用户与广告交互的不同阶段 + */ +export enum AdStatus { + STARTED = 'started', // 开始观看 - 用户点击观看广告 + COMPLETED = 'completed', // 观看完成 - 用户完整观看广告内容 + SKIPPED = 'skipped', // 跳过 - 用户在允许时间内跳过广告 + FAILED = 'failed' // 失败 - 广告加载失败或播放出错 +} + +/** + * 广告观看记录实体类 + * 记录用户在不同平台上观看广告的完整行为数据 + * 用于积分奖励、数据统计和用户行为分析 + */ +@Entity('ad_watches') +export class AdWatch { + /** 主键 - 广告观看记录的唯一标识符 */ + @PrimaryGeneratedColumn('uuid') + id: string; + + /** 用户ID - 关联到用户实体 */ + @Column() + userId: string; + + /** 平台类型 - 标识广告观看发生的平台(微信、支付宝等) */ + @Column({ + type: 'enum', + enum: PlatformType + }) + platform: PlatformType; + + /** 广告类型 - 标识广告的展现形式(横幅、插屏、激励视频、原生) */ + @Column({ + type: 'enum', + enum: AdType + }) + adType: AdType; + + /** 广告ID - 广告平台提供的广告唯一标识 */ + @Column({ length: 100 }) + adId: string; + + /** 广告位ID - 广告在应用中的位置标识,用于区分不同广告位 */ + @Column({ length: 100, nullable: true }) + adUnitId: string; + + /** 观看状态 - 记录用户观看广告的结果状态 */ + @Column({ + type: 'enum', + enum: AdStatus + }) + status: AdStatus; + + /** 观看时长 - 用户实际观看广告的秒数,用于计算奖励 */ + @Column({ type: 'integer', nullable: true }) + watchDuration: number; + + /** 奖励积分 - 用户观看广告获得的积分数量 */ + @Column({ type: 'integer', nullable: true }) + rewardCredits: number; + + /** 广告相关数据 - 存储广告的详细信息(创意内容、点击率等) */ + @Column({ type: 'json', nullable: true }) + adData: any; + + /** 创建时间 - 广告观看记录的创建时间戳 */ + @CreateDateColumn() + createdAt: Date; + + /** 更新时间 - 记录最后修改时间,用于数据追踪 */ + @UpdateDateColumn() + updatedAt: Date; + + /** 用户关系 - 与用户实体的多对一关系,级联删除 */ + @ManyToOne(() => User, user => user.adWatches, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) + user: User; +} \ No newline at end of file diff --git a/src/entities/extension-data.entity.ts b/src/entities/extension-data.entity.ts new file mode 100644 index 0000000..6849813 --- /dev/null +++ b/src/entities/extension-data.entity.ts @@ -0,0 +1,59 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { User } from './user.entity'; +import { PlatformType } from './platform-user.entity'; + +/** + * 扩展数据实体类 + * 用于存储各平台特有的业务数据和第三方集成数据 + * 提供灵活的JSON存储机制,支持动态数据结构 + */ +@Entity('extension_data') +export class ExtensionData { + /** 主键 - 扩展数据记录的唯一标识符 */ + @PrimaryGeneratedColumn('uuid') + id: string; + + /** 用户ID - 关联到用户实体 */ + @Column() + userId: string; + + /** 平台类型 - 标识数据来源平台(微信、支付宝等) */ + @Column({ + type: 'enum', + enum: PlatformType + }) + platform: PlatformType; + + /** 数据类型 - 标识扩展数据的业务类型('order'订单, 'payment'支付, 'push'推送, 'custom'自定义等) */ + @Column({ length: 50 }) + dataType: string; + + /** 外部引用ID - 关联第三方系统的业务ID,用于数据同步和追踪 */ + @Column({ length: 100, nullable: true }) + referenceId: string; + + /** 核心数据 - 灵活的JSON数据存储,根据dataType存储不同结构的业务数据 */ + @Column({ type: 'json' }) + data: any; + + /** 元数据 - 存储数据的附加信息,如来源、版本、处理状态等 */ + @Column({ type: 'json', nullable: true }) + metadata: any; + + /** 数据状态 - 标识数据的当前状态(active活跃, archived归档, deleted已删除等) */ + @Column({ length: 20, default: 'active' }) + status: string; + + /** 创建时间 - 扩展数据的创建时间戳 */ + @CreateDateColumn() + createdAt: Date; + + /** 更新时间 - 记录最后修改时间,用于数据同步和版本控制 */ + @UpdateDateColumn() + updatedAt: Date; + + /** 用户关系 - 与用户实体的多对一关系,级联删除 */ + @ManyToOne(() => User, user => user.extensionData, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) + user: User; +} \ No newline at end of file diff --git a/src/entities/n8n-template.entity.ts b/src/entities/n8n-template.entity.ts index 4a78f07..e3cb5d1 100644 --- a/src/entities/n8n-template.entity.ts +++ b/src/entities/n8n-template.entity.ts @@ -7,44 +7,64 @@ import { Index } from 'typeorm'; +/** + * 模板类型枚举 + * 定义N8N模板支持的输出内容类型 + */ export enum TemplateType { - IMAGE = 'image', - VIDEO = 'video' + IMAGE = 'image', // 图片模板 - 生成图片内容 + VIDEO = 'video' // 视频模板 - 生成视频内容 } +/** + * N8N模板实体类 + * 存储AI内容生成模板的完整配置信息 + * 支持图片和视频两种类型的AI生成任务 + * 包含模型配置、提示词、积分消耗等关键信息 + */ @Entity('n8n_templates') -@Index('idx_code', ['code']) -@Index('idx_type', ['templateType']) -@Index('idx_class', ['templateClass']) -@Index('idx_active', ['isActive']) +@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; + /** 模板代码 - 模板的唯一标识码,用于API调用和模板引用 */ @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; + /** 输入示例图片 - 展示给用户的输入样例图片URL,帮助用户理解模板用法 */ @Column({ name: 'input_example_url', type: 'text', nullable: true, comment: '输入示例图片' }) inputExampleUrl: string; + /** 输出示例图片 - 展示给用户的输出效果图片URL,演示模板生成效果 */ @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', @@ -53,36 +73,47 @@ export class N8nTemplateEntity { }) templateType: TemplateType; + /** 模板实现类 - 对应的N8N工作流类名,用于代码层面的模板实例化 */ @Column({ name: 'template_class', length: 100, comment: '具体模板类名' }) templateClass: string; + /** 图片生成模型 - 用于图片生成的AI模型名称(如DALL-E、Midjourney等) */ @Column({ name: 'image_model', length: 100, nullable: true, comment: '图片生成模型' }) imageModel: string; + /** 图片生成提示词 - 传递给图片AI模型的提示词模板,支持变量替换 */ @Column({ name: 'image_prompt', type: 'text', nullable: true, comment: '图片生成提示词' }) imagePrompt: string; + /** 视频生成模型 - 用于视频生成的AI模型名称(如RunwayML、Stable Video等) */ @Column({ name: 'video_model', length: 100, nullable: true, comment: '视频生成模型' }) videoModel: string; + /** 视频生成提示词 - 传递给视频AI模型的提示词模板,支持变量替换 */ @Column({ name: 'video_prompt', type: 'text', nullable: true, comment: '视频生成提示词' }) videoPrompt: string; + /** 视频时长 - 生成视频的持续时间(秒),用于控制生成视频的长度 */ @Column({ nullable: true, comment: '视频时长(秒)' }) duration: number; + /** 视频宽高比 - 视频的纵横比例(如16:9、9:16、1:1),影响视频尺寸 */ @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/entities/platform-user.entity.ts b/src/entities/platform-user.entity.ts new file mode 100644 index 0000000..905a4ba --- /dev/null +++ b/src/entities/platform-user.entity.ts @@ -0,0 +1,78 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { User } from './user.entity'; + +/** + * 平台类型枚举 + * 定义了应用支持的所有小程序平台和移动端平台 + * 用于多平台用户身份管理和数据隔离 + */ +export enum PlatformType { + WECHAT = 'wechat', // 微信小程序 - 腾讯微信生态 + ALIPAY = 'alipay', // 支付宝小程序 - 蚂蚁金服生态 + BAIDU = 'baidu', // 百度智能小程序 - 百度生态 + BYTEDANCE = 'bytedance', // 字节跳动小程序 - 抖音、今日头条等 + JD = 'jd', // 京东小程序 - 京东购物生态 + QQ = 'qq', // QQ小程序 - 腾讯QQ生态 + FEISHU = 'feishu', // 飞书小程序 - 企业办公生态 + KUAISHOU = 'kuaishou', // 快手小程序 - 快手短视频生态 + H5 = 'h5', // H5网页版 - 浏览器端应用 + RN = 'rn' // React Native应用 - 原生移动应用 +} + +/** + * 平台用户实体类 + * 管理用户在不同小程序平台的身份信息和授权令牌 + * 实现统一用户体系下的多平台身份绑定 + * 支持OAuth令牌管理和平台特有数据存储 + */ +@Entity('platform_users') +@Index(['platform', 'platformUserId'], { unique: true }) // 确保同一平台下的用户ID唯一 +export class PlatformUser { + /** 主键 - 平台用户记录的唯一标识符 */ + @PrimaryGeneratedColumn('uuid') + id: string; + + /** 统一用户ID - 关联到系统内部的用户实体 */ + @Column() + userId: string; + + /** 平台类型 - 标识用户所属的小程序平台 */ + @Column({ + type: 'enum', + enum: PlatformType + }) + platform: PlatformType; + + /** 平台用户ID - 用户在特定平台的唯一标识符(如微信openId、支付宝userId等) */ + @Column({ length: 100 }) + platformUserId: string; + + /** 平台数据 - 存储平台特有的用户信息(头像、昵称、性别等平台相关数据) */ + @Column({ type: 'json', nullable: true }) + platformData: any; + + /** 访问令牌 - 调用平台API的访问凭证,用于获取用户数据和执行操作 */ + @Column({ length: 500, nullable: true }) + accessToken: string; + + /** 刷新令牌 - 用于刷新过期的访问令牌,维持长期授权状态 */ + @Column({ length: 500, nullable: true }) + refreshToken: string; + + /** 令牌过期时间 - 访问令牌的过期时间戳,用于判断是否需要刷新令牌 */ + @Column({ type: 'timestamp', nullable: true }) + expiresAt: Date; + + /** 创建时间 - 平台用户绑定的创建时间戳 */ + @CreateDateColumn() + createdAt: Date; + + /** 更新时间 - 记录最后修改时间,用于令牌刷新和数据同步追踪 */ + @UpdateDateColumn() + updatedAt: Date; + + /** 用户关系 - 与统一用户实体的多对一关系,级联删除 */ + @ManyToOne(() => User, user => user.platformUsers, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) + user: User; +} \ No newline at end of file diff --git a/src/entities/template-execution.entity.ts b/src/entities/template-execution.entity.ts new file mode 100644 index 0000000..94961c8 --- /dev/null +++ b/src/entities/template-execution.entity.ts @@ -0,0 +1,132 @@ +import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { N8nTemplateEntity } from './n8n-template.entity'; +import { PlatformType } from './platform-user.entity'; + +/** + * 模板执行状态枚举 + * 定义AI内容生成任务的生命周期状态 + */ +export enum ExecutionStatus { + PENDING = 'pending', // 待处理 - 任务已提交,等待执行 + PROCESSING = 'processing', // 处理中 - 任务正在执行,AI模型生成中 + COMPLETED = 'completed', // 已完成 - 任务成功完成,内容已生成 + FAILED = 'failed', // 执行失败 - 任务执行出错,生成失败 + CANCELLED = 'cancelled' // 已取消 - 用户主动取消或系统超时取消 +} + +/** + * 执行类型枚举 + * 定义模板执行的内容输出类型 + */ +export enum ExecutionType { + IMAGE = 'image', // 图片生成 - 输出图片内容 + VIDEO = 'video' // 视频生成 - 输出视频内容 +} + +/** + * 模板执行记录实体类 + * 记录用户使用AI模板生成内容的完整执行过程 + * 包含执行状态、性能指标、积分消耗和结果数据 + * 支持图片和视频两种类型的AI生成任务追踪 + */ +@Entity('template_executions') +@Index(['templateId']) // 模板ID索引 - 快速查询特定模板的执行记录 +@Index(['userId']) // 用户ID索引 - 快速查询用户的执行历史 +@Index(['platform']) // 平台索引 - 按平台筛选执行记录 +@Index(['status']) // 状态索引 - 快速筛选不同状态的任务 +@Index(['type']) // 类型索引 - 按执行类型(图片/视频)筛选 +@Index(['creditTransactionId']) // 积分交易ID索引 - 关联积分消费记录 +export class TemplateExecutionEntity { + /** 主键 - 模板执行记录的唯一标识符 */ + @PrimaryGeneratedColumn() + id: number; + + /** 模板ID - 关联的N8N模板ID,标识使用的模板配置 */ + @Column({ name: 'template_id' }) + templateId: number; + + /** 用户ID - 发起执行任务的用户标识 */ + @Column({ name: 'user_id' }) + userId: string; + + /** 执行平台 - 任务发起的平台来源 */ + @Column({ type: 'enum', enum: PlatformType }) + platform: PlatformType; + + /** 执行类型 - 标识生成内容的类型(图片或视频) */ + @Column({ type: 'enum', enum: ExecutionType }) + type: ExecutionType; + + /** 用户提示词 - 用户输入的内容生成描述,作为AI模型的输入 */ + @Column({ type: 'text' }) + prompt: string; + + /** 输入图片URL - 作为参考或处理基础的输入图片地址(可选) */ + @Column({ name: 'input_image_url', length: 500, nullable: true }) + inputImageUrl: string; + + /** 输出内容URL - 生成完成后的图片或视频文件地址 */ + @Column({ name: 'output_url', length: 500, nullable: true }) + outputUrl: string; + + /** 缩略图URL - 生成内容的预览缩略图地址,用于快速预览 */ + @Column({ name: 'thumbnail_url', length: 500, nullable: true }) + thumbnailUrl: string; + + /** 执行状态 - 当前任务的处理状态 */ + @Column({ type: 'enum', enum: ExecutionStatus, default: ExecutionStatus.PENDING }) + status: ExecutionStatus; + + /** 执行进度 - 任务完成百分比(0-100),用于显示进度条 */ + @Column({ default: 0 }) + progress: number; + + /** 错误信息 - 任务失败时的详细错误描述 */ + @Column({ name: 'error_message', type: 'text', nullable: true }) + errorMessage: string; + + /** 执行结果 - AI模型返回的完整结果数据,包含生成参数和元信息 */ + @Column({ name: 'execution_result', type: 'json', nullable: true }) + executionResult: any; + + /** 积分消耗 - 本次执行实际消耗的用户积分数量 */ + @Column({ name: 'credit_cost', default: 0 }) + creditCost: number; + + /** 积分交易ID - 关联的积分消费记录ID,用于财务对账和退款处理 */ + @Column({ name: 'credit_transaction_id', length: 100, nullable: true }) + creditTransactionId: string; + + /** 输入参数 - 用户提供的完整输入参数,包含提示词、样式、尺寸等配置 */ + @Column({ name: 'input_params', type: 'json', nullable: true }) + inputParams: any; + + /** 执行配置 - 模板执行的技术配置,包含模型参数、生成选项等 */ + @Column({ name: 'execution_config', type: 'json', nullable: true }) + executionConfig: any; + + /** 开始执行时间 - 任务实际开始处理的时间戳 */ + @Column({ name: 'started_at', nullable: true }) + startedAt: Date; + + /** 完成时间 - 任务执行完成(成功或失败)的时间戳 */ + @Column({ name: 'completed_at', nullable: true }) + completedAt: Date; + + /** 执行时长 - 任务的实际处理时间(毫秒),用于性能分析和优化 */ + @Column({ name: 'execution_duration', nullable: true }) + executionDuration: number; + + /** 创建时间 - 执行记录的创建时间戳 */ + @CreateDateColumn({ name: 'created_at' }) + createdAt: Date; + + /** 更新时间 - 记录最后修改时间,用于状态变更追踪 */ + @UpdateDateColumn({ name: 'updated_at' }) + updatedAt: Date; + + /** 模板关系 - 与N8N模板实体的多对一关系 */ + @ManyToOne(() => N8nTemplateEntity) + @JoinColumn({ name: 'template_id' }) + template: N8nTemplateEntity; +} \ No newline at end of file diff --git a/src/entities/user-credit.entity.ts b/src/entities/user-credit.entity.ts new file mode 100644 index 0000000..20e38f6 --- /dev/null +++ b/src/entities/user-credit.entity.ts @@ -0,0 +1,97 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { User } from './user.entity'; +import { PlatformType } from './platform-user.entity'; + +/** + * 积分变动类型枚举 + * 定义用户积分账户的不同变动操作类型 + */ +export enum CreditType { + REWARD = 'reward', // 奖励获得 - 用户通过完成任务获得积分 + CONSUME = 'consume', // 消费扣除 - 用户使用功能消耗积分 + PURCHASE = 'purchase', // 购买获得 - 用户付费购买积分 + REFUND = 'refund' // 退款返还 - 服务异常或用户申请退款 +} + +/** + * 积分来源枚举 + * 标识积分变动的具体业务来源 + * 用于积分流水分析和运营统计 + */ +export enum CreditSource { + AD_WATCH = 'ad_watch', // 看广告 - 用户观看激励广告获得积分 + SUBSCRIPTION = 'subscription', // 订阅赠送 - 订阅会员获得的积分奖励 + AI_GENERATION = 'ai_generation', // AI生成消费 - 使用AI功能消耗积分 + MANUAL = 'manual' // 手动调整 - 管理员手动调整用户积分 +} + +/** + * 用户积分记录实体类 + * 记录用户积分账户的所有变动明细 + * 提供完整的积分流水追踪和余额管理 + * 支持多平台积分系统和业务关联 + */ +@Entity('user_credits') +export class UserCredit { + /** 主键 - 积分记录的唯一标识符 */ + @PrimaryGeneratedColumn('uuid') + id: string; + + /** 用户ID - 关联到用户实体 */ + @Column() + userId: string; + + /** 平台类型 - 标识积分变动发生的平台,支持多平台积分管理 */ + @Column({ + type: 'enum', + enum: PlatformType + }) + platform: PlatformType; + + /** 变动类型 - 积分的操作类型(奖励、消费、购买、退款) */ + @Column({ + type: 'enum', + enum: CreditType + }) + type: CreditType; + + /** 积分来源 - 标识积分变动的具体业务来源,用于数据分析和运营决策 */ + @Column({ + type: 'enum', + enum: CreditSource + }) + source: CreditSource; + + /** 变动金额 - 积分变动数量,正数为增加,负数为扣除 */ + @Column({ type: 'integer' }) + amount: number; + + /** 操作后余额 - 本次操作完成后的用户积分余额,用于余额校验和历史查询 */ + @Column({ type: 'integer' }) + balance: number; + + /** 变动描述 - 积分变动的文字说明,便于用户理解和客服处理 */ + @Column({ length: 200, nullable: true }) + description: string; + + /** 关联业务ID - 关联的业务记录ID(如广告观看ID、模板执行ID、订单ID等) */ + @Column({ length: 100, nullable: true }) + referenceId: string; + + /** 扩展元数据 - 存储积分操作的附加信息,如优惠规则、活动信息等 */ + @Column({ type: 'json', nullable: true }) + metadata: any; + + /** 创建时间 - 积分变动记录的创建时间戳 */ + @CreateDateColumn() + createdAt: Date; + + /** 更新时间 - 记录最后修改时间,用于数据审计 */ + @UpdateDateColumn() + updatedAt: Date; + + /** 用户关系 - 与用户实体的多对一关系,级联删除 */ + @ManyToOne(() => User, user => user.credits, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) + user: User; +} \ No newline at end of file diff --git a/src/entities/user-subscription.entity.ts b/src/entities/user-subscription.entity.ts new file mode 100644 index 0000000..eda7685 --- /dev/null +++ b/src/entities/user-subscription.entity.ts @@ -0,0 +1,109 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { User } from './user.entity'; +import { PlatformType } from './platform-user.entity'; + +/** + * 订阅套餐枚举 + * 定义不同级别的会员服务套餐 + * 每个套餐提供不同的功能权益和积分额度 + */ +export enum SubscriptionPlan { + BASIC = 'basic', // 基础版 - 提供基础AI生成功能和有限积分 + PREMIUM = 'premium', // 高级版 - 增加更多积分和高级功能访问权 + PRO = 'pro' // 专业版 - 提供最高级的功能和无限积分使用 +} + +/** + * 订阅状态枚举 + * 定义用户订阅的当前状态 + */ +export enum SubscriptionStatus { + ACTIVE = 'active', // 激活中 - 订阅正常生效,用户可享受所有权益 + EXPIRED = 'expired', // 已过期 - 订阅已过期,需要续费或降级为免费版 + CANCELLED = 'cancelled', // 已取消 - 用户主动取消订阅,不再自动续费 + PENDING = 'pending' // 待激活 - 支付完成但尚未激活的订阅 +} + +/** + * 用户订阅实体类 + * 管理用户的会员订阅服务信息 + * 支持多平台订阅和自动续费管理 + * 提供积分配额和特权功能管理 + */ +@Entity('user_subscriptions') +export class UserSubscription { + /** 主键 - 订阅记录的唯一标识符 */ + @PrimaryGeneratedColumn('uuid') + id: string; + + /** 用户ID - 关联到用户实体 */ + @Column() + userId: string; + + /** 订阅平台 - 订阅服务购买的平台来源 */ + @Column({ + type: 'enum', + enum: PlatformType + }) + platform: PlatformType; + + /** 订阅套餐 - 用户选择的会员级别(基础、高级、专业) */ + @Column({ + type: 'enum', + enum: SubscriptionPlan + }) + plan: SubscriptionPlan; + + /** 订阅状态 - 订阅的当前生命周期状态 */ + @Column({ + type: 'enum', + enum: SubscriptionStatus, + default: SubscriptionStatus.PENDING + }) + status: SubscriptionStatus; + + /** 订阅开始时间 - 会员服务的生效开始时间 */ + @Column({ type: 'timestamp' }) + startDate: Date; + + /** 订阅结束时间 - 会员服务的到期时间 */ + @Column({ type: 'timestamp' }) + endDate: Date; + + /** 订阅价格 - 用户实际支付的订阅费用金额 */ + @Column({ type: 'decimal', precision: 10, scale: 2 }) + price: number; + + /** 货币类型 - 订阅费用的货币单位,默认为人民币 */ + @Column({ length: 10, default: 'CNY' }) + currency: string; + + /** 每月赠送积分 - 订阅套餐每月可获得的积分数量 */ + @Column({ type: 'integer' }) + monthlyCredits: number; + + /** 支付订单ID - 关联的三方支付平台订单标识 */ + @Column({ length: 100, nullable: true }) + paymentId: string; + + /** 订阅特权 - 订阅套餐提供的具体功能和权益列表 */ + @Column({ type: 'json', nullable: true }) + features: any; + + /** 自动续费 - 是否开启自动续费功能,用于续费管理 */ + @Column({ type: 'boolean', default: true }) + autoRenew: boolean; + + /** 创建时间 - 订阅记录的创建时间戳 */ + @CreateDateColumn() + createdAt: Date; + + /** 更新时间 - 记录最后修改时间,用于订阅状态变更追踪 */ + @UpdateDateColumn() + updatedAt: Date; + + /** 用户关系 - 与用户实体的多对一关系,级联删除 */ + @ManyToOne(() => User, user => user.subscriptions, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'userId' }) + user: User; +} \ No newline at end of file diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts new file mode 100644 index 0000000..246922a --- /dev/null +++ b/src/entities/user.entity.ts @@ -0,0 +1,76 @@ +import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'; +import { PlatformUser } from './platform-user.entity'; +import { ExtensionData } from './extension-data.entity'; +import { UserCredit } from './user-credit.entity'; +import { TemplateExecutionEntity } from './template-execution.entity'; +import { UserSubscription } from './user-subscription.entity'; +import { AdWatch } from './ad-watch.entity'; + +/** + * 用户实体类 + * 系统的核心用户实体,管理统一用户身份和基本信息 + * 作为多平台用户数据的中心节点,关联所有业务模块 + * 支持跨平台用户身份统一管理 + */ +@Entity('users') +export class User { + /** 主键 - 用户的内部唯一标识符 */ + @PrimaryGeneratedColumn('uuid') + id: string; + + /** 统一用户ID - 跨平台的用户唯一标识,用于关联多个平台账号 */ + @Column({ unique: true, length: 64 }) + unifiedUserId: string; + + /** 用户昵称 - 用户在应用中显示的名称,可以来自不同平台 */ + @Column({ length: 100, nullable: true }) + nickname: string; + + /** 头像URL - 用户头像图片的存储地址,支持本地和云存储 */ + @Column({ length: 500, nullable: true }) + avatarUrl: string; + + /** 手机号码 - 用户绑定的手机号,用于身份验证和通知 */ + @Column({ length: 20, nullable: true }) + phone: string; + + /** 电子邮箱 - 用户的邮箱地址,用于账号管理和通知推送 */ + @Column({ length: 100, nullable: true }) + email: string; + + /** 用户状态 - 用户账号的当前状态(1:正常, 0:禁用, 2:待激活等) */ + @Column({ type: 'smallint', default: 1 }) + status: number; + + /** 创建时间 - 用户注册的时间戳 */ + @CreateDateColumn() + createdAt: Date; + + /** 更新时间 - 用户信息最后修改时间 */ + @UpdateDateColumn() + updatedAt: Date; + + /** 平台用户列表 - 用户在各个小程序平台的身份信息和授权令牌 */ + @OneToMany(() => PlatformUser, platformUser => platformUser.user) + platformUsers: PlatformUser[]; + + /** 扩展数据列表 - 用户在各平台的特有业务数据和第三方集成数据 */ + @OneToMany(() => ExtensionData, extensionData => extensionData.user) + extensionData: ExtensionData[]; + + /** 积分记录列表 - 用户的所有积分变动明细和交易流水 */ + @OneToMany(() => UserCredit, userCredit => userCredit.user) + credits: UserCredit[]; + + /** 模板执行列表 - 用户使用AI模板生成内容的历史记录 */ + @OneToMany(() => TemplateExecutionEntity, execution => execution.userId) + templateExecutions: TemplateExecutionEntity[]; + + /** 订阅列表 - 用户的会员订阅服务信息和历史记录 */ + @OneToMany(() => UserSubscription, userSubscription => userSubscription.user) + subscriptions: UserSubscription[]; + + /** 广告观看列表 - 用户观看广告的完整记录和奖励明细 */ + @OneToMany(() => AdWatch, adWatch => adWatch.user) + adWatches: AdWatch[]; +} \ No newline at end of file diff --git a/src/migrations/1725501000000-CreatePlatformTypeEnum.ts b/src/migrations/1725501000000-CreatePlatformTypeEnum.ts new file mode 100644 index 0000000..f62b097 --- /dev/null +++ b/src/migrations/1725501000000-CreatePlatformTypeEnum.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * 创建平台类型枚举 + * 定义系统支持的所有小程序平台和移动端平台类型 + * 为多平台用户身份管理提供基础数据类型 + */ +export class CreatePlatformTypeEnum1725501000000 implements MigrationInterface { + name = 'CreatePlatformTypeEnum1725501000000'; + + public async up(queryRunner: QueryRunner): Promise { + // MySQL 不需要单独创建 ENUM 类型,在创建表时直接使用 ENUM + // 这个 migration 在 MySQL 中实际上不需要执行任何操作 + // ENUM 会在具体的表字段中定义 + } + + public async down(queryRunner: QueryRunner): Promise { + // MySQL 中无需单独删除 ENUM 类型 + } +} \ No newline at end of file diff --git a/src/migrations/1725502000000-CreateUserTable.ts b/src/migrations/1725502000000-CreateUserTable.ts new file mode 100644 index 0000000..f80eade --- /dev/null +++ b/src/migrations/1725502000000-CreateUserTable.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * 创建用户表 + * 系统的核心用户实体,管理统一用户身份和基本信息 + * 作为多平台用户数据的中心节点,关联所有业务模块 + */ +export class CreateUserTable1725502000000 implements MigrationInterface { + name = 'CreateUserTable1725502000000'; + + public async up(queryRunner: QueryRunner): Promise { + // 创建用户表 - 统一用户身份管理的核心表 + await queryRunner.query(` + CREATE TABLE \`users\` ( + \`id\` VARCHAR(36) NOT NULL, + \`unifiedUserId\` VARCHAR(64) NOT NULL, + \`nickname\` VARCHAR(100), + \`avatarUrl\` VARCHAR(500), + \`phone\` VARCHAR(20), + \`email\` VARCHAR(100), + \`status\` SMALLINT NOT NULL DEFAULT 1, + \`createdAt\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + \`updatedAt\` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT \`UQ_users_unifiedUserId\` UNIQUE (\`unifiedUserId\`), + CONSTRAINT \`PK_users_id\` PRIMARY KEY (\`id\`) + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "users"`); + } +} \ No newline at end of file diff --git a/src/migrations/1725503000000-CreatePlatformUserTable.ts b/src/migrations/1725503000000-CreatePlatformUserTable.ts new file mode 100644 index 0000000..f34af08 --- /dev/null +++ b/src/migrations/1725503000000-CreatePlatformUserTable.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * 创建平台用户关联表 + * 管理用户在不同小程序平台的身份信息和授权令牌 + * 实现统一用户体系下的多平台身份绑定 + */ +export class CreatePlatformUserTable1725503000000 implements MigrationInterface { + name = 'CreatePlatformUserTable1725503000000'; + + public async up(queryRunner: QueryRunner): Promise { + // 创建平台用户关联表 - 支持OAuth令牌管理和平台特有数据存储 + await queryRunner.query(` + CREATE TABLE "platform_users" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "userId" uuid NOT NULL, + "platform" "platform_type" NOT NULL, + "platformUserId" character varying(100) NOT NULL, + "platformData" json, + "accessToken" character varying(500), + "refreshToken" character varying(500), + "expiresAt" TIMESTAMP, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "UQ_platform_users_platform_platformUserId" UNIQUE ("platform", "platformUserId"), + CONSTRAINT "PK_platform_users_id" PRIMARY KEY ("id"), + CONSTRAINT "FK_platform_users_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "platform_users"`); + } +} \ No newline at end of file diff --git a/src/migrations/1725504000000-CreateExtensionDataTable.ts b/src/migrations/1725504000000-CreateExtensionDataTable.ts new file mode 100644 index 0000000..cef9bf1 --- /dev/null +++ b/src/migrations/1725504000000-CreateExtensionDataTable.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * 创建扩展数据表 + * 用于存储各平台特有的业务数据和第三方集成数据 + * 提供灵活的JSON存储机制,支持动态数据结构 + */ +export class CreateExtensionDataTable1725504000000 implements MigrationInterface { + name = 'CreateExtensionDataTable1725504000000'; + + public async up(queryRunner: QueryRunner): Promise { + // 创建扩展数据表 - 灵活存储各平台特有业务数据 + await queryRunner.query(` + CREATE TABLE "extension_data" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "userId" uuid NOT NULL, + "platform" "platform_type" NOT NULL, + "dataType" character varying(50) NOT NULL, + "referenceId" character varying(100), + "data" json NOT NULL, + "metadata" json, + "status" character varying(20) NOT NULL DEFAULT 'active', + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_extension_data_id" PRIMARY KEY ("id"), + CONSTRAINT "FK_extension_data_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "extension_data"`); + } +} \ No newline at end of file diff --git a/src/migrations/1725505000000-CreateUserCreditTable.ts b/src/migrations/1725505000000-CreateUserCreditTable.ts new file mode 100644 index 0000000..4980709 --- /dev/null +++ b/src/migrations/1725505000000-CreateUserCreditTable.ts @@ -0,0 +1,48 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * 创建积分相关枚举和用户积分表 + * 记录用户积分账户的所有变动明细 + * 提供完整的积分流水追踪和余额管理 + */ +export class CreateUserCreditTable1725505000000 implements MigrationInterface { + name = 'CreateUserCreditTable1725505000000'; + + public async up(queryRunner: QueryRunner): Promise { + // 创建积分变动类型枚举 + await queryRunner.query(` + CREATE TYPE "credit_type" AS ENUM('reward', 'consume', 'purchase', 'refund') + `); + + // 创建积分来源枚举 + await queryRunner.query(` + CREATE TYPE "credit_source" AS ENUM('ad_watch', 'subscription', 'ai_generation', 'manual') + `); + + // 创建用户积分表 - 支持多平台积分系统和业务关联 + await queryRunner.query(` + CREATE TABLE "user_credits" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "userId" uuid NOT NULL, + "platform" "platform_type" NOT NULL, + "type" "credit_type" NOT NULL, + "source" "credit_source" NOT NULL, + "amount" integer NOT NULL, + "balance" integer NOT NULL, + "description" character varying(200), + "referenceId" character varying(100), + "metadata" json, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_user_credits_id" PRIMARY KEY ("id"), + CONSTRAINT "FK_user_credits_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "user_credits"`); + await queryRunner.query(`DROP TYPE "credit_source"`); + await queryRunner.query(`DROP TYPE "credit_type"`); + } +} \ No newline at end of file diff --git a/src/migrations/1725506000000-CreateTemplateExecutionTable.ts b/src/migrations/1725506000000-CreateTemplateExecutionTable.ts new file mode 100644 index 0000000..016b379 --- /dev/null +++ b/src/migrations/1725506000000-CreateTemplateExecutionTable.ts @@ -0,0 +1,58 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * 创建模板执行相关枚举和表 + * 记录用户使用AI模板生成内容的完整执行过程 + * 包含执行状态、性能指标、积分消耗和结果数据 + */ +export class CreateTemplateExecutionTable1725506000000 implements MigrationInterface { + name = 'CreateTemplateExecutionTable1725506000000'; + + public async up(queryRunner: QueryRunner): Promise { + // 创建模板执行类型枚举 + await queryRunner.query(` + CREATE TYPE "execution_type" AS ENUM('image', 'video') + `); + + // 创建执行状态枚举 + await queryRunner.query(` + CREATE TYPE "execution_status" AS ENUM('pending', 'processing', 'completed', 'failed', 'cancelled') + `); + + // 创建模板执行记录表 - 支持图片和视频两种类型的AI生成任务追踪 + await queryRunner.query(` + CREATE TABLE "template_executions" ( + "id" SERIAL PRIMARY KEY, + "templateId" integer NOT NULL, + "userId" uuid NOT NULL, + "platform" "platform_type" NOT NULL, + "type" "execution_type" NOT NULL, + "prompt" text NOT NULL, + "inputImageUrl" character varying(500), + "outputUrl" character varying(500), + "thumbnailUrl" character varying(500), + "status" "execution_status" NOT NULL DEFAULT 'pending', + "progress" integer DEFAULT 0, + "errorMessage" text, + "executionResult" json, + "creditCost" integer NOT NULL DEFAULT 0, + "creditTransactionId" character varying(100), + "inputParams" json, + "executionConfig" json, + "startedAt" TIMESTAMP, + "completedAt" TIMESTAMP, + "executionDuration" integer, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_template_executions_id" PRIMARY KEY ("id"), + CONSTRAINT "FK_template_executions_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "template_executions"`); + await queryRunner.query(`DROP TYPE "execution_status"`); + await queryRunner.query(`DROP TYPE "execution_type"`); + } +} \ No newline at end of file diff --git a/src/migrations/1725507000000-CreateUserSubscriptionTable.ts b/src/migrations/1725507000000-CreateUserSubscriptionTable.ts new file mode 100644 index 0000000..2341552 --- /dev/null +++ b/src/migrations/1725507000000-CreateUserSubscriptionTable.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * 创建订阅相关枚举和用户订阅表 + * 管理用户的会员订阅服务信息 + * 支持多平台订阅和自动续费管理 + */ +export class CreateUserSubscriptionTable1725507000000 implements MigrationInterface { + name = 'CreateUserSubscriptionTable1725507000000'; + + public async up(queryRunner: QueryRunner): Promise { + // 创建订阅套餐类型枚举 + await queryRunner.query(` + CREATE TYPE "subscription_plan" AS ENUM('basic', 'premium', 'pro') + `); + + // 创建订阅状态枚举 + await queryRunner.query(` + CREATE TYPE "subscription_status" AS ENUM('active', 'expired', 'cancelled', 'pending') + `); + + // 创建用户订阅表 - 提供积分配额和特权功能管理 + await queryRunner.query(` + CREATE TABLE "user_subscriptions" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "userId" uuid NOT NULL, + "platform" "platform_type" NOT NULL, + "plan" "subscription_plan" NOT NULL, + "status" "subscription_status" NOT NULL DEFAULT 'pending', + "startDate" TIMESTAMP NOT NULL, + "endDate" TIMESTAMP NOT NULL, + "price" decimal(10,2) NOT NULL, + "currency" character varying(10) NOT NULL DEFAULT 'CNY', + "monthlyCredits" integer NOT NULL, + "paymentId" character varying(100), + "features" json, + "autoRenew" boolean NOT NULL DEFAULT true, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_user_subscriptions_id" PRIMARY KEY ("id"), + CONSTRAINT "FK_user_subscriptions_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "user_subscriptions"`); + await queryRunner.query(`DROP TYPE "subscription_status"`); + await queryRunner.query(`DROP TYPE "subscription_plan"`); + } +} \ No newline at end of file diff --git a/src/migrations/1725508000000-CreateAdWatchTable.ts b/src/migrations/1725508000000-CreateAdWatchTable.ts new file mode 100644 index 0000000..f6821a0 --- /dev/null +++ b/src/migrations/1725508000000-CreateAdWatchTable.ts @@ -0,0 +1,48 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * 创建广告相关枚举和广告观看表 + * 记录用户在不同平台上观看广告的完整行为数据 + * 用于积分奖励、数据统计和用户行为分析 + */ +export class CreateAdWatchTable1725508000000 implements MigrationInterface { + name = 'CreateAdWatchTable1725508000000'; + + public async up(queryRunner: QueryRunner): Promise { + // 创建广告类型枚举 - 定义小程序支持的各种广告形式 + await queryRunner.query(` + CREATE TYPE "ad_type" AS ENUM('banner', 'interstitial', 'rewarded', 'native') + `); + + // 创建广告观看状态枚举 - 记录用户与广告交互的不同阶段 + await queryRunner.query(` + CREATE TYPE "ad_status" AS ENUM('started', 'completed', 'skipped', 'failed') + `); + + // 创建广告观看记录表 - 完整记录用户广告观看行为和奖励数据 + await queryRunner.query(` + CREATE TABLE "ad_watches" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "userId" uuid NOT NULL, + "platform" "platform_type" NOT NULL, + "adType" "ad_type" NOT NULL, + "adId" character varying(100) NOT NULL, + "adUnitId" character varying(100), + "status" "ad_status" NOT NULL, + "watchDuration" integer, + "rewardCredits" integer, + "adData" json, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "PK_ad_watches_id" PRIMARY KEY ("id"), + CONSTRAINT "FK_ad_watches_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "ad_watches"`); + await queryRunner.query(`DROP TYPE "ad_status"`); + await queryRunner.query(`DROP TYPE "ad_type"`); + } +} \ No newline at end of file diff --git a/src/migrations/1725509000000-CreateTableIndexes.ts b/src/migrations/1725509000000-CreateTableIndexes.ts new file mode 100644 index 0000000..0aa3974 --- /dev/null +++ b/src/migrations/1725509000000-CreateTableIndexes.ts @@ -0,0 +1,96 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * 创建所有表的性能优化索引 + * 为用户数据查询和业务操作提供高性能数据库索引 + * 优化查询性能和数据检索效率 + */ +export class CreateTableIndexes1725509000000 implements MigrationInterface { + name = 'CreateTableIndexes1725509000000'; + + public async up(queryRunner: QueryRunner): Promise { + // 平台用户表索引 - 优化多平台身份查询性能 + await queryRunner.query(`CREATE INDEX "IDX_platform_users_platform" ON "platform_users" ("platform")`); + await queryRunner.query(`CREATE INDEX "IDX_platform_users_userId" ON "platform_users" ("userId")`); + + // 扩展数据表索引 - 优化业务数据查询和状态筛选 + await queryRunner.query(`CREATE INDEX "IDX_extension_data_userId" ON "extension_data" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_extension_data_platform" ON "extension_data" ("platform")`); + await queryRunner.query(`CREATE INDEX "IDX_extension_data_dataType" ON "extension_data" ("dataType")`); + await queryRunner.query(`CREATE INDEX "IDX_extension_data_referenceId" ON "extension_data" ("referenceId")`); + await queryRunner.query(`CREATE INDEX "IDX_extension_data_status" ON "extension_data" ("status")`); + await queryRunner.query(`CREATE INDEX "IDX_extension_data_createdAt" ON "extension_data" ("createdAt")`); + + // 用户积分表索引 - 优化积分流水查询和统计分析 + await queryRunner.query(`CREATE INDEX "IDX_user_credits_userId" ON "user_credits" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_user_credits_platform" ON "user_credits" ("platform")`); + await queryRunner.query(`CREATE INDEX "IDX_user_credits_type" ON "user_credits" ("type")`); + await queryRunner.query(`CREATE INDEX "IDX_user_credits_source" ON "user_credits" ("source")`); + await queryRunner.query(`CREATE INDEX "IDX_user_credits_createdAt" ON "user_credits" ("createdAt")`); + + // 模板执行表索引 - 优化AI生成任务查询和状态追踪 + await queryRunner.query(`CREATE INDEX "IDX_template_executions_templateId" ON "template_executions" ("templateId")`); + await queryRunner.query(`CREATE INDEX "IDX_template_executions_userId" ON "template_executions" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_template_executions_platform" ON "template_executions" ("platform")`); + await queryRunner.query(`CREATE INDEX "IDX_template_executions_type" ON "template_executions" ("type")`); + await queryRunner.query(`CREATE INDEX "IDX_template_executions_status" ON "template_executions" ("status")`); + await queryRunner.query(`CREATE INDEX "IDX_template_executions_creditTransactionId" ON "template_executions" ("creditTransactionId")`); + await queryRunner.query(`CREATE INDEX "IDX_template_executions_createdAt" ON "template_executions" ("createdAt")`); + + // 用户订阅表索引 - 优化订阅状态查询和到期管理 + await queryRunner.query(`CREATE INDEX "IDX_user_subscriptions_userId" ON "user_subscriptions" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_user_subscriptions_platform" ON "user_subscriptions" ("platform")`); + await queryRunner.query(`CREATE INDEX "IDX_user_subscriptions_status" ON "user_subscriptions" ("status")`); + await queryRunner.query(`CREATE INDEX "IDX_user_subscriptions_endDate" ON "user_subscriptions" ("endDate")`); + + // 广告观看表索引 - 优化广告数据分析和奖励统计 + await queryRunner.query(`CREATE INDEX "IDX_ad_watches_userId" ON "ad_watches" ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_ad_watches_platform" ON "ad_watches" ("platform")`); + await queryRunner.query(`CREATE INDEX "IDX_ad_watches_adType" ON "ad_watches" ("adType")`); + await queryRunner.query(`CREATE INDEX "IDX_ad_watches_status" ON "ad_watches" ("status")`); + await queryRunner.query(`CREATE INDEX "IDX_ad_watches_createdAt" ON "ad_watches" ("createdAt")`); + } + + public async down(queryRunner: QueryRunner): Promise { + // 删除广告观看表索引 + await queryRunner.query(`DROP INDEX "IDX_ad_watches_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_ad_watches_status"`); + await queryRunner.query(`DROP INDEX "IDX_ad_watches_adType"`); + await queryRunner.query(`DROP INDEX "IDX_ad_watches_platform"`); + await queryRunner.query(`DROP INDEX "IDX_ad_watches_userId"`); + + // 删除用户订阅表索引 + await queryRunner.query(`DROP INDEX "IDX_user_subscriptions_endDate"`); + await queryRunner.query(`DROP INDEX "IDX_user_subscriptions_status"`); + await queryRunner.query(`DROP INDEX "IDX_user_subscriptions_platform"`); + await queryRunner.query(`DROP INDEX "IDX_user_subscriptions_userId"`); + + // 删除模板执行表索引 + await queryRunner.query(`DROP INDEX "IDX_template_executions_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_template_executions_creditTransactionId"`); + await queryRunner.query(`DROP INDEX "IDX_template_executions_status"`); + await queryRunner.query(`DROP INDEX "IDX_template_executions_type"`); + await queryRunner.query(`DROP INDEX "IDX_template_executions_platform"`); + await queryRunner.query(`DROP INDEX "IDX_template_executions_userId"`); + await queryRunner.query(`DROP INDEX "IDX_template_executions_templateId"`); + + // 删除用户积分表索引 + await queryRunner.query(`DROP INDEX "IDX_user_credits_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_user_credits_source"`); + await queryRunner.query(`DROP INDEX "IDX_user_credits_type"`); + await queryRunner.query(`DROP INDEX "IDX_user_credits_platform"`); + await queryRunner.query(`DROP INDEX "IDX_user_credits_userId"`); + + // 删除扩展数据表索引 + await queryRunner.query(`DROP INDEX "IDX_extension_data_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_extension_data_status"`); + await queryRunner.query(`DROP INDEX "IDX_extension_data_referenceId"`); + await queryRunner.query(`DROP INDEX "IDX_extension_data_dataType"`); + await queryRunner.query(`DROP INDEX "IDX_extension_data_platform"`); + await queryRunner.query(`DROP INDEX "IDX_extension_data_userId"`); + + // 删除平台用户表索引 + await queryRunner.query(`DROP INDEX "IDX_platform_users_userId"`); + await queryRunner.query(`DROP INDEX "IDX_platform_users_platform"`); + } +} \ No newline at end of file diff --git a/src/services/n8n-template-factory.service.ts b/src/services/n8n-template-factory.service.ts index e234994..985de3f 100644 --- a/src/services/n8n-template-factory.service.ts +++ b/src/services/n8n-template-factory.service.ts @@ -2,10 +2,11 @@ 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'; +import { TemplateExecutionEntity, ExecutionStatus } from '../entities/template-execution.entity'; +import { + DynamicN8nImageTemplate, + DynamicN8nVideoTemplate +} from '../templates/n8n-dynamic-template'; /** * N8n模板工厂服务 @@ -16,15 +17,17 @@ export class N8nTemplateFactoryService { constructor( @InjectRepository(N8nTemplateEntity) private templateRepository: Repository, - ) {} + @InjectRepository(TemplateExecutionEntity) + private executionRepository: Repository, + ) { } /** * 创建图片模板实例 * @param templateId 模板ID * @returns 初始化后的图片模板实例 */ - async createImageTemplate(templateId: number): Promise { - const instance = new N8nImageGenerateTemplateEntity(templateId, this.templateRepository); + async createImageTemplate(templateId: number): Promise { + const instance = new DynamicN8nImageTemplate(templateId, this.templateRepository); return await instance.onInit(); } @@ -33,8 +36,8 @@ export class N8nTemplateFactoryService { * @param templateId 模板ID * @returns 初始化后的视频模板实例 */ - async createVideoTemplate(templateId: number): Promise { - const instance = new N8nVideoGenerateTemplateEntity(templateId, this.templateRepository); + async createVideoTemplate(templateId: number): Promise { + const instance = new DynamicN8nVideoTemplate(templateId, this.templateRepository); return await instance.onInit(); } @@ -45,7 +48,7 @@ export class N8nTemplateFactoryService { */ async createTemplateByCode( code: string - ): Promise { + ): Promise { const config = await this.templateRepository.findOne({ where: { code, isActive: true } }); @@ -68,7 +71,7 @@ export class N8nTemplateFactoryService { */ async createTemplate( templateId: number - ): Promise { + ): Promise { const config = await this.templateRepository.findOne({ where: { id: templateId, isActive: true } }); @@ -172,21 +175,101 @@ export class N8nTemplateFactoryService { } /** - * 获取模板使用统计(预留接口,需要配合使用记录表实现) - * @param templateId 模板ID - * @returns 使用统计信息 + * 创建执行记录 + * @param data 执行记录数据 + * @returns 创建的执行记录 */ - async getTemplateStats(templateId: number): Promise<{ + async createExecution(data: Partial): Promise { + const execution = this.executionRepository.create(data); + return this.executionRepository.save(execution); + } + + /** + * 更新执行记录 + * @param id 执行记录ID + * @param data 更新数据 + * @returns 更新后的执行记录 + */ + async updateExecution(id: number, data: Partial): Promise { + await this.executionRepository.update(id, data); + const item = await this.executionRepository.findOne({ where: { id } }); + if (item) return item; + throw new Error(`not found ${id} from template execution table`) + } + + /** + * 获取用户执行记录 + * @param userId 用户ID + * @param limit 记录数量限制 + * @returns 用户的执行记录列表 + */ + async getUserExecutions(userId: string, limit = 20): Promise { + return this.executionRepository.find({ + where: { userId }, + relations: ['template'], + order: { createdAt: 'DESC' }, + take: limit + }); + } + + /** + * 获取模板执行统计 + * @param templateId 模板ID + * @returns 执行统计信息 + */ + async getTemplateExecutionStats(templateId: number): Promise<{ totalUsage: number; successRate: number; averageExecutionTime: number; + totalCreditCost: number; }> { - // TODO: 实现模板使用统计逻辑 - // 需要配合模板使用记录表(template_usage_logs) + const stats = await this.executionRepository + .createQueryBuilder('execution') + .select('execution.status', 'status') + .addSelect('COUNT(*)', 'count') + .addSelect('AVG(execution.executionDuration)', 'avgDuration') + .addSelect('SUM(execution.creditCost)', 'totalCredit') + .where('execution.templateId = :templateId', { templateId }) + .groupBy('execution.status') + .getRawMany(); + + const total = stats.reduce((sum, stat) => sum + parseInt(stat.count), 0); + const completed = stats.find(stat => stat.status === 'completed'); + const successRate = total > 0 ? (parseInt(completed?.count || '0') / total) : 1.0; + const avgTime = completed ? parseFloat(completed.avgDuration || '0') : 0; + const totalCredit = stats.reduce((sum, stat) => sum + parseInt(stat.totalCredit || '0'), 0); + return { - totalUsage: 0, - successRate: 1.0, - averageExecutionTime: 0 + totalUsage: total, + successRate, + averageExecutionTime: avgTime, + totalCreditCost: totalCredit + }; + } + + /** + * 获取用户积分消耗统计 + * @param userId 用户ID + * @returns 积分消耗统计 + */ + async getUserCreditStats(userId: string): Promise<{ + totalCreditSpent: number; + totalExecutions: number; + averageCreditPerExecution: number; + }> { + const stats = await this.executionRepository + .createQueryBuilder('execution') + .select('COUNT(*)', 'totalExecutions') + .addSelect('SUM(execution.creditCost)', 'totalCreditSpent') + .addSelect('AVG(execution.creditCost)', 'avgCreditPerExecution') + .where('execution.userId = :userId', { userId }) + .andWhere('execution.status = :status', { status: ExecutionStatus.COMPLETED }) + .getRawOne(); + + return { + totalCreditSpent: parseInt(stats.totalCreditSpent || '0'), + totalExecutions: parseInt(stats.totalExecutions || '0'), + averageCreditPerExecution: parseFloat(stats.avgCreditPerExecution || '0') }; } } \ No newline at end of file diff --git a/src/entities/n8n-template-instance.entity.ts b/src/templates/n8n-dynamic-template.ts similarity index 89% rename from src/entities/n8n-template-instance.entity.ts rename to src/templates/n8n-dynamic-template.ts index dd96ae9..9b78709 100644 --- a/src/entities/n8n-template-instance.entity.ts +++ b/src/templates/n8n-dynamic-template.ts @@ -1,12 +1,12 @@ import { Repository } from 'typeorm'; -import { N8nTemplateEntity, TemplateType } from './n8n-template.entity'; -import { N8nImageGenerateTemplate, N8nVideoGenerateTemplate } from '../templates/n8nTemplate'; +import { N8nTemplateEntity, TemplateType } from '../entities/n8n-template.entity'; +import { N8nImageGenerateTemplate, N8nVideoGenerateTemplate } from './n8nTemplate'; /** - * 动态图片模板实体 + * 动态图片模板实现 * 从数据库加载配置,实现N8nImageGenerateTemplate抽象类 */ -export class N8nImageGenerateTemplateEntity extends N8nImageGenerateTemplate { +export class DynamicN8nImageTemplate extends N8nImageGenerateTemplate { private config: N8nTemplateEntity | null = null; constructor( @@ -19,7 +19,7 @@ export class N8nImageGenerateTemplateEntity extends N8nImageGenerateTemplate { /** * 初始化模板配置,从数据库加载数据 */ - async onInit(): Promise { + async onInit(): Promise { this.config = await this.templateRepository.findOne({ where: { id: this.templateId, @@ -81,10 +81,10 @@ export class N8nImageGenerateTemplateEntity extends N8nImageGenerateTemplate { } /** - * 动态视频模板实体 + * 动态视频模板实现 * 从数据库加载配置,实现N8nVideoGenerateTemplate抽象类 */ -export class N8nVideoGenerateTemplateEntity extends N8nVideoGenerateTemplate { +export class DynamicN8nVideoTemplate extends N8nVideoGenerateTemplate { private config: N8nTemplateEntity | null = null; constructor( @@ -97,7 +97,7 @@ export class N8nVideoGenerateTemplateEntity extends N8nVideoGenerateTemplate { /** * 初始化模板配置,从数据库加载数据 */ - async onInit(): Promise { + async onInit(): Promise { this.config = await this.templateRepository.findOne({ where: { id: this.templateId,