diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c130978 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# 数据库配置 +DB_HOST=mysql-6bc9094abd49-public.rds.volces.com +DB_PORT=53305 +DB_USERNAME=root +DB_PASSWORD=WsJWXfwFx0bq6DE2kmB6 +DB_DATABASE=nano_camera_miniapp +DB_SYNCHRONIZE=false +DB_LOGGING=true +DB_MIGRATIONS_RUN=true + +# 应用配置 +NODE_ENV=development +PORT=3000 + +# N8N配置 +N8N_BASE_URL=http://localhost:5678 +N8N_API_KEY=your_n8n_api_key \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index 453c3f8..66069d8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,17 +1,41 @@ import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { TemplateService } from './templates/index'; import { TemplateManager } from './templates/types'; -import { typeOrmConfig } from './config/database.config'; +import { databaseConfig } from './config/database.config'; import { N8nTemplateEntity } from './entities/n8n-template.entity'; +import { TemplateExecutionEntity } from './entities/template-execution.entity'; +import { UserEntity } from './entities/user.entity'; +import { PlatformUserEntity } from './entities/platform-user.entity'; +import { ExtensionDataEntity } from './entities/extension-data.entity'; +import { UserCreditEntity } from './entities/user-credit.entity'; +import { UserSubscriptionEntity } from './entities/user-subscription.entity'; +import { AdWatchEntity } from './entities/ad-watch.entity'; import { N8nTemplateFactoryService } from './services/n8n-template-factory.service'; import { TemplateController } from './controllers/template.controller'; @Module({ imports: [ - TypeOrmModule.forRoot(typeOrmConfig), - TypeOrmModule.forFeature([N8nTemplateEntity]) + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: ['.env.local', '.env'], + }), + TypeOrmModule.forRootAsync({ + inject: [ConfigService], + useFactory: (configService: ConfigService) => databaseConfig(configService), + }), + TypeOrmModule.forFeature([ + N8nTemplateEntity, + TemplateExecutionEntity, + UserEntity, + PlatformUserEntity, + ExtensionDataEntity, + UserCreditEntity, + UserSubscriptionEntity, + AdWatchEntity + ]) ], controllers: [AppController, TemplateController], providers: [ diff --git a/src/config/database.config.ts b/src/config/database.config.ts index 24e726e..ca21e2a 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -3,33 +3,16 @@ import { ConfigService } from '@nestjs/config'; export const databaseConfig = (configService: ConfigService): TypeOrmModuleOptions => ({ type: 'mysql', - host: 'mysql-6bc9094abd49-public.rds.volces.com', - port: 53305, - username: 'root', - password: 'WsJWXfwFx0bq6DE2kmB6', - database: 'nano_camera_miniapp', + host: configService.get('DB_HOST', 'mysql-6bc9094abd49-public.rds.volces.com'), + port: configService.get('DB_PORT', 53305), + username: configService.get('DB_USERNAME', 'root'), + password: configService.get('DB_PASSWORD', 'WsJWXfwFx0bq6DE2kmB6'), + database: configService.get('DB_DATABASE', 'nano_camera_miniapp'), entities: [__dirname + '/../**/*.entity{.ts,.js}'], migrations: [__dirname + '/../migrations/*{.ts,.js}'], - synchronize: false, // 生产环境建议设为false - logging: process.env.NODE_ENV === 'development', - migrationsRun: true, // 自动运行迁移 + synchronize: configService.get('DB_SYNCHRONIZE', false), // 生产环境建议设为false + logging: configService.get('DB_LOGGING', process.env.NODE_ENV === 'development'), + migrationsRun: configService.get('DB_MIGRATIONS_RUN', true), // 自动运行迁移 charset: 'utf8mb4', timezone: '+08:00', -}); - -// 也可以直接导出配置对象 -export const typeOrmConfig: TypeOrmModuleOptions = { - type: 'mysql', - host: 'mysql-6bc9094abd49-public.rds.volces.com', - port: 53305, - username: 'root', - password: 'WsJWXfwFx0bq6DE2kmB6', - database: 'nano_camera_miniapp', - entities: [__dirname + '/../**/*.entity{.ts,.js}'], - migrations: [__dirname + '/../migrations/*{.ts,.js}'], - synchronize: false, - logging: process.env.NODE_ENV === 'development', - migrationsRun: true, - charset: 'utf8mb4', - timezone: '+08:00', -}; \ No newline at end of file +}); \ No newline at end of file diff --git a/src/controllers/template.controller.ts b/src/controllers/template.controller.ts index c459f25..a26ad2c 100644 --- a/src/controllers/template.controller.ts +++ b/src/controllers/template.controller.ts @@ -14,6 +14,7 @@ import { import { N8nTemplateFactoryService } from '../services/n8n-template-factory.service'; import { N8nTemplateEntity, TemplateType } from '../entities/n8n-template.entity'; import { CreateTemplateDto, UpdateTemplateDto } from '../dto/template.dto'; +import { TemplateExecutionEntity } from '../entities/template-execution.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; @@ -26,7 +27,9 @@ export class TemplateController { constructor( private readonly templateFactory: N8nTemplateFactoryService, @InjectRepository(N8nTemplateEntity) - private readonly templateRepository: Repository + private readonly templateRepository: Repository, + @InjectRepository(TemplateExecutionEntity) + private readonly executionRepository: Repository ) {} /** @@ -287,6 +290,112 @@ export class TemplateController { } } + /** + * 查询模板执行进度 + * @param taskId 任务ID + * @returns 执行进度信息 + */ + @Get('execution/:taskId/progress') + async getExecutionProgress(@Param('taskId', ParseIntPipe) taskId: number) { + try { + const execution = await this.executionRepository.findOne({ + where: { id: taskId }, + relations: ['template'] + }); + + if (!execution) { + throw new HttpException('Execution task not found', HttpStatus.NOT_FOUND); + } + + return { + success: true, + data: { + taskId: execution.id, + templateId: execution.templateId, + templateName: execution.template?.name, + userId: execution.userId, + platform: execution.platform, + type: execution.type, + status: execution.status, + progress: execution.progress, + inputImageUrl: execution.inputImageUrl, + outputUrl: execution.outputUrl, + thumbnailUrl: execution.thumbnailUrl, + errorMessage: execution.errorMessage, + creditCost: execution.creditCost, + startedAt: execution.startedAt, + completedAt: execution.completedAt, + executionDuration: execution.executionDuration, + createdAt: execution.createdAt, + updatedAt: execution.updatedAt + } + }; + } catch (error) { + if (error instanceof HttpException) { + throw error; + } + throw new HttpException( + error.message || 'Failed to fetch execution progress', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + /** + * 查询用户的执行任务列表 + * @param userId 用户ID + * @param status 状态筛选 + * @param limit 数量限制 + * @returns 用户执行任务列表 + */ + @Get('executions/user/:userId') + async getUserExecutions( + @Param('userId') userId: string, + @Query('status') status?: string, + @Query('limit') limit: number = 20 + ) { + try { + const queryBuilder = this.executionRepository + .createQueryBuilder('execution') + .leftJoinAndSelect('execution.template', 'template') + .where('execution.userId = :userId', { userId }) + .orderBy('execution.createdAt', 'DESC') + .limit(limit); + + if (status) { + queryBuilder.andWhere('execution.status = :status', { status }); + } + + const executions = await queryBuilder.getMany(); + + return { + success: true, + data: executions.map(execution => ({ + taskId: execution.id, + templateId: execution.templateId, + templateName: execution.template?.name, + templateCode: execution.template?.code, + type: execution.type, + status: execution.status, + progress: execution.progress, + inputImageUrl: execution.inputImageUrl, + outputUrl: execution.outputUrl, + thumbnailUrl: execution.thumbnailUrl, + creditCost: execution.creditCost, + startedAt: execution.startedAt, + completedAt: execution.completedAt, + executionDuration: execution.executionDuration, + createdAt: execution.createdAt + })) + }; + } catch (error) { + throw new HttpException( + error.message || 'Failed to fetch user executions', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + /** * 创建新模板 * @param createTemplateDto 创建模板的数据传输对象 diff --git a/src/entities/ad-watch.entity.ts b/src/entities/ad-watch.entity.ts index 3e4758c..a22b2b6 100644 --- a/src/entities/ad-watch.entity.ts +++ b/src/entities/ad-watch.entity.ts @@ -1,5 +1,5 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; -import { User } from './user.entity'; +import { UserEntity } from './user.entity'; import { PlatformType } from './platform-user.entity'; /** @@ -30,7 +30,7 @@ export enum AdStatus { * 用于积分奖励、数据统计和用户行为分析 */ @Entity('ad_watches') -export class AdWatch { +export class AdWatchEntity { /** 主键 - 广告观看记录的唯一标识符 */ @PrimaryGeneratedColumn('uuid') id: string; @@ -89,7 +89,7 @@ export class AdWatch { updatedAt: Date; /** 用户关系 - 与用户实体的多对一关系,级联删除 */ - @ManyToOne(() => User, user => user.adWatches, { onDelete: 'CASCADE' }) + @ManyToOne(() => UserEntity, user => user.adWatches, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'userId' }) - user: User; + user: UserEntity; } \ No newline at end of file diff --git a/src/entities/extension-data.entity.ts b/src/entities/extension-data.entity.ts index 6849813..a914871 100644 --- a/src/entities/extension-data.entity.ts +++ b/src/entities/extension-data.entity.ts @@ -1,5 +1,5 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; -import { User } from './user.entity'; +import { UserEntity } from './user.entity'; import { PlatformType } from './platform-user.entity'; /** @@ -8,7 +8,7 @@ import { PlatformType } from './platform-user.entity'; * 提供灵活的JSON存储机制,支持动态数据结构 */ @Entity('extension_data') -export class ExtensionData { +export class ExtensionDataEntity { /** 主键 - 扩展数据记录的唯一标识符 */ @PrimaryGeneratedColumn('uuid') id: string; @@ -53,7 +53,7 @@ export class ExtensionData { updatedAt: Date; /** 用户关系 - 与用户实体的多对一关系,级联删除 */ - @ManyToOne(() => User, user => user.extensionData, { onDelete: 'CASCADE' }) + @ManyToOne(() => UserEntity, user => user.extensionData, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'userId' }) - user: User; + user: UserEntity; } \ No newline at end of file diff --git a/src/entities/platform-user.entity.ts b/src/entities/platform-user.entity.ts index 905a4ba..f2eada2 100644 --- a/src/entities/platform-user.entity.ts +++ b/src/entities/platform-user.entity.ts @@ -1,5 +1,5 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm'; -import { User } from './user.entity'; +import { UserEntity } from './user.entity'; /** * 平台类型枚举 @@ -27,7 +27,7 @@ export enum PlatformType { */ @Entity('platform_users') @Index(['platform', 'platformUserId'], { unique: true }) // 确保同一平台下的用户ID唯一 -export class PlatformUser { +export class PlatformUserEntity { /** 主键 - 平台用户记录的唯一标识符 */ @PrimaryGeneratedColumn('uuid') id: string; @@ -72,7 +72,7 @@ export class PlatformUser { updatedAt: Date; /** 用户关系 - 与统一用户实体的多对一关系,级联删除 */ - @ManyToOne(() => User, user => user.platformUsers, { onDelete: 'CASCADE' }) + @ManyToOne(() => UserEntity, user => user.platformUsers, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'userId' }) - user: User; + user: UserEntity; } \ No newline at end of file diff --git a/src/entities/user-credit.entity.ts b/src/entities/user-credit.entity.ts index 20e38f6..8f3f339 100644 --- a/src/entities/user-credit.entity.ts +++ b/src/entities/user-credit.entity.ts @@ -1,5 +1,5 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; -import { User } from './user.entity'; +import { UserEntity } from './user.entity'; import { PlatformType } from './platform-user.entity'; /** @@ -32,7 +32,7 @@ export enum CreditSource { * 支持多平台积分系统和业务关联 */ @Entity('user_credits') -export class UserCredit { +export class UserCreditEntity { /** 主键 - 积分记录的唯一标识符 */ @PrimaryGeneratedColumn('uuid') id: string; @@ -91,7 +91,7 @@ export class UserCredit { updatedAt: Date; /** 用户关系 - 与用户实体的多对一关系,级联删除 */ - @ManyToOne(() => User, user => user.credits, { onDelete: 'CASCADE' }) + @ManyToOne(() => UserEntity, user => user.credits, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'userId' }) - user: User; + user: UserEntity; } \ No newline at end of file diff --git a/src/entities/user-subscription.entity.ts b/src/entities/user-subscription.entity.ts index eda7685..e6edd95 100644 --- a/src/entities/user-subscription.entity.ts +++ b/src/entities/user-subscription.entity.ts @@ -1,5 +1,5 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; -import { User } from './user.entity'; +import { UserEntity } from './user.entity'; import { PlatformType } from './platform-user.entity'; /** @@ -31,7 +31,7 @@ export enum SubscriptionStatus { * 提供积分配额和特权功能管理 */ @Entity('user_subscriptions') -export class UserSubscription { +export class UserSubscriptionEntity { /** 主键 - 订阅记录的唯一标识符 */ @PrimaryGeneratedColumn('uuid') id: string; @@ -103,7 +103,7 @@ export class UserSubscription { updatedAt: Date; /** 用户关系 - 与用户实体的多对一关系,级联删除 */ - @ManyToOne(() => User, user => user.subscriptions, { onDelete: 'CASCADE' }) + @ManyToOne(() => UserEntity, user => user.subscriptions, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'userId' }) - user: User; + user: UserEntity; } \ No newline at end of file diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 246922a..f14bc7c 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -1,10 +1,10 @@ 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 { PlatformUserEntity } from './platform-user.entity'; +import { ExtensionDataEntity } from './extension-data.entity'; +import { UserCreditEntity } from './user-credit.entity'; import { TemplateExecutionEntity } from './template-execution.entity'; -import { UserSubscription } from './user-subscription.entity'; -import { AdWatch } from './ad-watch.entity'; +import { UserSubscriptionEntity } from './user-subscription.entity'; +import { AdWatchEntity } from './ad-watch.entity'; /** * 用户实体类 @@ -13,7 +13,7 @@ import { AdWatch } from './ad-watch.entity'; * 支持跨平台用户身份统一管理 */ @Entity('users') -export class User { +export class UserEntity { /** 主键 - 用户的内部唯一标识符 */ @PrimaryGeneratedColumn('uuid') id: string; @@ -51,26 +51,26 @@ export class User { updatedAt: Date; /** 平台用户列表 - 用户在各个小程序平台的身份信息和授权令牌 */ - @OneToMany(() => PlatformUser, platformUser => platformUser.user) - platformUsers: PlatformUser[]; + @OneToMany(() => PlatformUserEntity, platformUser => platformUser.user) + platformUsers: PlatformUserEntity[]; /** 扩展数据列表 - 用户在各平台的特有业务数据和第三方集成数据 */ - @OneToMany(() => ExtensionData, extensionData => extensionData.user) - extensionData: ExtensionData[]; + @OneToMany(() => ExtensionDataEntity, extensionData => extensionData.user) + extensionData: ExtensionDataEntity[]; /** 积分记录列表 - 用户的所有积分变动明细和交易流水 */ - @OneToMany(() => UserCredit, userCredit => userCredit.user) - credits: UserCredit[]; + @OneToMany(() => UserCreditEntity, userCredit => userCredit.user) + credits: UserCreditEntity[]; /** 模板执行列表 - 用户使用AI模板生成内容的历史记录 */ @OneToMany(() => TemplateExecutionEntity, execution => execution.userId) templateExecutions: TemplateExecutionEntity[]; /** 订阅列表 - 用户的会员订阅服务信息和历史记录 */ - @OneToMany(() => UserSubscription, userSubscription => userSubscription.user) - subscriptions: UserSubscription[]; + @OneToMany(() => UserSubscriptionEntity, userSubscription => userSubscription.user) + subscriptions: UserSubscriptionEntity[]; /** 广告观看列表 - 用户观看广告的完整记录和奖励明细 */ - @OneToMany(() => AdWatch, adWatch => adWatch.user) - adWatches: AdWatch[]; + @OneToMany(() => AdWatchEntity, adWatch => adWatch.user) + adWatches: AdWatchEntity[]; } \ No newline at end of file diff --git a/src/platform/adapters/base.adapter.ts b/src/platform/adapters/base.adapter.ts new file mode 100644 index 0000000..ffd111a --- /dev/null +++ b/src/platform/adapters/base.adapter.ts @@ -0,0 +1,191 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { Repository } from 'typeorm'; +import { JwtService } from '@nestjs/jwt'; +import { UserEntity } from '../../entities/user.entity'; +import { PlatformUserEntity, PlatformType } from '../../entities/platform-user.entity'; +import { + IPlatformAdapter, + PlatformLoginData, + PlatformRegisterData, + UserAuthResult, + PlatformUserInfo, + TokenRefreshResult, + UnifiedUserInfo +} from '../interfaces/platform.interface'; + +@Injectable() +export abstract class BaseAdapter implements IPlatformAdapter { + abstract platform: PlatformType; + + constructor( + protected readonly httpService: HttpService, + protected readonly configService: ConfigService, + protected readonly userRepository: Repository, + protected readonly platformUserRepository: Repository, + protected readonly jwtService: JwtService, + ) {} + + /** + * 查找或创建统一用户 + * 根据平台用户信息查找现有用户,如不存在则创建新用户 + */ + async findOrCreateUnifiedUser(platformUserData: PlatformUserInfo): Promise { + // 查找是否已存在平台用户绑定 + const existingPlatformUser = await this.platformUserRepository.findOne({ + where: { + platform: this.platform, + platformUserId: platformUserData.platformUserId, + }, + relations: ['user'], + }); + + if (existingPlatformUser) { + // 更新平台用户信息 + await this.updatePlatformUserInfo(existingPlatformUser, platformUserData); + return existingPlatformUser.user; + } + + // 创建新的统一用户 + const unifiedUser = await this.createUnifiedUser(platformUserData); + await this.createPlatformUser(unifiedUser.id, platformUserData); + + return unifiedUser; + } + + /** + * 创建统一用户记录 + */ + protected async createUnifiedUser(platformData: PlatformUserInfo): Promise { + const user = new UserEntity(); + user.unifiedUserId = this.generateUnifiedUserId(); + user.nickname = platformData.nickname; + user.avatarUrl = platformData.avatarUrl; + user.phone = platformData.phone; + user.email = platformData.email; + user.status = 1; // 正常状态 + return this.userRepository.save(user); + } + + /** + * 创建平台用户绑定记录 + */ + protected async createPlatformUser(userId: string, platformData: PlatformUserInfo, authData?: any): Promise { + const platformUser = new PlatformUserEntity(); + platformUser.userId = userId; + platformUser.platform = this.platform; + platformUser.platformUserId = platformData.platformUserId; + platformUser.platformData = platformData; + + if (authData) { + platformUser.accessToken = authData.access_token; + platformUser.refreshToken = authData.refresh_token; + platformUser.expiresAt = authData.expires_at ? new Date(authData.expires_at * 1000) : null; + } + + return this.platformUserRepository.save(platformUser); + } + + /** + * 更新平台用户信息 + */ + protected async updatePlatformUserInfo(platformUser: PlatformUserEntity, newData: PlatformUserInfo): Promise { + platformUser.platformData = { ...platformUser.platformData, ...newData }; + await this.platformUserRepository.save(platformUser); + } + + /** + * 更新平台用户认证数据 + */ + protected async updatePlatformUserData(userId: string, authData: any, userInfo?: any): Promise { + const platformUser = await this.platformUserRepository.findOne({ + where: { userId, platform: this.platform } + }); + + if (platformUser) { + // 统一存储session_key到accessToken字段 + platformUser.accessToken = authData.session_key; + platformUser.refreshToken = null; // 两平台都不支持refresh + platformUser.expiresAt = null; // session_key无固定过期时间 + + if (userInfo) { + platformUser.platformData = { ...platformUser.platformData, ...userInfo }; + } + + await this.platformUserRepository.save(platformUser); + } + } + + /** + * 生成统一用户ID + */ + protected generateUnifiedUserId(): string { + return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + /** + * 生成JWT令牌 + */ + protected async generateTokens(user: UserEntity, platform: PlatformType): Promise<{ + accessToken: string; + refreshToken: string; + expiresAt: Date; + }> { + const payload = { + userId: user.id, + unifiedUserId: user.unifiedUserId, + platform, + }; + + const accessToken = this.jwtService.sign(payload, { expiresIn: '24h' }); + const refreshToken = this.jwtService.sign(payload, { expiresIn: '30d' }); + const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24小时后 + + return { accessToken, refreshToken, expiresAt }; + } + + /** + * 格式化统一用户信息 + */ + protected formatUnifiedUserInfo(user: UserEntity): UnifiedUserInfo { + return { + id: user.id, + unifiedUserId: user.unifiedUserId, + nickname: user.nickname, + avatarUrl: user.avatarUrl, + phone: user.phone, + email: user.email, + status: user.status, + platforms: user.platformUsers?.map(pu => pu.platform) || [this.platform], + }; + } + + /** + * 统一错误处理 + */ + protected handlePlatformError(error: any, platform: string): Error { + // 统一不同平台的错误格式 + if (error.response?.data) { + const errorData = error.response.data; + // 微信格式: {errcode, errmsg} + if (errorData.errcode) { + return new Error(`${platform}错误[${errorData.errcode}]: ${errorData.errmsg}`); + } + // 抖音格式: {err_no, err_tips} + if (errorData.err_no !== undefined) { + return new Error(`${platform}错误[${errorData.err_no}]: ${errorData.err_tips}`); + } + } + return new Error(`${platform}API调用失败: ${error.message}`); + } + + // 抽象方法 - 子类必须实现 + abstract login(loginData: PlatformLoginData): Promise; + abstract getUserInfo(token: string, platformUserId: string): Promise; + abstract refreshToken(refreshToken: string): Promise; + abstract register(registerData: PlatformRegisterData): Promise; + abstract updateUserInfo(userId: string, updateData: Partial): Promise; + abstract validateToken(token: string): Promise; + abstract revokeToken(token: string): Promise; +} \ No newline at end of file