From 14f4c450b52bc11a986f5b80cbdf90a2bc059d7b Mon Sep 17 00:00:00 2001 From: imeepos Date: Thu, 4 Sep 2025 17:12:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=A4=9A=E5=B9=B3?= =?UTF-8?q?=E5=8F=B0=E9=80=82=E9=85=8D=E5=99=A8=E6=9E=B6=E6=9E=84=E5=92=8C?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E5=B0=8F=E7=A8=8B=E5=BA=8F=E9=9B=86=E6=88=90?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增字节跳动和微信平台适配器实现 - 完善平台适配器基础架构和接口定义 - 添加平台用户管理和数据同步功能 - 实现完整的平台模块和控制器 - 更新所有实体类支持多平台数据结构 - 优化模板系统支持平台特定功能 --- .env.example | 13 +- docs/platform-adapter-design.md | 1280 +++++++++++++++++ package.json | 5 + pnpm-lock.yaml | 274 +++- src/app.controller.ts | 40 +- src/app.module.ts | 17 +- src/config/database.config.ts | 16 +- src/controllers/template.controller.ts | 218 +-- src/dto/template.dto.ts | 13 +- src/entities/ad-watch.entity.ts | 36 +- src/entities/extension-data.entity.ts | 18 +- src/entities/n8n-template.entity.ts | 81 +- src/entities/platform-user.entity.ts | 41 +- src/entities/template-execution.entity.ts | 43 +- src/entities/user-credit.entity.ts | 36 +- src/entities/user-subscription.entity.ts | 34 +- src/entities/user.entity.ts | 26 +- .../1725434673000-CreateN8nTemplatesTable.ts | 98 +- .../1725434680000-AddPhotoRestoreTemplate.ts | 6 +- ...5434690000-AddCharacterFigurineTemplate.ts | 6 +- .../1725434700000-AddPetFigurineTemplate.ts | 2 +- ...5434710000-AddCosplayRealPersonTemplate.ts | 6 +- .../1725434720000-AddGarageOpeningTemplate.ts | 6 +- ...25434730000-AddJapaneseMagazineTemplate.ts | 6 +- ...25434740000-AddNuclearExplosionTemplate.ts | 6 +- .../1725434750000-AddOpenEyesTemplate.ts | 2 +- .../1725501000000-CreatePlatformTypeEnum.ts | 2 +- .../1725502000000-CreateUserTable.ts | 2 +- .../1725503000000-CreatePlatformUserTable.ts | 6 +- .../1725504000000-CreateExtensionDataTable.ts | 6 +- .../1725505000000-CreateUserCreditTable.ts | 2 +- ...5506000000-CreateTemplateExecutionTable.ts | 6 +- ...25507000000-CreateUserSubscriptionTable.ts | 6 +- .../1725508000000-CreateAdWatchTable.ts | 2 +- .../1725509000000-CreateTableIndexes.ts | 14 +- src/platform/adapters/base.adapter.ts | 100 +- src/platform/adapters/bytedance.adapter.ts | 172 +++ src/platform/adapters/index.ts | 3 + src/platform/adapters/wechat.adapter.ts | 218 +++ .../controllers/unified-user.controller.ts | 139 ++ src/platform/dto/platform-login.dto.ts | 113 ++ src/platform/guards/platform-auth.guard.ts | 54 + src/platform/interfaces/platform.interface.ts | 76 + .../interfaces/user-auth.interface.ts | 11 + src/platform/platform.module.ts | 75 + .../services/platform-adapter.factory.ts | 44 + src/platform/services/unified-user.service.ts | 188 +++ src/services/n8n-template-factory.service.ts | 105 +- src/templates/index.ts | 2 +- src/templates/n8n-dynamic-template.ts | 190 +-- src/templates/n8nTemplate.ts | 139 +- .../n8nTemplates/CharacterFigurineTemplate.ts | 44 +- .../n8nTemplates/CosplayRealPersonTemplate.ts | 37 +- .../n8nTemplates/GarageOpeningTemplate.ts | 45 +- .../n8nTemplates/JapaneseMagazineTemplate.ts | 45 +- .../n8nTemplates/NuclearExplosionTemplate.ts | 37 +- .../n8nTemplates/PetFigurineTemplate.ts | 44 +- .../n8nTemplates/PhotoRestoreTemplate.ts | 29 +- src/templates/n8nTemplates/index.ts | 2 +- .../n8nTemplates/open-eyes.template.ts | 25 +- src/templates/template.service.ts | 25 +- src/templates/types.ts | 92 +- 62 files changed, 3702 insertions(+), 727 deletions(-) create mode 100644 docs/platform-adapter-design.md create mode 100644 src/platform/adapters/bytedance.adapter.ts create mode 100644 src/platform/adapters/index.ts create mode 100644 src/platform/adapters/wechat.adapter.ts create mode 100644 src/platform/controllers/unified-user.controller.ts create mode 100644 src/platform/dto/platform-login.dto.ts create mode 100644 src/platform/guards/platform-auth.guard.ts create mode 100644 src/platform/interfaces/platform.interface.ts create mode 100644 src/platform/interfaces/user-auth.interface.ts create mode 100644 src/platform/platform.module.ts create mode 100644 src/platform/services/platform-adapter.factory.ts create mode 100644 src/platform/services/unified-user.service.ts diff --git a/.env.example b/.env.example index c130978..034f8a4 100644 --- a/.env.example +++ b/.env.example @@ -14,4 +14,15 @@ PORT=3000 # N8N配置 N8N_BASE_URL=http://localhost:5678 -N8N_API_KEY=your_n8n_api_key \ No newline at end of file +N8N_API_KEY=your_n8n_api_key + +# JWT配置 +JWT_SECRET=your_jwt_secret_key_here + +# 微信小程序配置 +WECHAT_APP_ID=your_wechat_app_id +WECHAT_APP_SECRET=your_wechat_app_secret + +# 抖音小程序配置 +BYTEDANCE_APP_ID=your_bytedance_app_id +BYTEDANCE_APP_SECRET=your_bytedance_app_secret \ No newline at end of file diff --git a/docs/platform-adapter-design.md b/docs/platform-adapter-design.md new file mode 100644 index 0000000..2ba3de5 --- /dev/null +++ b/docs/platform-adapter-design.md @@ -0,0 +1,1280 @@ +# 平台适配器模块设计方案 + +## 🏗️ 核心架构设计 + +基于NestJS依赖注入的平台适配器系统,支持微信小程序和抖音小程序的统一用户登录、注册、信息管理。 + +``` +Platform Adapter 层次结构: +┌─────────────────────────────────────┐ +│ Unified User Service │ ← 统一用户服务接口 +├─────────────────────────────────────┤ +│ Platform Adapter Factory │ ← 适配器工厂(策略模式) +├─────────────────────────────────────┤ +│ WechatAdapter │ BytedanceAdapter │ ← 具体平台适配器 +├─────────────────────────────────────┤ +│ BaseAdapter (抽象基类) │ ← 通用适配器接口 +├─────────────────────────────────────┤ +│ External Platform APIs │ ← 微信/抖音API +└─────────────────────────────────────┘ +``` + +## 📁 模块结构 + +```typescript +src/ +├── platform/ +│ ├── adapters/ +│ │ ├── base.adapter.ts // 基础适配器抽象类 +│ │ ├── wechat.adapter.ts // 微信适配器 +│ │ ├── bytedance.adapter.ts // 抖音适配器 +│ │ └── index.ts +│ ├── services/ +│ │ ├── platform-adapter.factory.ts // 适配器工厂 +│ │ └── unified-user.service.ts // 统一用户服务 +│ ├── interfaces/ +│ │ ├── platform.interface.ts // 平台接口定义 +│ │ └── user-auth.interface.ts // 用户认证接口 +│ ├── dto/ +│ │ ├── platform-login.dto.ts // 平台登录DTO +│ │ └── user-info.dto.ts // 用户信息DTO +│ ├── guards/ +│ │ └── platform-auth.guard.ts // 平台认证守卫 +│ └── platform.module.ts // 平台模块 +``` + +## 🔧 核心接口定义 + +### 平台适配器接口 + +```typescript +// platform/interfaces/platform.interface.ts +export interface IPlatformAdapter { + platform: PlatformType; + + // 用户认证相关 + login(loginData: PlatformLoginData): Promise; + getUserInfo(token: string, platformUserId: string): Promise; + refreshToken(refreshToken: string): Promise; + + // 用户注册相关 + register(registerData: PlatformRegisterData): Promise; + + // 用户信息管理 + updateUserInfo(userId: string, updateData: Partial): Promise; + + // 平台特定功能 + validateToken(token: string): Promise; + revokeToken(token: string): Promise; +} + +export interface PlatformLoginData { + code: string; + encryptedData?: string; // 微信小程序加密用户信息 + iv?: string; // 微信小程序加密向量 + userInfo?: any; // 前端传递的基础用户信息 + anonymousCode?: string; // 抖音小程序匿名code(可选) +} + +export interface UserAuthResult { + user: UnifiedUserInfo; + tokens: { + accessToken: string; + refreshToken: string; + expiresAt: Date; + }; + platformData: any; +} + +export interface PlatformUserInfo { + platformUserId: string; // openid + nickname: string; + avatarUrl: string; + gender?: number; + country?: string; + province?: string; + city?: string; + phone?: string; + email?: string; + unionid?: string; // 跨应用用户标识 + anonymousOpenid?: string; // 抖音匿名用户标识 +} + +export interface TokenRefreshResult { + accessToken: string; + refreshToken: string; + expiresAt: Date; +} + +// 注意:两个平台都不支持传统的token刷新机制 +// 微信和抖音的session_key都需要通过重新登录获取 + +export interface UnifiedUserInfo { + id: string; + unifiedUserId: string; + nickname: string; + avatarUrl: string; + phone?: string; + email?: string; + status: number; + platforms: PlatformType[]; +} +``` + +## 🏛️ 基础适配器抽象类 + +```typescript +// platform/adapters/base.adapter.ts +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 { User } from '../../entities/user.entity'; +import { PlatformUser, PlatformType } from '../../entities/platform-user.entity'; +import { IPlatformAdapter, PlatformLoginData, UserAuthResult, PlatformUserInfo, TokenRefreshResult } 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 User(); + 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 PlatformUser(); + 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: PlatformUser, 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: User, 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: User): 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; +} +``` + +## 📱 微信适配器实现 + +```typescript +// platform/adapters/wechat.adapter.ts +import { Injectable, BadRequestException } from '@nestjs/common'; +import { BaseAdapter } from './base.adapter'; +import { PlatformType } from '../../entities/platform-user.entity'; +import { PlatformLoginData, UserAuthResult, PlatformUserInfo, TokenRefreshResult } from '../interfaces/platform.interface'; + +interface WechatAuthResponse { + openid: string; + session_key: string; + unionid?: string; + errcode?: number; + errmsg?: string; +} + +interface WechatUserInfo { + openId: string; + nickName: string; + gender: number; + language: string; + city: string; + province: string; + country: string; + avatarUrl: string; +} + +@Injectable() +export class WechatAdapter extends BaseAdapter { + platform = PlatformType.WECHAT; + + private readonly wechatConfig = { + appId: this.configService.get('WECHAT_APP_ID'), + appSecret: this.configService.get('WECHAT_APP_SECRET'), + }; + + async login(loginData: PlatformLoginData): Promise { + try { + // 1. 使用code换取session_key和openid + const authResult = await this.getWechatAuth(loginData.code); + + // 2. 解密用户信息(如果提供) + let userInfo: WechatUserInfo; + if (loginData.encryptedData && loginData.iv) { + userInfo = this.decryptWechatUserInfo( + loginData.encryptedData, + loginData.iv, + authResult.session_key + ); + } else { + // 使用基础信息创建用户 + userInfo = { + openId: authResult.openid, + nickName: loginData.userInfo?.nickName || '微信用户', + avatarUrl: loginData.userInfo?.avatarUrl || '', + gender: loginData.userInfo?.gender || 0, + language: 'zh_CN', + city: '', + province: '', + country: 'CN', + }; + } + + // 3. 创建或查找统一用户 + const platformUserInfo: PlatformUserInfo = { + platformUserId: authResult.openid, + nickname: userInfo.nickName, + avatarUrl: userInfo.avatarUrl, + gender: userInfo.gender, + country: userInfo.country, + province: userInfo.province, + city: userInfo.city, + }; + + const unifiedUser = await this.findOrCreateUnifiedUser(platformUserInfo); + + // 4. 更新平台用户数据 + await this.updatePlatformUserData(unifiedUser.id, authResult, userInfo); + + // 5. 生成JWT令牌 + const tokens = await this.generateTokens(unifiedUser, this.platform); + + return { + user: this.formatUnifiedUserInfo(unifiedUser), + tokens, + platformData: userInfo, + }; + } catch (error) { + const platformError = this.handlePlatformError(error, '微信'); + throw new BadRequestException(`微信登录失败: ${platformError.message}`); + } + } + + async register(registerData: PlatformRegisterData): Promise { + // 微信小程序通常通过login方法完成注册 + return this.login(registerData); + } + + async getUserInfo(token: string, platformUserId: string): Promise { + const platformUser = await this.platformUserRepository.findOne({ + where: { + platform: this.platform, + platformUserId, + }, + relations: ['user'], + }); + + if (!platformUser) { + throw new BadRequestException('用户不存在'); + } + + return { + platformUserId: platformUser.platformUserId, + nickname: platformUser.user.nickname, + avatarUrl: platformUser.user.avatarUrl, + phone: platformUser.user.phone, + email: platformUser.user.email, + ...platformUser.platformData, + }; + } + + async updateUserInfo(userId: string, updateData: Partial): Promise { + // 更新统一用户信息 + await this.userRepository.update(userId, { + nickname: updateData.nickname, + avatarUrl: updateData.avatarUrl, + phone: updateData.phone, + email: updateData.email, + }); + + // 更新平台用户数据 + const platformUser = await this.platformUserRepository.findOne({ + where: { userId, platform: this.platform } + }); + + if (platformUser) { + platformUser.platformData = { ...platformUser.platformData, ...updateData }; + await this.platformUserRepository.save(platformUser); + } + } + + async refreshToken(refreshToken: string): Promise { + // 微信小程序的session_key通常不支持刷新,需要重新登录 + throw new BadRequestException('微信平台需要重新登录'); + } + + async validateToken(token: string): Promise { + try { + // 验证微信session_key是否有效 + // 通常通过调用微信API验证 + return true; + } catch (error) { + return false; + } + } + + async revokeToken(token: string): Promise { + // 微信平台令牌撤销逻辑 + // 清除本地存储的session_key + } + + /** + * 使用code获取微信授权信息 + */ + private async getWechatAuth(code: string): Promise { + const url = `https://api.weixin.qq.com/sns/jscode2session`; + const params = { + appid: this.wechatConfig.appId, + secret: this.wechatConfig.appSecret, + js_code: code, + grant_type: 'authorization_code', + }; + + const response = await this.httpService.axiosRef.get(url, { params }); + + if (response.data.errcode) { + throw new Error(`微信认证失败: ${response.data.errmsg}`); + } + + return response.data; + } + + /** + * 解密微信用户信息 + */ + private decryptWechatUserInfo(encryptedData: string, iv: string, sessionKey: string): WechatUserInfo { + // 实现微信用户信息解密逻辑 + // 使用crypto模块进行AES解密 + const crypto = require('crypto'); + const decipher = crypto.createDecipheriv('aes-128-cbc', + Buffer.from(sessionKey, 'base64'), + Buffer.from(iv, 'base64') + ); + + let decrypted = decipher.update(encryptedData, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + + return JSON.parse(decrypted); + } +} +``` + +## 📺 抖音适配器实现 + +```typescript +// platform/adapters/bytedance.adapter.ts +import { Injectable, BadRequestException } from '@nestjs/common'; +import { BaseAdapter } from './base.adapter'; +import { PlatformType } from '../../entities/platform-user.entity'; +import { PlatformLoginData, UserAuthResult, PlatformUserInfo, TokenRefreshResult } from '../interfaces/platform.interface'; + +interface BytedanceAuthResponse { + err_no: number; + err_tips: string; + log_id: string; + data: { + session_key: string; + openid: string; + anonymous_openid: string; + unionid: string; + }; +} + +@Injectable() +export class BytedanceAdapter extends BaseAdapter { + platform = PlatformType.BYTEDANCE; + + private readonly bytedanceConfig = { + appId: this.configService.get('BYTEDANCE_APP_ID'), + appSecret: this.configService.get('BYTEDANCE_APP_SECRET'), + }; + + async login(loginData: PlatformLoginData): Promise { + try { + // 1. 使用code换取session_key和openid(一步到位) + const authResult = await this.getBytedanceAuth(loginData.code); + + // 2. 直接使用返回的信息创建用户(无需额外API调用) + const platformUserInfo: PlatformUserInfo = { + platformUserId: authResult.data.openid, + nickname: loginData.userInfo?.nickname || '抖音用户', + avatarUrl: loginData.userInfo?.avatarUrl || '', + }; + + const unifiedUser = await this.findOrCreateUnifiedUser(platformUserInfo); + + // 3. 存储session_key和相关信息(支持匿名用户) + const authData = { + session_key: authResult.data.session_key, + }; + + const extendedUserInfo = { + ...platformUserInfo, + unionid: authResult.data.unionid, + anonymousOpenid: authResult.data.anonymous_openid, + }; + + await this.updatePlatformUserData(unifiedUser.id, authData, extendedUserInfo); + + // 4. 生成JWT令牌 + const tokens = await this.generateTokens(unifiedUser, this.platform); + + return { + user: this.formatUnifiedUserInfo(unifiedUser), + tokens, + platformData: authResult.data, + }; + } catch (error) { + const platformError = this.handlePlatformError(error, '抖音'); + throw new BadRequestException(`抖音登录失败: ${platformError.message}`); + } + } + + async register(registerData: PlatformRegisterData): Promise { + // 抖音小程序通常通过login方法完成注册 + return this.login(registerData); + } + + async getUserInfo(token: string, platformUserId: string): Promise { + const platformUser = await this.platformUserRepository.findOne({ + where: { + platform: this.platform, + platformUserId, + }, + relations: ['user'], + }); + + if (!platformUser) { + throw new BadRequestException('用户不存在'); + } + + return { + platformUserId: platformUser.platformUserId, + nickname: platformUser.user.nickname, + avatarUrl: platformUser.user.avatarUrl, + phone: platformUser.user.phone, + email: platformUser.user.email, + ...platformUser.platformData, + }; + } + + async updateUserInfo(userId: string, updateData: Partial): Promise { + // 更新统一用户信息 + await this.userRepository.update(userId, { + nickname: updateData.nickname, + avatarUrl: updateData.avatarUrl, + phone: updateData.phone, + email: updateData.email, + }); + + // 更新平台用户数据 + const platformUser = await this.platformUserRepository.findOne({ + where: { userId, platform: this.platform } + }); + + if (platformUser) { + platformUser.platformData = { ...platformUser.platformData, ...updateData }; + await this.platformUserRepository.save(platformUser); + } + } + + async refreshToken(refreshToken: string): Promise { + // 抖音小程序的session_key需要重新登录获取,不支持刷新 + throw new BadRequestException('抖音平台需要重新登录'); + } + + async validateToken(token: string): Promise { + try { + // 抖音小程序验证session_key有效性 + // 通常通过checkSession或API调用验证 + return true; + } catch (error) { + return false; + } + } + + async revokeToken(token: string): Promise { + // 抖音平台session_key撤销逻辑 + // session_key无需特殊撤销,清除本地存储即可 + } + + /** + * 获取抖音授权信息 + */ + private async getBytedanceAuth(code: string): Promise { + const url = `https://developer.toutiao.com/api/apps/v2/jscode2session`; + const data = { + appid: this.bytedanceConfig.appId, + secret: this.bytedanceConfig.appSecret, + code: code, + }; + + const response = await this.httpService.axiosRef.post(url, data, { + headers: { 'Content-Type': 'application/json' } + }); + + if (response.data.err_no !== 0) { + throw new Error(`抖音认证失败: ${response.data.err_tips}`); + } + + return response.data; + } +} +``` + +## 🏭 适配器工厂服务 + +```typescript +// platform/services/platform-adapter.factory.ts +import { Injectable, BadRequestException } from '@nestjs/common'; +import { PlatformType } from '../../entities/platform-user.entity'; +import { IPlatformAdapter } from '../interfaces/platform.interface'; +import { WechatAdapter } from '../adapters/wechat.adapter'; +import { BytedanceAdapter } from '../adapters/bytedance.adapter'; + +@Injectable() +export class PlatformAdapterFactory { + private readonly adapters = new Map(); + + constructor( + private readonly wechatAdapter: WechatAdapter, + private readonly bytedanceAdapter: BytedanceAdapter, + ) { + // 注册所有可用的适配器 + this.adapters.set(PlatformType.WECHAT, this.wechatAdapter); + this.adapters.set(PlatformType.BYTEDANCE, this.bytedanceAdapter); + } + + /** + * 根据平台类型获取对应的适配器 + */ + getAdapter(platform: PlatformType): IPlatformAdapter { + const adapter = this.adapters.get(platform); + if (!adapter) { + throw new BadRequestException(`不支持的平台: ${platform}`); + } + return adapter; + } + + /** + * 获取所有支持的平台列表 + */ + getSupportedPlatforms(): PlatformType[] { + return Array.from(this.adapters.keys()); + } + + /** + * 检查平台是否支持 + */ + isPlatformSupported(platform: PlatformType): boolean { + return this.adapters.has(platform); + } + + /** + * 注册新的平台适配器(用于动态扩展) + */ + registerAdapter(platform: PlatformType, adapter: IPlatformAdapter): void { + this.adapters.set(platform, adapter); + } +} +``` + +## 🌐 统一用户服务 + +```typescript +// platform/services/unified-user.service.ts +import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { PlatformAdapterFactory } from './platform-adapter.factory'; +import { User } from '../../entities/user.entity'; +import { PlatformUser, PlatformType } from '../../entities/platform-user.entity'; +import { PlatformLoginData, UserAuthResult, PlatformUserInfo, UnifiedUserInfo } from '../interfaces/platform.interface'; + +@Injectable() +export class UnifiedUserService { + constructor( + private readonly platformAdapterFactory: PlatformAdapterFactory, + @InjectRepository(User) + private readonly userRepository: Repository, + @InjectRepository(PlatformUser) + private readonly platformUserRepository: Repository, + ) {} + + /** + * 统一登录接口 + */ + async login(platform: PlatformType, loginData: PlatformLoginData): Promise { + const adapter = this.platformAdapterFactory.getAdapter(platform); + return adapter.login(loginData); + } + + /** + * 统一注册接口 + */ + async register(platform: PlatformType, registerData: PlatformRegisterData): Promise { + const adapter = this.platformAdapterFactory.getAdapter(platform); + return adapter.register(registerData); + } + + /** + * 获取用户信息 + */ + async getUserInfo(userId: string): Promise { + const user = await this.userRepository.findOne({ + where: { id: userId }, + relations: ['platformUsers'], + }); + + if (!user) { + throw new NotFoundException('用户不存在'); + } + + 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), + }; + } + + /** + * 获取平台用户信息 + */ + async getPlatformUserInfo(platform: PlatformType, token: string, platformUserId: string): Promise { + const adapter = this.platformAdapterFactory.getAdapter(platform); + return adapter.getUserInfo(token, platformUserId); + } + + /** + * 更新用户信息 + */ + async updateUserInfo(platform: PlatformType, userId: string, updateData: Partial): Promise { + const adapter = this.platformAdapterFactory.getAdapter(platform); + return adapter.updateUserInfo(userId, updateData); + } + + /** + * 绑定新平台账号 + */ + async bindPlatform(userId: string, platform: PlatformType, loginData: PlatformLoginData): Promise { + const user = await this.userRepository.findOne({ + where: { id: userId }, + }); + + if (!user) { + throw new NotFoundException('用户不存在'); + } + + // 检查是否已绑定该平台 + const existingBinding = await this.platformUserRepository.findOne({ + where: { userId, platform }, + }); + + if (existingBinding) { + throw new BadRequestException('该平台账号已绑定'); + } + + // 使用适配器获取平台用户信息 + const adapter = this.platformAdapterFactory.getAdapter(platform); + const authResult = await adapter.login(loginData); + + // 创建新的平台用户绑定 + const platformUser = new PlatformUser(); + platformUser.userId = userId; + platformUser.platform = platform; + platformUser.platformUserId = authResult.platformData.openid || authResult.platformData.openId; + platformUser.platformData = authResult.platformData; + + await this.platformUserRepository.save(platformUser); + } + + /** + * 解绑平台账号 + */ + async unbindPlatform(userId: string, platform: PlatformType): Promise { + const platformUser = await this.platformUserRepository.findOne({ + where: { userId, platform }, + }); + + if (!platformUser) { + throw new NotFoundException('平台账号绑定不存在'); + } + + // 撤销平台令牌 + const adapter = this.platformAdapterFactory.getAdapter(platform); + if (platformUser.accessToken) { + try { + await adapter.revokeToken(platformUser.accessToken); + } catch (error) { + // 忽略撤销令牌的错误 + console.warn(`撤销${platform}令牌失败:`, error.message); + } + } + + // 删除平台用户绑定 + await this.platformUserRepository.remove(platformUser); + } + + /** + * 刷新平台令牌 + */ + async refreshPlatformToken(platform: PlatformType, refreshToken: string): Promise { + const adapter = this.platformAdapterFactory.getAdapter(platform); + return adapter.refreshToken(refreshToken); + } + + /** + * 获取支持的平台列表 + */ + getSupportedPlatforms(): PlatformType[] { + return this.platformAdapterFactory.getSupportedPlatforms(); + } +} +``` + +## 📋 DTO定义 + +```typescript +// platform/dto/platform-login.dto.ts +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsEnum, IsOptional, IsObject } from 'class-validator'; +import { PlatformType } from '../../entities/platform-user.entity'; + +export class PlatformLoginDto { + @ApiProperty({ + description: '平台类型', + enum: PlatformType, + example: PlatformType.WECHAT, + }) + @IsEnum(PlatformType) + platform: PlatformType; + + @ApiProperty({ + description: '平台授权码', + example: '081234567890abcdef', + }) + @IsString() + code: string; + + @ApiProperty({ + description: '加密用户数据(微信小程序)', + required: false, + }) + @IsOptional() + @IsString() + encryptedData?: string; + + @ApiProperty({ + description: '加密向量(微信小程序)', + required: false, + }) + @IsOptional() + @IsString() + iv?: string; + + @ApiProperty({ + description: '用户基础信息', + required: false, + }) + @IsOptional() + @IsObject() + userInfo?: any; +} + +export class UserInfoUpdateDto { + @ApiProperty({ description: '用户昵称', required: false }) + @IsOptional() + @IsString() + nickname?: string; + + @ApiProperty({ description: '头像URL', required: false }) + @IsOptional() + @IsString() + avatarUrl?: string; + + @ApiProperty({ description: '手机号码', required: false }) + @IsOptional() + @IsString() + phone?: string; + + @ApiProperty({ description: '电子邮箱', required: false }) + @IsOptional() + @IsString() + email?: string; +} +``` + +## 🌐 平台模块配置 + +```typescript +// platform/platform.module.ts +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { HttpModule } from '@nestjs/axios'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; + +// 实体 +import { User } from '../entities/user.entity'; +import { PlatformUser } from '../entities/platform-user.entity'; +import { ExtensionData } from '../entities/extension-data.entity'; + +// 适配器 +import { WechatAdapter } from './adapters/wechat.adapter'; +import { BytedanceAdapter } from './adapters/bytedance.adapter'; + +// 服务 +import { PlatformAdapterFactory } from './services/platform-adapter.factory'; +import { UnifiedUserService } from './services/unified-user.service'; + +// 控制器 +import { UnifiedUserController } from './controllers/unified-user.controller'; + +// 守卫 +import { PlatformAuthGuard } from './guards/platform-auth.guard'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([User, PlatformUser, ExtensionData]), + HttpModule.register({ + timeout: 5000, + maxRedirects: 3, + }), + JwtModule.registerAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + secret: config.get('JWT_SECRET'), + signOptions: { expiresIn: '24h' }, + }), + }), + ], + providers: [ + // 适配器实现 + WechatAdapter, + BytedanceAdapter, + + // 工厂和服务 + PlatformAdapterFactory, + UnifiedUserService, + + // 守卫 + PlatformAuthGuard, + ], + controllers: [ + UnifiedUserController, + ], + exports: [ + UnifiedUserService, + PlatformAdapterFactory, + ], +}) +export class PlatformModule {} +``` + +## 🎮 控制器实现 + +```typescript +// platform/controllers/unified-user.controller.ts +import { Controller, Post, Body, Get, Put, UseGuards, Request } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; +import { UnifiedUserService } from '../services/unified-user.service'; +import { PlatformLoginDto, UserInfoUpdateDto } from '../dto/platform-login.dto'; +import { PlatformAuthGuard } from '../guards/platform-auth.guard'; + +@ApiTags('统一用户管理') +@Controller('api/v1/users') +export class UnifiedUserController { + constructor( + private readonly unifiedUserService: UnifiedUserService, + ) {} + + @Post('login') + @ApiOperation({ summary: '统一登录接口' }) + @ApiResponse({ status: 200, description: '登录成功' }) + @ApiResponse({ status: 400, description: '请求参数错误' }) + @ApiResponse({ status: 401, description: '授权失败' }) + async login(@Body() loginDto: PlatformLoginDto) { + const result = await this.unifiedUserService.login(loginDto.platform, loginDto); + return { + code: 200, + message: '登录成功', + data: result, + }; + } + + @Post('register') + @ApiOperation({ summary: '统一注册接口' }) + @ApiResponse({ status: 200, description: '注册成功' }) + async register(@Body() registerDto: PlatformLoginDto) { + const result = await this.unifiedUserService.register(registerDto.platform, registerDto); + return { + code: 200, + message: '注册成功', + data: result, + }; + } + + @Get('profile') + @UseGuards(PlatformAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: '获取用户信息' }) + @ApiResponse({ status: 200, description: '获取成功' }) + async getProfile(@Request() req) { + const userInfo = await this.unifiedUserService.getUserInfo(req.user.userId); + return { + code: 200, + message: '获取成功', + data: userInfo, + }; + } + + @Put('profile') + @UseGuards(PlatformAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: '更新用户信息' }) + @ApiResponse({ status: 200, description: '更新成功' }) + async updateProfile(@Request() req, @Body() updateDto: UserInfoUpdateDto) { + await this.unifiedUserService.updateUserInfo(req.user.platform, req.user.userId, updateDto); + return { + code: 200, + message: '更新成功', + }; + } + + @Get('platforms') + @ApiOperation({ summary: '获取支持的平台列表' }) + @ApiResponse({ status: 200, description: '获取成功' }) + async getSupportedPlatforms() { + const platforms = this.unifiedUserService.getSupportedPlatforms(); + return { + code: 200, + message: '获取成功', + data: platforms, + }; + } +} +``` + +## 🛡️ 认证守卫 + +```typescript +// platform/guards/platform-auth.guard.ts +import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { PlatformAdapterFactory } from '../services/platform-adapter.factory'; + +@Injectable() +export class PlatformAuthGuard implements CanActivate { + constructor( + private readonly jwtService: JwtService, + private readonly platformAdapterFactory: PlatformAdapterFactory, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + + if (!token) { + throw new UnauthorizedException('缺少访问令牌'); + } + + try { + // 验证JWT令牌 + const payload = this.jwtService.verify(token); + + // 验证平台令牌有效性 + const adapter = this.platformAdapterFactory.getAdapter(payload.platform); + const isValid = await adapter.validateToken(token); + + if (!isValid) { + throw new UnauthorizedException('令牌无效'); + } + + // 将用户信息附加到请求对象 + request.user = { + userId: payload.userId, + unifiedUserId: payload.unifiedUserId, + platform: payload.platform, + }; + + return true; + } catch (error) { + throw new UnauthorizedException('令牌验证失败'); + } + } + + private extractTokenFromHeader(request: any): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} +``` + +## 📊 核心优势 + +### 🎯 设计优势 +- **策略模式**: 通过工厂模式动态选择平台适配器,易于扩展 +- **统一接口**: 所有平台遵循相同的接口规范,简化上层业务逻辑 +- **依赖注入**: 充分利用NestJS的DI容器管理依赖关系 +- **类型安全**: 全链路TypeScript类型检查,减少运行时错误 + +### 🔧 技术特点 +- **异步处理**: 支持OAuth流程的异步令牌刷新机制 +- **错误隔离**: 各平台错误不会相互影响,提高系统稳定性 +- **配置分离**: 平台密钥通过环境变量管理,提高安全性 +- **数据统一**: 不同平台用户数据映射到统一结构 + +### 📈 扩展能力 +- **快速接入**: 新增平台只需实现BaseAdapter接口 +- **功能扩展**: 通过ExtensionData表存储平台特有功能 +- **多租户**: 通过平台隔离支持多租户架构 +- **监控集成**: 便于集成日志、监控和性能追踪 + +## 📋 官方API验证结果 + +### 微信小程序 API (已验证) +- **接口地址**: `GET https://api.weixin.qq.com/sns/jscode2session` +- **请求参数**: `appid`, `secret`, `js_code`, `grant_type=authorization_code` +- **返回数据**: `openid`, `session_key`, `unionid`, `errcode`, `errmsg` +- **重要特性**: + - code只能使用一次,有效期5分钟 + - session_key不支持刷新,需要重新登录 + - 支持加密用户信息解密(encryptedData + iv) + +### 抖音小程序 API (已验证) +- **接口地址**: `POST https://developer.toutiao.com/api/apps/v2/jscode2session` +- **请求参数**: `appid`, `secret`, `code`, `anonymous_code`(可选) +- **返回数据**: `session_key`, `openid`, `anonymous_openid`, `unionid` +- **重要特性**: + - 返回格式为 `{err_no: 0, err_tips: "success", data: {...}}` + - session_key每次登录都会刷新 + - 支持匿名用户和实名用户 + +## ⚠️ 设计方案需要修正的问题 + +### 1. 抖音API返回格式错误 +**问题**: 原设计中抖音返回格式不正确 +**修正**: 抖音API返回格式应为: +```typescript +interface BytedanceAuthResponse { + err_no: number; + err_tips: string; + log_id: string; + data: { + session_key: string; + openid: string; + anonymous_openid: string; + unionid: string; + }; +} +``` + +### 2. 抖音认证流程简化 +**问题**: 抖音不需要separate的getUserInfo调用 +**修正**: 抖音的code2session已经包含基本用户标识,不需要额外的用户信息接口 + +### 3. Token存储策略调整 +**问题**: 微信存储session_key,抖音存储access_token,混淆了概念 +**修正**: 统一存储策略,微信存储session_key到accessToken字段,抖音存储实际的session_key + +## 🤔 关键讨论要点 + +1. **认证流程差异处理**: + - **微信**: code -> session_key+openid,可选解密用户信息 + - **抖音**: code -> session_key+openid+unionid,一步到位 + - **统一方案**: 都使用session_key作为身份凭证,存储到同一字段 + +2. **用户信息获取策略**: + - **微信**: 依赖前端传递基础信息或解密详细信息 + - **抖音**: 服务端直接获取基础身份信息 + - **建议**: 以openid为唯一标识,其他信息为可选补充 + +3. **Token刷新机制**: + - **微信**: 不支持刷新,需要重新登录 + - **抖音**: session_key每次登录自动刷新 + - **建议**: 统一使用重新登录策略,简化复杂度 + +4. **错误处理统一**: + - **微信**: errcode/errmsg格式 + - **抖音**: err_no/err_tips格式 + - **建议**: 在适配器层统一错误格式 + +5. **匿名用户支持**: + - **抖音**: 支持anonymous_openid + - **微信**: 不支持匿名模式 + - **建议**: 在抖音适配器中处理匿名用户逻辑 + +6. **数据安全考虑**: + - 两平台都强调session_key不能下发到客户端 + - 需要在设计中明确session_key的服务端存储和使用 + +## 📝 实施步骤建议 + +1. **Phase 1**: 实现基础架构和接口定义 +2. **Phase 2**: 完成微信适配器和测试用例 +3. **Phase 3**: 完成抖音适配器和测试用例 +4. **Phase 4**: 集成测试和性能优化 +5. **Phase 5**: 部署和监控配置 + +--- + +*本设计方案遵循NestJS最佳实践,采用依赖注入和模块化架构,确保代码质量和扩展性。* \ No newline at end of file diff --git a/package.json b/package.json index 27b76e9..e811260 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,17 @@ "schema:drop": "npm run typeorm -- schema:drop" }, "dependencies": { + "@nestjs/axios": "^4.0.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.0", "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", "axios": "^1.11.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", "mysql2": "^3.9.7", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9884d3a..199c3f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,24 +8,39 @@ importers: .: dependencies: + '@nestjs/axios': + specifier: ^4.0.1 + version: 4.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.11.0)(rxjs@7.8.2) '@nestjs/common': specifier: ^11.0.1 - version: 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': specifier: ^4.0.2 - version: 4.0.2(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + version: 4.0.2(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': specifier: ^11.0.1 - version: 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': + specifier: ^11.0.0 + version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/platform-express': specifier: ^11.0.1 - version: 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) + version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) + '@nestjs/swagger': + specifier: ^11.2.0 + version: 11.2.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) '@nestjs/typeorm': specifier: ^11.0.0 - version: 11.0.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2))) + version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2))) axios: specifier: ^1.11.0 version: 1.11.0 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.2 + version: 0.14.2 mysql2: specifier: ^3.9.7 version: 3.14.4 @@ -53,7 +68,7 @@ importers: version: 11.0.7(chokidar@4.0.3)(typescript@5.9.2) '@nestjs/testing': specifier: ^11.0.1 - version: 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-express@11.1.6) + version: 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-express@11.1.6) '@types/express': specifier: ^5.0.0 version: 5.0.3 @@ -640,9 +655,19 @@ packages: resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} + '@microsoft/tsdoc@0.15.1': + resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@nestjs/axios@4.0.1': + resolution: {integrity: sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + axios: ^1.3.1 + rxjs: ^7.0.0 + '@nestjs/cli@11.0.10': resolution: {integrity: sha512-4waDT0yGWANg0pKz4E47+nUrqIJv/UqrZ5wLPkCqc7oMGRMWKAaw1NDZ9rKsaqhqvxb2LfI5+uXOWr4yi94DOQ==} engines: {node: '>= 20.11'} @@ -693,6 +718,24 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/jwt@11.0.0': + resolution: {integrity: sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + + '@nestjs/mapped-types@2.1.0': + resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/platform-express@11.1.6': resolution: {integrity: sha512-HErwPmKnk+loTq8qzu1up+k7FC6Kqa8x6lJ4cDw77KnTxLzsCaPt+jBvOq6UfICmfqcqCCf3dKXg+aObQp+kIQ==} peerDependencies: @@ -704,6 +747,23 @@ packages: peerDependencies: typescript: '>=4.8.2' + '@nestjs/swagger@11.2.0': + resolution: {integrity: sha512-5wolt8GmpNcrQv34tIPUtPoV1EeFbCetm40Ij3+M0FNNnf2RJ3FyWfuQvI8SBlcJyfaounYVTKzKHreFXsUyOg==} + peerDependencies: + '@fastify/static': ^8.0.0 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + '@nestjs/testing@11.1.6': resolution: {integrity: sha512-srYzzDNxGvVCe1j0SpTS9/ix75PKt6Sn6iMaH1rpJ6nj2g8vwNrhK0CoJJXvpCYgrnI+2WES2pprYnq8rAMYHA==} peerDependencies: @@ -758,6 +818,9 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@sinclair/typebox@0.34.41': resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} @@ -846,6 +909,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonwebtoken@9.0.7': + resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} + '@types/methods@1.1.4': resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} @@ -876,6 +942,9 @@ packages: '@types/supertest@6.0.3': resolution: {integrity: sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w==} + '@types/validator@13.15.3': + resolution: {integrity: sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1290,6 +1359,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1360,6 +1432,12 @@ packages: cjs-module-lexer@2.1.0: resolution: {integrity: sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==} + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.14.2: + resolution: {integrity: sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -1548,6 +1626,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -2239,6 +2320,16 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2250,6 +2341,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libphonenumber-js@1.12.15: + resolution: {integrity: sha512-TMDCtIhWUDHh91wRC+wFuGlIzKdPzaTUHHVrIZ3vPUEoNaXFLrsIQ1ZpAeZeXApIF6rvDksMTvjrIQlLKaYxqQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -2269,12 +2363,33 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -2869,6 +2984,9 @@ packages: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} engines: {node: '>=10'} + swagger-ui-dist@5.21.0: + resolution: {integrity: sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==} + symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} @@ -3148,6 +3266,10 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + validator@13.15.15: + resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} + engines: {node: '>= 0.10'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -3925,6 +4047,8 @@ snapshots: '@lukeed/csprng@1.1.0': {} + '@microsoft/tsdoc@0.15.1': {} + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.5.0 @@ -3932,6 +4056,12 @@ snapshots: '@tybys/wasm-util': 0.10.0 optional: true + '@nestjs/axios@4.0.1(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.11.0)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + axios: 1.11.0 + rxjs: 7.8.2 + '@nestjs/cli@11.0.10(@types/node@22.18.0)': dependencies: '@angular-devkit/core': 19.2.15(chokidar@4.0.3) @@ -3959,7 +4089,7 @@ snapshots: - uglify-js - webpack-cli - '@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: file-type: 21.0.0 iterare: 1.2.1 @@ -3968,20 +4098,23 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.2 transitivePeerDependencies: - supports-color - '@nestjs/config@4.0.2(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + '@nestjs/config@4.0.2(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) dotenv: 16.4.7 dotenv-expand: 12.0.1 lodash: 4.17.21 rxjs: 7.8.2 - '@nestjs/core@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/core@11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - '@nestjs/common': 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -3991,12 +4124,26 @@ snapshots: tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) + '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) - '@nestjs/platform-express@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)': + '@nestjs/jwt@11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.7 + jsonwebtoken: 9.0.2 + + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.2 + + '@nestjs/platform-express@11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)': + dependencies: + '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) cors: 2.8.5 express: 5.1.0 multer: 2.0.2 @@ -4027,18 +4174,33 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/testing@11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-express@11.1.6)': + '@nestjs/swagger@11.2.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@microsoft/tsdoc': 0.15.1 + '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2) + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 8.2.0 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.21.0 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.2 + + '@nestjs/testing@11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(@nestjs/platform-express@11.1.6)': + dependencies: + '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) + '@nestjs/platform-express': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6) - '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)))': + '@nestjs/typeorm@11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)))': dependencies: - '@nestjs/common': 11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.6(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 rxjs: 7.8.2 typeorm: 0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)) @@ -4070,6 +4232,8 @@ snapshots: '@pkgr/core@0.2.9': {} + '@scarf/scarf@1.4.0': {} + '@sinclair/typebox@0.34.41': {} '@sinonjs/commons@3.0.1': @@ -4181,6 +4345,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.7': + dependencies: + '@types/node': 22.18.0 + '@types/methods@1.1.4': {} '@types/mime@1.3.5': {} @@ -4218,6 +4386,8 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 + '@types/validator@13.15.3': {} + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.33': @@ -4673,6 +4843,8 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -4735,6 +4907,14 @@ snapshots: cjs-module-lexer@2.1.0: {} + class-transformer@0.5.1: {} + + class-validator@0.14.2: + dependencies: + '@types/validator': 13.15.3 + libphonenumber-js: 1.12.15 + validator: 13.15.15 + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -4887,6 +5067,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.213: {} @@ -5797,6 +5981,30 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.2 + + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -5808,6 +6016,8 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libphonenumber-js@1.12.15: {} + lines-and-columns@1.2.4: {} load-esm@1.0.2: {} @@ -5822,10 +6032,24 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash@4.17.21: {} log-symbols@4.1.0: @@ -6392,6 +6616,10 @@ snapshots: dependencies: has-flag: 4.0.0 + swagger-ui-dist@5.21.0: + dependencies: + '@scarf/scarf': 1.4.0 + symbol-observable@4.0.0: {} synckit@0.11.11: @@ -6640,6 +6868,8 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + validator@13.15.15: {} + vary@1.1.2: {} walker@1.0.8: diff --git a/src/app.controller.ts b/src/app.controller.ts index a1a651d..b26e39b 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,5 +1,5 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common'; -import { TemplateService } from './templates/index' +import { TemplateService } from './templates/index'; import { Template } from './templates/types'; interface ApiResponse { status: boolean | string; @@ -9,7 +9,7 @@ interface ApiResponse { @Controller('api/v1/templates') export class AppController { - constructor(private readonly tempalte: TemplateService) { } + constructor(private readonly tempalte: TemplateService) {} @Get() async getTemplates(): Promise> { @@ -22,7 +22,9 @@ export class AppController { // 安全地输出模板列表 try { - const templateNames = templates.map(t => t?.code || t?.name || '[未命名模板]').filter(Boolean); + const templateNames = templates + .map((t) => t?.code || t?.name || '[未命名模板]') + .filter(Boolean); console.log(`📊 模板列表: ${templateNames.join(', ')}`); } catch (error) { console.log(`📊 模板列表: [无法获取模板名称]`); @@ -31,12 +33,14 @@ export class AppController { return { status: true, data: templates, - msg: 'success' + msg: 'success', }; } @Get(':templateCode') - async getTemplate(@Param('templateCode') templateCode: string): Promise> { + async getTemplate( + @Param('templateCode') templateCode: string, + ): Promise> { console.log(`🔍 [获取模板] 请求获取模板: ${templateCode}`); console.log(`⏰ 请求时间: ${new Date().toISOString()}`); @@ -61,7 +65,7 @@ export class AppController { return { status: true, data: template, - msg: 'success' + msg: 'success', }; } @@ -70,12 +74,15 @@ export class AppController { return { status: false, data: null, - msg: '模板不存在' + msg: '模板不存在', }; } @Post() - async executeTemplate(@Body('imageUrl') imageUrl: string, @Body('templateCode') templateCode: string): Promise> { + async executeTemplate( + @Body('imageUrl') imageUrl: string, + @Body('templateCode') templateCode: string, + ): Promise> { const startTime = Date.now(); console.log(`🚀 [模板执行] 开始执行模板`); console.log(`📋 模板代码: ${templateCode}`); @@ -89,7 +96,7 @@ export class AppController { return { status: false, data: null, - msg: '模板代码不能为空' + msg: '模板代码不能为空', }; } @@ -98,13 +105,16 @@ export class AppController { return { status: false, data: null, - msg: '图片URL不能为空' + msg: '图片URL不能为空', }; } console.log(`✅ [模板执行] 参数验证通过,开始执行模板处理`); - const result = await this.tempalte.executeTemplate(templateCode, imageUrl); + const result = await this.tempalte.executeTemplate( + templateCode, + imageUrl, + ); const executionTime = Date.now() - startTime; if (result) { @@ -126,7 +136,7 @@ export class AppController { return { status: true, data: result, - msg: 'success' + msg: 'success', }; } @@ -137,7 +147,7 @@ export class AppController { return { status: false, data: null, - msg: '执行失败' + msg: '执行失败', }; } catch (e) { const executionTime = Date.now() - startTime; @@ -155,7 +165,7 @@ export class AppController { return { status: false, data: null, - msg: e?.message || '模板执行异常' + msg: e?.message || '模板执行异常', }; } } @@ -167,7 +177,7 @@ export class AppController { return { status: true, data: body, - msg: 'success' + msg: 'success', }; } } diff --git a/src/app.module.ts b/src/app.module.ts index 66069d8..6a2a996 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -15,6 +15,7 @@ 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'; +import { PlatformModule } from './platform/platform.module'; @Module({ imports: [ @@ -24,7 +25,8 @@ import { TemplateController } from './controllers/template.controller'; }), TypeOrmModule.forRootAsync({ inject: [ConfigService], - useFactory: (configService: ConfigService) => databaseConfig(configService), + useFactory: (configService: ConfigService) => + databaseConfig(configService), }), TypeOrmModule.forFeature([ N8nTemplateEntity, @@ -34,15 +36,12 @@ import { TemplateController } from './controllers/template.controller'; ExtensionDataEntity, UserCreditEntity, UserSubscriptionEntity, - AdWatchEntity - ]) + AdWatchEntity, + ]), + PlatformModule, ], controllers: [AppController, TemplateController], - providers: [ - TemplateService, - TemplateManager, - N8nTemplateFactoryService, - ], - exports: [N8nTemplateFactoryService] + providers: [TemplateService, TemplateManager, N8nTemplateFactoryService], + exports: [N8nTemplateFactoryService], }) export class AppModule {} diff --git a/src/config/database.config.ts b/src/config/database.config.ts index ca21e2a..078bd25 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -1,9 +1,14 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm'; import { ConfigService } from '@nestjs/config'; -export const databaseConfig = (configService: ConfigService): TypeOrmModuleOptions => ({ +export const databaseConfig = ( + configService: ConfigService, +): TypeOrmModuleOptions => ({ type: 'mysql', - host: configService.get('DB_HOST', 'mysql-6bc9094abd49-public.rds.volces.com'), + 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'), @@ -11,8 +16,11 @@ export const databaseConfig = (configService: ConfigService): TypeOrmModuleOptio entities: [__dirname + '/../**/*.entity{.ts,.js}'], migrations: [__dirname + '/../migrations/*{.ts,.js}'], synchronize: configService.get('DB_SYNCHRONIZE', false), // 生产环境建议设为false - logging: configService.get('DB_LOGGING', process.env.NODE_ENV === 'development'), + logging: configService.get( + 'DB_LOGGING', + process.env.NODE_ENV === 'development', + ), migrationsRun: configService.get('DB_MIGRATIONS_RUN', true), // 自动运行迁移 charset: 'utf8mb4', timezone: '+08:00', -}); \ No newline at end of file +}); diff --git a/src/controllers/template.controller.ts b/src/controllers/template.controller.ts index a26ad2c..45edc66 100644 --- a/src/controllers/template.controller.ts +++ b/src/controllers/template.controller.ts @@ -1,18 +1,21 @@ -import { - Controller, - Get, - Post, +import { + Controller, + Get, + Post, Put, Delete, - Param, - Body, - ParseIntPipe, + Param, + Body, + ParseIntPipe, Query, HttpException, - HttpStatus + HttpStatus, } from '@nestjs/common'; import { N8nTemplateFactoryService } from '../services/n8n-template-factory.service'; -import { N8nTemplateEntity, TemplateType } from '../entities/n8n-template.entity'; +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'; @@ -29,7 +32,7 @@ export class TemplateController { @InjectRepository(N8nTemplateEntity) private readonly templateRepository: Repository, @InjectRepository(TemplateExecutionEntity) - private readonly executionRepository: Repository + private readonly executionRepository: Repository, ) {} /** @@ -41,11 +44,11 @@ export class TemplateController { @Post(':templateId/execute') async executeTemplateById( @Param('templateId', ParseIntPipe) templateId: number, - @Body() body: { imageUrl: string } + @Body() body: { imageUrl: string }, ) { try { const { imageUrl } = body; - + if (!imageUrl) { throw new HttpException('imageUrl is required', HttpStatus.BAD_REQUEST); } @@ -62,13 +65,13 @@ export class TemplateController { templateName: template.name, inputUrl: imageUrl, outputUrl: result, - creditCost: template.creditCost - } + creditCost: template.creditCost, + }, }; } catch (error) { throw new HttpException( error.message || 'Template execution failed', - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -82,11 +85,11 @@ export class TemplateController { @Post('code/:code/execute') async executeTemplateByCode( @Param('code') code: string, - @Body() body: { imageUrl: string } + @Body() body: { imageUrl: string }, ) { try { const { imageUrl } = body; - + if (!imageUrl) { throw new HttpException('imageUrl is required', HttpStatus.BAD_REQUEST); } @@ -102,13 +105,13 @@ export class TemplateController { templateName: template.name, inputUrl: imageUrl, outputUrl: result, - creditCost: template.creditCost - } + creditCost: template.creditCost, + }, }; } catch (error) { throw new HttpException( error.message || 'Template execution failed', - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -122,17 +125,23 @@ export class TemplateController { @Post(':templateId/batch-execute') async batchExecuteTemplate( @Param('templateId', ParseIntPipe) templateId: number, - @Body() body: { imageUrls: string[] } + @Body() body: { imageUrls: string[] }, ) { try { const { imageUrls } = body; - + if (!imageUrls || !Array.isArray(imageUrls) || imageUrls.length === 0) { - throw new HttpException('imageUrls array is required', HttpStatus.BAD_REQUEST); + throw new HttpException( + 'imageUrls array is required', + HttpStatus.BAD_REQUEST, + ); } - const results = await this.templateFactory.batchExecute(templateId, imageUrls); - + const results = await this.templateFactory.batchExecute( + templateId, + imageUrls, + ); + return { success: true, data: { @@ -140,14 +149,14 @@ export class TemplateController { totalCount: imageUrls.length, results: imageUrls.map((inputUrl, index) => ({ inputUrl, - outputUrl: results[index] - })) - } + outputUrl: results[index], + })), + }, }; } catch (error) { throw new HttpException( error.message || 'Batch execution failed', - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -162,7 +171,7 @@ export class TemplateController { const templates = await this.templateFactory.getAllTemplates(); return { success: true, - data: templates.map(template => ({ + data: templates.map((template) => ({ id: template.id, code: template.code, name: template.name, @@ -177,13 +186,13 @@ export class TemplateController { sortOrder: template.sortOrder, isActive: template.isActive, createdAt: template.createdAt, - updatedAt: template.updatedAt - })) + updatedAt: template.updatedAt, + })), }; } catch (error) { throw new HttpException( error.message || 'Failed to fetch templates', - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -198,12 +207,12 @@ export class TemplateController { const templates = await this.templateFactory.getImageTemplates(); return { success: true, - data: templates + data: templates, }; } catch (error) { throw new HttpException( error.message || 'Failed to fetch image templates', - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -218,12 +227,12 @@ export class TemplateController { const templates = await this.templateFactory.getVideoTemplates(); return { success: true, - data: templates + data: templates, }; } catch (error) { throw new HttpException( error.message || 'Failed to fetch video templates', - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -237,23 +246,26 @@ export class TemplateController { @Get('recommend') async recommendTemplates( @Query('tags') tags?: string, - @Query('limit') limit: number = 5 + @Query('limit') limit: number = 5, ) { try { - const userTags = tags ? tags.split(',').map(tag => tag.trim()) : []; - const templates = await this.templateFactory.recommendTemplates(userTags, limit); - + const userTags = tags ? tags.split(',').map((tag) => tag.trim()) : []; + const templates = await this.templateFactory.recommendTemplates( + userTags, + limit, + ); + return { success: true, data: { userTags, - recommendedTemplates: templates - } + recommendedTemplates: templates, + }, }; } catch (error) { throw new HttpException( error.message || 'Failed to recommend templates', - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -267,7 +279,7 @@ export class TemplateController { async getTemplateById(@Param('templateId', ParseIntPipe) templateId: number) { try { const template = await this.templateFactory.createTemplate(templateId); - + return { success: true, data: { @@ -279,13 +291,13 @@ export class TemplateController { version: template.version, tags: template.tags, inputExample: template.input, - outputExample: template.output - } + outputExample: template.output, + }, }; } catch (error) { throw new HttpException( error.message || 'Template not found', - HttpStatus.NOT_FOUND + HttpStatus.NOT_FOUND, ); } } @@ -300,11 +312,14 @@ export class TemplateController { try { const execution = await this.executionRepository.findOne({ where: { id: taskId }, - relations: ['template'] + relations: ['template'], }); if (!execution) { - throw new HttpException('Execution task not found', HttpStatus.NOT_FOUND); + throw new HttpException( + 'Execution task not found', + HttpStatus.NOT_FOUND, + ); } return { @@ -327,8 +342,8 @@ export class TemplateController { completedAt: execution.completedAt, executionDuration: execution.executionDuration, createdAt: execution.createdAt, - updatedAt: execution.updatedAt - } + updatedAt: execution.updatedAt, + }, }; } catch (error) { if (error instanceof HttpException) { @@ -336,7 +351,7 @@ export class TemplateController { } throw new HttpException( error.message || 'Failed to fetch execution progress', - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -352,7 +367,7 @@ export class TemplateController { async getUserExecutions( @Param('userId') userId: string, @Query('status') status?: string, - @Query('limit') limit: number = 20 + @Query('limit') limit: number = 20, ) { try { const queryBuilder = this.executionRepository @@ -370,7 +385,7 @@ export class TemplateController { return { success: true, - data: executions.map(execution => ({ + data: executions.map((execution) => ({ taskId: execution.id, templateId: execution.templateId, templateName: execution.template?.name, @@ -385,13 +400,13 @@ export class TemplateController { startedAt: execution.startedAt, completedAt: execution.completedAt, executionDuration: execution.executionDuration, - createdAt: execution.createdAt - })) + createdAt: execution.createdAt, + })), }; } catch (error) { throw new HttpException( error.message || 'Failed to fetch user executions', - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.INTERNAL_SERVER_ERROR, ); } } @@ -405,22 +420,22 @@ export class TemplateController { async createTemplate(@Body() createTemplateDto: CreateTemplateDto) { try { const existingTemplate = await this.templateRepository.findOne({ - where: { code: createTemplateDto.code } + where: { code: createTemplateDto.code }, }); - + if (existingTemplate) { throw new HttpException( `Template with code '${createTemplateDto.code}' already exists`, - HttpStatus.CONFLICT + HttpStatus.CONFLICT, ); } const template = this.templateRepository.create(createTemplateDto); const savedTemplate = await this.templateRepository.save(template); - + return { success: true, - data: savedTemplate + data: savedTemplate, }; } catch (error) { if (error instanceof HttpException) { @@ -428,7 +443,7 @@ export class TemplateController { } throw new HttpException( error.message || 'Failed to create template', - HttpStatus.BAD_REQUEST + HttpStatus.BAD_REQUEST, ); } } @@ -442,38 +457,41 @@ export class TemplateController { @Put(':templateId') async updateTemplate( @Param('templateId', ParseIntPipe) templateId: number, - @Body() updateTemplateDto: UpdateTemplateDto + @Body() updateTemplateDto: UpdateTemplateDto, ) { try { const existingTemplate = await this.templateRepository.findOne({ - where: { id: templateId } + where: { id: templateId }, }); - + if (!existingTemplate) { throw new HttpException('Template not found', HttpStatus.NOT_FOUND); } - if (updateTemplateDto.code && updateTemplateDto.code !== existingTemplate.code) { + if ( + updateTemplateDto.code && + updateTemplateDto.code !== existingTemplate.code + ) { const codeConflict = await this.templateRepository.findOne({ - where: { code: updateTemplateDto.code } + where: { code: updateTemplateDto.code }, }); - + if (codeConflict) { throw new HttpException( `Template with code '${updateTemplateDto.code}' already exists`, - HttpStatus.CONFLICT + HttpStatus.CONFLICT, ); } } await this.templateRepository.update(templateId, updateTemplateDto); const updatedTemplate = await this.templateRepository.findOne({ - where: { id: templateId } + where: { id: templateId }, }); - + return { success: true, - data: updatedTemplate + data: updatedTemplate, }; } catch (error) { if (error instanceof HttpException) { @@ -481,7 +499,7 @@ export class TemplateController { } throw new HttpException( error.message || 'Failed to update template', - HttpStatus.BAD_REQUEST + HttpStatus.BAD_REQUEST, ); } } @@ -495,23 +513,23 @@ export class TemplateController { async deleteTemplate(@Param('templateId', ParseIntPipe) templateId: number) { try { const template = await this.templateRepository.findOne({ - where: { id: templateId } + 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' + message: 'Template deleted successfully', }; } catch (error) { throw new HttpException( error.message || 'Failed to delete template', - HttpStatus.BAD_REQUEST + HttpStatus.BAD_REQUEST, ); } } @@ -522,24 +540,26 @@ export class TemplateController { * @returns 模板完整详情(包含非激活状态的模板) */ @Get('admin/:templateId') - async getTemplateForAdmin(@Param('templateId', ParseIntPipe) templateId: number) { + async getTemplateForAdmin( + @Param('templateId', ParseIntPipe) templateId: number, + ) { try { const template = await this.templateRepository.findOne({ - where: { id: templateId } + where: { id: templateId }, }); - + if (!template) { throw new HttpException('Template not found', HttpStatus.NOT_FOUND); } - + return { success: true, - data: template + data: template, }; } catch (error) { throw new HttpException( error.message || 'Template not found', - HttpStatus.NOT_FOUND + HttpStatus.NOT_FOUND, ); } } @@ -557,27 +577,28 @@ export class TemplateController { @Query('page') page: number = 1, @Query('limit') limit: number = 10, @Query('type') type?: TemplateType, - @Query('isActive') isActive?: boolean + @Query('isActive') isActive?: boolean, ) { try { - const queryBuilder = this.templateRepository.createQueryBuilder('template'); - + 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: { @@ -586,16 +607,15 @@ export class TemplateController { page, limit, total, - totalPages: Math.ceil(total / limit) - } - } + totalPages: Math.ceil(total / limit), + }, + }, }; } catch (error) { throw new HttpException( error.message || 'Failed to fetch templates', - HttpStatus.INTERNAL_SERVER_ERROR + HttpStatus.INTERNAL_SERVER_ERROR, ); } } - -} \ No newline at end of file +} diff --git a/src/dto/template.dto.ts b/src/dto/template.dto.ts index d8ee538..5e7774a 100644 --- a/src/dto/template.dto.ts +++ b/src/dto/template.dto.ts @@ -1,4 +1,13 @@ -import { IsString, IsNumber, IsEnum, IsBoolean, IsOptional, IsArray, Min, Max } from 'class-validator'; +import { + IsString, + IsNumber, + IsEnum, + IsBoolean, + IsOptional, + IsArray, + Min, + Max, +} from 'class-validator'; import { TemplateType } from '../entities/n8n-template.entity'; export class CreateTemplateDto { @@ -146,4 +155,4 @@ export class UpdateTemplateDto { @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 index a22b2b6..54d9420 100644 --- a/src/entities/ad-watch.entity.ts +++ b/src/entities/ad-watch.entity.ts @@ -1,4 +1,12 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; import { UserEntity } from './user.entity'; import { PlatformType } from './platform-user.entity'; @@ -7,10 +15,10 @@ import { PlatformType } from './platform-user.entity'; * 定义了小程序支持的各种广告形式 */ export enum AdType { - BANNER = 'banner', // 横幅广告 - 显示在页面顶部或底部的条状广告 + BANNER = 'banner', // 横幅广告 - 显示在页面顶部或底部的条状广告 INTERSTITIAL = 'interstitial', // 插屏广告 - 在应用界面切换时显示的全屏广告 - REWARDED = 'rewarded', // 激励视频广告 - 用户观看完整视频后给予奖励的广告 - NATIVE = 'native' // 原生广告 - 与应用内容自然融合的广告形式 + REWARDED = 'rewarded', // 激励视频广告 - 用户观看完整视频后给予奖励的广告 + NATIVE = 'native', // 原生广告 - 与应用内容自然融合的广告形式 } /** @@ -18,10 +26,10 @@ export enum AdType { * 记录用户与广告交互的不同阶段 */ export enum AdStatus { - STARTED = 'started', // 开始观看 - 用户点击观看广告 - COMPLETED = 'completed', // 观看完成 - 用户完整观看广告内容 - SKIPPED = 'skipped', // 跳过 - 用户在允许时间内跳过广告 - FAILED = 'failed' // 失败 - 广告加载失败或播放出错 + STARTED = 'started', // 开始观看 - 用户点击观看广告 + COMPLETED = 'completed', // 观看完成 - 用户完整观看广告内容 + SKIPPED = 'skipped', // 跳过 - 用户在允许时间内跳过广告 + FAILED = 'failed', // 失败 - 广告加载失败或播放出错 } /** @@ -42,14 +50,14 @@ export class AdWatchEntity { /** 平台类型 - 标识广告观看发生的平台(微信、支付宝等) */ @Column({ type: 'enum', - enum: PlatformType + enum: PlatformType, }) platform: PlatformType; /** 广告类型 - 标识广告的展现形式(横幅、插屏、激励视频、原生) */ @Column({ type: 'enum', - enum: AdType + enum: AdType, }) adType: AdType; @@ -64,7 +72,7 @@ export class AdWatchEntity { /** 观看状态 - 记录用户观看广告的结果状态 */ @Column({ type: 'enum', - enum: AdStatus + enum: AdStatus, }) status: AdStatus; @@ -89,7 +97,9 @@ export class AdWatchEntity { updatedAt: Date; /** 用户关系 - 与用户实体的多对一关系,级联删除 */ - @ManyToOne(() => UserEntity, user => user.adWatches, { onDelete: 'CASCADE' }) + @ManyToOne(() => UserEntity, (user) => user.adWatches, { + onDelete: 'CASCADE', + }) @JoinColumn({ name: 'userId' }) 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 a914871..c33201a 100644 --- a/src/entities/extension-data.entity.ts +++ b/src/entities/extension-data.entity.ts @@ -1,4 +1,12 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; import { UserEntity } from './user.entity'; import { PlatformType } from './platform-user.entity'; @@ -20,7 +28,7 @@ export class ExtensionDataEntity { /** 平台类型 - 标识数据来源平台(微信、支付宝等) */ @Column({ type: 'enum', - enum: PlatformType + enum: PlatformType, }) platform: PlatformType; @@ -53,7 +61,9 @@ export class ExtensionDataEntity { updatedAt: Date; /** 用户关系 - 与用户实体的多对一关系,级联删除 */ - @ManyToOne(() => UserEntity, user => user.extensionData, { onDelete: 'CASCADE' }) + @ManyToOne(() => UserEntity, (user) => user.extensionData, { + onDelete: 'CASCADE', + }) @JoinColumn({ name: 'userId' }) user: UserEntity; -} \ No newline at end of file +} diff --git a/src/entities/n8n-template.entity.ts b/src/entities/n8n-template.entity.ts index e3cb5d1..abec290 100644 --- a/src/entities/n8n-template.entity.ts +++ b/src/entities/n8n-template.entity.ts @@ -1,10 +1,10 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - CreateDateColumn, +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, UpdateDateColumn, - Index + Index, } from 'typeorm'; /** @@ -12,8 +12,8 @@ import { * 定义N8N模板支持的输出内容类型 */ export enum TemplateType { - IMAGE = 'image', // 图片模板 - 生成图片内容 - VIDEO = 'video' // 视频模板 - 生成视频内容 + IMAGE = 'image', // 图片模板 - 生成图片内容 + VIDEO = 'video', // 视频模板 - 生成视频内容 } /** @@ -23,10 +23,10 @@ export enum TemplateType { * 包含模型配置、提示词、积分消耗等关键信息 */ @Entity('n8n_templates') -@Index('idx_code', ['code']) // 模板代码索引 - 快速查找特定模板 +@Index('idx_code', ['code']) // 模板代码索引 - 快速查找特定模板 @Index('idx_type', ['templateType']) // 类型索引 - 按模板类型筛选 @Index('idx_class', ['templateClass']) // 类名索引 - 按具体实现类筛选 -@Index('idx_active', ['isActive']) // 激活状态索引 - 快速筛选可用模板 +@Index('idx_active', ['isActive']) // 激活状态索引 - 快速筛选可用模板 export class N8nTemplateEntity { /** 主键 - 模板的唯一标识符,自增长整数 */ @PrimaryGeneratedColumn('increment', { type: 'bigint' }) @@ -53,11 +53,21 @@ export class N8nTemplateEntity { version: string; /** 输入示例图片 - 展示给用户的输入样例图片URL,帮助用户理解模板用法 */ - @Column({ name: 'input_example_url', type: 'text', nullable: true, comment: '输入示例图片' }) + @Column({ + name: 'input_example_url', + type: 'text', + nullable: true, + comment: '输入示例图片', + }) inputExampleUrl: string; /** 输出示例图片 - 展示给用户的输出效果图片URL,演示模板生成效果 */ - @Column({ name: 'output_example_url', type: 'text', nullable: true, comment: '输出示例图片' }) + @Column({ + name: 'output_example_url', + type: 'text', + nullable: true, + comment: '输出示例图片', + }) outputExampleUrl: string; /** 标签数组 - 模板的分类标签,用于搜索和分类筛选 */ @@ -65,11 +75,11 @@ export class N8nTemplateEntity { tags: string[]; /** 模板类型 - 标识模板的输出内容类型(图片或视频) */ - @Column({ - name: 'template_type', - type: 'enum', - enum: TemplateType, - comment: '模板类型' + @Column({ + name: 'template_type', + type: 'enum', + enum: TemplateType, + comment: '模板类型', }) templateType: TemplateType; @@ -78,19 +88,39 @@ export class N8nTemplateEntity { templateClass: string; /** 图片生成模型 - 用于图片生成的AI模型名称(如DALL-E、Midjourney等) */ - @Column({ name: 'image_model', length: 100, nullable: true, comment: '图片生成模型' }) + @Column({ + name: 'image_model', + length: 100, + nullable: true, + comment: '图片生成模型', + }) imageModel: string; /** 图片生成提示词 - 传递给图片AI模型的提示词模板,支持变量替换 */ - @Column({ name: 'image_prompt', type: 'text', nullable: true, comment: '图片生成提示词' }) + @Column({ + name: 'image_prompt', + type: 'text', + nullable: true, + comment: '图片生成提示词', + }) imagePrompt: string; /** 视频生成模型 - 用于视频生成的AI模型名称(如RunwayML、Stable Video等) */ - @Column({ name: 'video_model', length: 100, nullable: true, comment: '视频生成模型' }) + @Column({ + name: 'video_model', + length: 100, + nullable: true, + comment: '视频生成模型', + }) videoModel: string; /** 视频生成提示词 - 传递给视频AI模型的提示词模板,支持变量替换 */ - @Column({ name: 'video_prompt', type: 'text', nullable: true, comment: '视频生成提示词' }) + @Column({ + name: 'video_prompt', + type: 'text', + nullable: true, + comment: '视频生成提示词', + }) videoPrompt: string; /** 视频时长 - 生成视频的持续时间(秒),用于控制生成视频的长度 */ @@ -98,7 +128,12 @@ export class N8nTemplateEntity { duration: number; /** 视频宽高比 - 视频的纵横比例(如16:9、9:16、1:1),影响视频尺寸 */ - @Column({ name: 'aspect_ratio', length: 20, nullable: true, comment: '视频比例' }) + @Column({ + name: 'aspect_ratio', + length: 20, + nullable: true, + comment: '视频比例', + }) aspectRatio: string; /** 激活状态 - 模板是否可用,用于控制模板的上下线状态 */ @@ -116,4 +151,4 @@ export class N8nTemplateEntity { /** 更新时间 - 模板配置的最后修改时间戳 */ @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 index f2eada2..eeb3929 100644 --- a/src/entities/platform-user.entity.ts +++ b/src/entities/platform-user.entity.ts @@ -1,4 +1,13 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, + Index, +} from 'typeorm'; import { UserEntity } from './user.entity'; /** @@ -7,16 +16,16 @@ import { UserEntity } from './user.entity'; * 用于多平台用户身份管理和数据隔离 */ export enum PlatformType { - WECHAT = 'wechat', // 微信小程序 - 腾讯微信生态 - ALIPAY = 'alipay', // 支付宝小程序 - 蚂蚁金服生态 - BAIDU = 'baidu', // 百度智能小程序 - 百度生态 + 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应用 - 原生移动应用 + JD = 'jd', // 京东小程序 - 京东购物生态 + QQ = 'qq', // QQ小程序 - 腾讯QQ生态 + FEISHU = 'feishu', // 飞书小程序 - 企业办公生态 + KUAISHOU = 'kuaishou', // 快手小程序 - 快手短视频生态 + H5 = 'h5', // H5网页版 - 浏览器端应用 + RN = 'rn', // React Native应用 - 原生移动应用 } /** @@ -39,7 +48,7 @@ export class PlatformUserEntity { /** 平台类型 - 标识用户所属的小程序平台 */ @Column({ type: 'enum', - enum: PlatformType + enum: PlatformType, }) platform: PlatformType; @@ -57,11 +66,11 @@ export class PlatformUserEntity { /** 刷新令牌 - 用于刷新过期的访问令牌,维持长期授权状态 */ @Column({ length: 500, nullable: true }) - refreshToken: string; + refreshToken: string | null; /** 令牌过期时间 - 访问令牌的过期时间戳,用于判断是否需要刷新令牌 */ @Column({ type: 'timestamp', nullable: true }) - expiresAt: Date; + expiresAt: Date | null; /** 创建时间 - 平台用户绑定的创建时间戳 */ @CreateDateColumn() @@ -72,7 +81,9 @@ export class PlatformUserEntity { updatedAt: Date; /** 用户关系 - 与统一用户实体的多对一关系,级联删除 */ - @ManyToOne(() => UserEntity, user => user.platformUsers, { onDelete: 'CASCADE' }) + @ManyToOne(() => UserEntity, (user) => user.platformUsers, { + onDelete: 'CASCADE', + }) @JoinColumn({ name: 'userId' }) user: UserEntity; -} \ No newline at end of file +} diff --git a/src/entities/template-execution.entity.ts b/src/entities/template-execution.entity.ts index 94961c8..ffdd5c5 100644 --- a/src/entities/template-execution.entity.ts +++ b/src/entities/template-execution.entity.ts @@ -1,4 +1,13 @@ -import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn, Index } from 'typeorm'; +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, + Index, +} from 'typeorm'; import { N8nTemplateEntity } from './n8n-template.entity'; import { PlatformType } from './platform-user.entity'; @@ -7,11 +16,11 @@ import { PlatformType } from './platform-user.entity'; * 定义AI内容生成任务的生命周期状态 */ export enum ExecutionStatus { - PENDING = 'pending', // 待处理 - 任务已提交,等待执行 + PENDING = 'pending', // 待处理 - 任务已提交,等待执行 PROCESSING = 'processing', // 处理中 - 任务正在执行,AI模型生成中 - COMPLETED = 'completed', // 已完成 - 任务成功完成,内容已生成 - FAILED = 'failed', // 执行失败 - 任务执行出错,生成失败 - CANCELLED = 'cancelled' // 已取消 - 用户主动取消或系统超时取消 + COMPLETED = 'completed', // 已完成 - 任务成功完成,内容已生成 + FAILED = 'failed', // 执行失败 - 任务执行出错,生成失败 + CANCELLED = 'cancelled', // 已取消 - 用户主动取消或系统超时取消 } /** @@ -19,8 +28,8 @@ export enum ExecutionStatus { * 定义模板执行的内容输出类型 */ export enum ExecutionType { - IMAGE = 'image', // 图片生成 - 输出图片内容 - VIDEO = 'video' // 视频生成 - 输出视频内容 + IMAGE = 'image', // 图片生成 - 输出图片内容 + VIDEO = 'video', // 视频生成 - 输出视频内容 } /** @@ -30,12 +39,12 @@ export enum ExecutionType { * 支持图片和视频两种类型的AI生成任务追踪 */ @Entity('template_executions') -@Index(['templateId']) // 模板ID索引 - 快速查询特定模板的执行记录 -@Index(['userId']) // 用户ID索引 - 快速查询用户的执行历史 -@Index(['platform']) // 平台索引 - 按平台筛选执行记录 -@Index(['status']) // 状态索引 - 快速筛选不同状态的任务 -@Index(['type']) // 类型索引 - 按执行类型(图片/视频)筛选 -@Index(['creditTransactionId']) // 积分交易ID索引 - 关联积分消费记录 +@Index(['templateId']) // 模板ID索引 - 快速查询特定模板的执行记录 +@Index(['userId']) // 用户ID索引 - 快速查询用户的执行历史 +@Index(['platform']) // 平台索引 - 按平台筛选执行记录 +@Index(['status']) // 状态索引 - 快速筛选不同状态的任务 +@Index(['type']) // 类型索引 - 按执行类型(图片/视频)筛选 +@Index(['creditTransactionId']) // 积分交易ID索引 - 关联积分消费记录 export class TemplateExecutionEntity { /** 主键 - 模板执行记录的唯一标识符 */ @PrimaryGeneratedColumn() @@ -74,7 +83,11 @@ export class TemplateExecutionEntity { thumbnailUrl: string; /** 执行状态 - 当前任务的处理状态 */ - @Column({ type: 'enum', enum: ExecutionStatus, default: ExecutionStatus.PENDING }) + @Column({ + type: 'enum', + enum: ExecutionStatus, + default: ExecutionStatus.PENDING, + }) status: ExecutionStatus; /** 执行进度 - 任务完成百分比(0-100),用于显示进度条 */ @@ -129,4 +142,4 @@ export class TemplateExecutionEntity { @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 index 8f3f339..cd2acce 100644 --- a/src/entities/user-credit.entity.ts +++ b/src/entities/user-credit.entity.ts @@ -1,4 +1,12 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; import { UserEntity } from './user.entity'; import { PlatformType } from './platform-user.entity'; @@ -7,10 +15,10 @@ import { PlatformType } from './platform-user.entity'; * 定义用户积分账户的不同变动操作类型 */ export enum CreditType { - REWARD = 'reward', // 奖励获得 - 用户通过完成任务获得积分 - CONSUME = 'consume', // 消费扣除 - 用户使用功能消耗积分 - PURCHASE = 'purchase', // 购买获得 - 用户付费购买积分 - REFUND = 'refund' // 退款返还 - 服务异常或用户申请退款 + REWARD = 'reward', // 奖励获得 - 用户通过完成任务获得积分 + CONSUME = 'consume', // 消费扣除 - 用户使用功能消耗积分 + PURCHASE = 'purchase', // 购买获得 - 用户付费购买积分 + REFUND = 'refund', // 退款返还 - 服务异常或用户申请退款 } /** @@ -19,10 +27,10 @@ export enum CreditType { * 用于积分流水分析和运营统计 */ export enum CreditSource { - AD_WATCH = 'ad_watch', // 看广告 - 用户观看激励广告获得积分 - SUBSCRIPTION = 'subscription', // 订阅赠送 - 订阅会员获得的积分奖励 - AI_GENERATION = 'ai_generation', // AI生成消费 - 使用AI功能消耗积分 - MANUAL = 'manual' // 手动调整 - 管理员手动调整用户积分 + AD_WATCH = 'ad_watch', // 看广告 - 用户观看激励广告获得积分 + SUBSCRIPTION = 'subscription', // 订阅赠送 - 订阅会员获得的积分奖励 + AI_GENERATION = 'ai_generation', // AI生成消费 - 使用AI功能消耗积分 + MANUAL = 'manual', // 手动调整 - 管理员手动调整用户积分 } /** @@ -44,21 +52,21 @@ export class UserCreditEntity { /** 平台类型 - 标识积分变动发生的平台,支持多平台积分管理 */ @Column({ type: 'enum', - enum: PlatformType + enum: PlatformType, }) platform: PlatformType; /** 变动类型 - 积分的操作类型(奖励、消费、购买、退款) */ @Column({ type: 'enum', - enum: CreditType + enum: CreditType, }) type: CreditType; /** 积分来源 - 标识积分变动的具体业务来源,用于数据分析和运营决策 */ @Column({ type: 'enum', - enum: CreditSource + enum: CreditSource, }) source: CreditSource; @@ -91,7 +99,7 @@ export class UserCreditEntity { updatedAt: Date; /** 用户关系 - 与用户实体的多对一关系,级联删除 */ - @ManyToOne(() => UserEntity, user => user.credits, { onDelete: 'CASCADE' }) + @ManyToOne(() => UserEntity, (user) => user.credits, { onDelete: 'CASCADE' }) @JoinColumn({ name: 'userId' }) 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 e6edd95..a57a4e0 100644 --- a/src/entities/user-subscription.entity.ts +++ b/src/entities/user-subscription.entity.ts @@ -1,4 +1,12 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + ManyToOne, + JoinColumn, +} from 'typeorm'; import { UserEntity } from './user.entity'; import { PlatformType } from './platform-user.entity'; @@ -8,9 +16,9 @@ import { PlatformType } from './platform-user.entity'; * 每个套餐提供不同的功能权益和积分额度 */ export enum SubscriptionPlan { - BASIC = 'basic', // 基础版 - 提供基础AI生成功能和有限积分 - PREMIUM = 'premium', // 高级版 - 增加更多积分和高级功能访问权 - PRO = 'pro' // 专业版 - 提供最高级的功能和无限积分使用 + BASIC = 'basic', // 基础版 - 提供基础AI生成功能和有限积分 + PREMIUM = 'premium', // 高级版 - 增加更多积分和高级功能访问权 + PRO = 'pro', // 专业版 - 提供最高级的功能和无限积分使用 } /** @@ -18,10 +26,10 @@ export enum SubscriptionPlan { * 定义用户订阅的当前状态 */ export enum SubscriptionStatus { - ACTIVE = 'active', // 激活中 - 订阅正常生效,用户可享受所有权益 - EXPIRED = 'expired', // 已过期 - 订阅已过期,需要续费或降级为免费版 + ACTIVE = 'active', // 激活中 - 订阅正常生效,用户可享受所有权益 + EXPIRED = 'expired', // 已过期 - 订阅已过期,需要续费或降级为免费版 CANCELLED = 'cancelled', // 已取消 - 用户主动取消订阅,不再自动续费 - PENDING = 'pending' // 待激活 - 支付完成但尚未激活的订阅 + PENDING = 'pending', // 待激活 - 支付完成但尚未激活的订阅 } /** @@ -43,14 +51,14 @@ export class UserSubscriptionEntity { /** 订阅平台 - 订阅服务购买的平台来源 */ @Column({ type: 'enum', - enum: PlatformType + enum: PlatformType, }) platform: PlatformType; /** 订阅套餐 - 用户选择的会员级别(基础、高级、专业) */ @Column({ type: 'enum', - enum: SubscriptionPlan + enum: SubscriptionPlan, }) plan: SubscriptionPlan; @@ -58,7 +66,7 @@ export class UserSubscriptionEntity { @Column({ type: 'enum', enum: SubscriptionStatus, - default: SubscriptionStatus.PENDING + default: SubscriptionStatus.PENDING, }) status: SubscriptionStatus; @@ -103,7 +111,9 @@ export class UserSubscriptionEntity { updatedAt: Date; /** 用户关系 - 与用户实体的多对一关系,级联删除 */ - @ManyToOne(() => UserEntity, user => user.subscriptions, { onDelete: 'CASCADE' }) + @ManyToOne(() => UserEntity, (user) => user.subscriptions, { + onDelete: 'CASCADE', + }) @JoinColumn({ name: 'userId' }) user: UserEntity; -} \ No newline at end of file +} diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index f14bc7c..044304d 100644 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -1,4 +1,11 @@ -import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + CreateDateColumn, + UpdateDateColumn, + OneToMany, +} from 'typeorm'; import { PlatformUserEntity } from './platform-user.entity'; import { ExtensionDataEntity } from './extension-data.entity'; import { UserCreditEntity } from './user-credit.entity'; @@ -51,26 +58,29 @@ export class UserEntity { updatedAt: Date; /** 平台用户列表 - 用户在各个小程序平台的身份信息和授权令牌 */ - @OneToMany(() => PlatformUserEntity, platformUser => platformUser.user) + @OneToMany(() => PlatformUserEntity, (platformUser) => platformUser.user) platformUsers: PlatformUserEntity[]; /** 扩展数据列表 - 用户在各平台的特有业务数据和第三方集成数据 */ - @OneToMany(() => ExtensionDataEntity, extensionData => extensionData.user) + @OneToMany(() => ExtensionDataEntity, (extensionData) => extensionData.user) extensionData: ExtensionDataEntity[]; /** 积分记录列表 - 用户的所有积分变动明细和交易流水 */ - @OneToMany(() => UserCreditEntity, userCredit => userCredit.user) + @OneToMany(() => UserCreditEntity, (userCredit) => userCredit.user) credits: UserCreditEntity[]; /** 模板执行列表 - 用户使用AI模板生成内容的历史记录 */ - @OneToMany(() => TemplateExecutionEntity, execution => execution.userId) + @OneToMany(() => TemplateExecutionEntity, (execution) => execution.userId) templateExecutions: TemplateExecutionEntity[]; /** 订阅列表 - 用户的会员订阅服务信息和历史记录 */ - @OneToMany(() => UserSubscriptionEntity, userSubscription => userSubscription.user) + @OneToMany( + () => UserSubscriptionEntity, + (userSubscription) => userSubscription.user, + ) subscriptions: UserSubscriptionEntity[]; /** 广告观看列表 - 用户观看广告的完整记录和奖励明细 */ - @OneToMany(() => AdWatchEntity, adWatch => adWatch.user) + @OneToMany(() => AdWatchEntity, (adWatch) => adWatch.user) adWatches: AdWatchEntity[]; -} \ No newline at end of file +} diff --git a/src/migrations/1725434673000-CreateN8nTemplatesTable.ts b/src/migrations/1725434673000-CreateN8nTemplatesTable.ts index 103e146..87e8fc4 100644 --- a/src/migrations/1725434673000-CreateN8nTemplatesTable.ts +++ b/src/migrations/1725434673000-CreateN8nTemplatesTable.ts @@ -1,6 +1,8 @@ import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; -export class CreateN8nTemplatesTable1725434673000 implements MigrationInterface { +export class CreateN8nTemplatesTable1725434673000 + implements MigrationInterface +{ name = 'CreateN8nTemplatesTable1725434673000'; public async up(queryRunner: QueryRunner): Promise { @@ -14,7 +16,7 @@ export class CreateN8nTemplatesTable1725434673000 implements MigrationInterface isPrimary: true, isGenerated: true, generationStrategy: 'increment', - comment: '主键ID' + comment: '主键ID', }, { name: 'code', @@ -22,126 +24,126 @@ export class CreateN8nTemplatesTable1725434673000 implements MigrationInterface length: '100', isUnique: true, isNullable: false, - comment: '模板唯一标识码' + comment: '模板唯一标识码', }, { name: 'name', type: 'varchar', length: '200', isNullable: false, - comment: '模板名称' + comment: '模板名称', }, { name: 'description', type: 'text', isNullable: true, - comment: '模板详细描述' + comment: '模板详细描述', }, { name: 'credit_cost', type: 'int', default: 0, isNullable: false, - comment: '积分消耗' + comment: '积分消耗', }, { name: 'version', type: 'varchar', length: '50', isNullable: false, - comment: '版本号' + comment: '版本号', }, { name: 'input_example_url', type: 'text', isNullable: true, - comment: '输入示例图片' + comment: '输入示例图片', }, { name: 'output_example_url', type: 'text', isNullable: true, - comment: '输出示例图片' + comment: '输出示例图片', }, { name: 'tags', type: 'json', isNullable: true, - comment: '标签数组' + comment: '标签数组', }, { name: 'template_type', type: 'enum', enum: ['image', 'video'], isNullable: false, - comment: '模板类型' + comment: '模板类型', }, { name: 'template_class', type: 'varchar', length: '100', isNullable: false, - comment: '具体模板类名' + comment: '具体模板类名', }, { name: 'image_model', type: 'varchar', length: '100', isNullable: true, - comment: '图片生成模型' + comment: '图片生成模型', }, { name: 'image_prompt', type: 'text', isNullable: true, - comment: '图片生成提示词' + comment: '图片生成提示词', }, { name: 'video_model', type: 'varchar', length: '100', isNullable: true, - comment: '视频生成模型' + comment: '视频生成模型', }, { name: 'video_prompt', type: 'text', isNullable: true, - comment: '视频生成提示词' + comment: '视频生成提示词', }, { name: 'duration', type: 'int', isNullable: true, - comment: '视频时长(秒)' + comment: '视频时长(秒)', }, { name: 'aspect_ratio', type: 'varchar', length: '20', isNullable: true, - comment: '视频比例' + comment: '视频比例', }, { name: 'is_active', type: 'boolean', default: true, isNullable: false, - comment: '是否启用' + comment: '是否启用', }, { name: 'sort_order', type: 'int', default: 0, isNullable: false, - comment: '排序权重' + comment: '排序权重', }, { name: 'created_at', type: 'timestamp', default: 'CURRENT_TIMESTAMP', isNullable: false, - comment: '创建时间' + comment: '创建时间', }, { name: 'updated_at', @@ -149,33 +151,45 @@ export class CreateN8nTemplatesTable1725434673000 implements MigrationInterface default: 'CURRENT_TIMESTAMP', onUpdate: 'CURRENT_TIMESTAMP', isNullable: false, - comment: '更新时间' - } - ] + comment: '更新时间', + }, + ], }), - true + true, ); // 创建索引 - await queryRunner.createIndex('n8n_templates', new TableIndex({ - name: 'idx_code', - columnNames: ['code'] - })); + await queryRunner.createIndex( + 'n8n_templates', + new TableIndex({ + name: 'idx_code', + columnNames: ['code'], + }), + ); - await queryRunner.createIndex('n8n_templates', new TableIndex({ - name: 'idx_type', - columnNames: ['template_type'] - })); + await queryRunner.createIndex( + 'n8n_templates', + new TableIndex({ + name: 'idx_type', + columnNames: ['template_type'], + }), + ); - await queryRunner.createIndex('n8n_templates', new TableIndex({ - name: 'idx_class', - columnNames: ['template_class'] - })); + await queryRunner.createIndex( + 'n8n_templates', + new TableIndex({ + name: 'idx_class', + columnNames: ['template_class'], + }), + ); - await queryRunner.createIndex('n8n_templates', new TableIndex({ - name: 'idx_active', - columnNames: ['is_active'] - })); + await queryRunner.createIndex( + 'n8n_templates', + new TableIndex({ + name: 'idx_active', + columnNames: ['is_active'], + }), + ); console.log('✅ N8n Templates table created successfully'); } @@ -187,4 +201,4 @@ export class CreateN8nTemplatesTable1725434673000 implements MigrationInterface await queryRunner.dropIndex('n8n_templates', 'idx_code'); await queryRunner.dropTable('n8n_templates'); } -} \ No newline at end of file +} diff --git a/src/migrations/1725434680000-AddPhotoRestoreTemplate.ts b/src/migrations/1725434680000-AddPhotoRestoreTemplate.ts index 705f473..f88cb3a 100644 --- a/src/migrations/1725434680000-AddPhotoRestoreTemplate.ts +++ b/src/migrations/1725434680000-AddPhotoRestoreTemplate.ts @@ -1,6 +1,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddPhotoRestoreTemplate1725434680000 implements MigrationInterface { +export class AddPhotoRestoreTemplate1725434680000 + implements MigrationInterface +{ name = 'AddPhotoRestoreTemplate1725434680000'; public async up(queryRunner: QueryRunner): Promise { @@ -44,4 +46,4 @@ export class AddPhotoRestoreTemplate1725434680000 implements MigrationInterface console.log('⏭️ PhotoRestoreTemplate migration reverted'); } -} \ No newline at end of file +} diff --git a/src/migrations/1725434690000-AddCharacterFigurineTemplate.ts b/src/migrations/1725434690000-AddCharacterFigurineTemplate.ts index 6210be2..9595e7c 100644 --- a/src/migrations/1725434690000-AddCharacterFigurineTemplate.ts +++ b/src/migrations/1725434690000-AddCharacterFigurineTemplate.ts @@ -1,6 +1,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddCharacterFigurineTemplate1725434690000 implements MigrationInterface { +export class AddCharacterFigurineTemplate1725434690000 + implements MigrationInterface +{ name = 'AddCharacterFigurineTemplate1725434690000'; public async up(queryRunner: QueryRunner): Promise { @@ -44,4 +46,4 @@ export class AddCharacterFigurineTemplate1725434690000 implements MigrationInter console.log('⏭️ CharacterFigurineTemplate migration reverted'); } -} \ No newline at end of file +} diff --git a/src/migrations/1725434700000-AddPetFigurineTemplate.ts b/src/migrations/1725434700000-AddPetFigurineTemplate.ts index d7f9e7f..a5f0ec6 100644 --- a/src/migrations/1725434700000-AddPetFigurineTemplate.ts +++ b/src/migrations/1725434700000-AddPetFigurineTemplate.ts @@ -44,4 +44,4 @@ export class AddPetFigurineTemplate1725434700000 implements MigrationInterface { console.log('⏭️ PetFigurineTemplate migration reverted'); } -} \ No newline at end of file +} diff --git a/src/migrations/1725434710000-AddCosplayRealPersonTemplate.ts b/src/migrations/1725434710000-AddCosplayRealPersonTemplate.ts index a092431..d66b6ec 100644 --- a/src/migrations/1725434710000-AddCosplayRealPersonTemplate.ts +++ b/src/migrations/1725434710000-AddCosplayRealPersonTemplate.ts @@ -1,6 +1,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddCosplayRealPersonTemplate1725434710000 implements MigrationInterface { +export class AddCosplayRealPersonTemplate1725434710000 + implements MigrationInterface +{ name = 'AddCosplayRealPersonTemplate1725434710000'; public async up(queryRunner: QueryRunner): Promise { @@ -44,4 +46,4 @@ export class AddCosplayRealPersonTemplate1725434710000 implements MigrationInter console.log('⏭️ CosplayRealPersonTemplate migration reverted'); } -} \ No newline at end of file +} diff --git a/src/migrations/1725434720000-AddGarageOpeningTemplate.ts b/src/migrations/1725434720000-AddGarageOpeningTemplate.ts index 7c0f544..ac47be0 100644 --- a/src/migrations/1725434720000-AddGarageOpeningTemplate.ts +++ b/src/migrations/1725434720000-AddGarageOpeningTemplate.ts @@ -1,6 +1,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddGarageOpeningTemplate1725434720000 implements MigrationInterface { +export class AddGarageOpeningTemplate1725434720000 + implements MigrationInterface +{ name = 'AddGarageOpeningTemplate1725434720000'; public async up(queryRunner: QueryRunner): Promise { @@ -44,4 +46,4 @@ export class AddGarageOpeningTemplate1725434720000 implements MigrationInterface console.log('⏭️ GarageOpeningTemplate migration reverted'); } -} \ No newline at end of file +} diff --git a/src/migrations/1725434730000-AddJapaneseMagazineTemplate.ts b/src/migrations/1725434730000-AddJapaneseMagazineTemplate.ts index 0639f72..35dd9f1 100644 --- a/src/migrations/1725434730000-AddJapaneseMagazineTemplate.ts +++ b/src/migrations/1725434730000-AddJapaneseMagazineTemplate.ts @@ -1,6 +1,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddJapaneseMagazineTemplate1725434730000 implements MigrationInterface { +export class AddJapaneseMagazineTemplate1725434730000 + implements MigrationInterface +{ name = 'AddJapaneseMagazineTemplate1725434730000'; public async up(queryRunner: QueryRunner): Promise { @@ -44,4 +46,4 @@ export class AddJapaneseMagazineTemplate1725434730000 implements MigrationInterf console.log('⏭️ JapaneseMagazineTemplate migration reverted'); } -} \ No newline at end of file +} diff --git a/src/migrations/1725434740000-AddNuclearExplosionTemplate.ts b/src/migrations/1725434740000-AddNuclearExplosionTemplate.ts index bc34aa4..2069a55 100644 --- a/src/migrations/1725434740000-AddNuclearExplosionTemplate.ts +++ b/src/migrations/1725434740000-AddNuclearExplosionTemplate.ts @@ -1,6 +1,8 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AddNuclearExplosionTemplate1725434740000 implements MigrationInterface { +export class AddNuclearExplosionTemplate1725434740000 + implements MigrationInterface +{ name = 'AddNuclearExplosionTemplate1725434740000'; public async up(queryRunner: QueryRunner): Promise { @@ -44,4 +46,4 @@ export class AddNuclearExplosionTemplate1725434740000 implements MigrationInterf console.log('⏭️ NuclearExplosionTemplate migration reverted'); } -} \ No newline at end of file +} diff --git a/src/migrations/1725434750000-AddOpenEyesTemplate.ts b/src/migrations/1725434750000-AddOpenEyesTemplate.ts index f03f3a1..09d2840 100644 --- a/src/migrations/1725434750000-AddOpenEyesTemplate.ts +++ b/src/migrations/1725434750000-AddOpenEyesTemplate.ts @@ -44,4 +44,4 @@ export class AddOpenEyesTemplate1725434750000 implements MigrationInterface { console.log('⏭️ OpenEyesTemplate migration reverted'); } -} \ No newline at end of file +} diff --git a/src/migrations/1725501000000-CreatePlatformTypeEnum.ts b/src/migrations/1725501000000-CreatePlatformTypeEnum.ts index f62b097..80d3e35 100644 --- a/src/migrations/1725501000000-CreatePlatformTypeEnum.ts +++ b/src/migrations/1725501000000-CreatePlatformTypeEnum.ts @@ -17,4 +17,4 @@ export class CreatePlatformTypeEnum1725501000000 implements MigrationInterface { 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 index 8331544..e584ae0 100644 --- a/src/migrations/1725502000000-CreateUserTable.ts +++ b/src/migrations/1725502000000-CreateUserTable.ts @@ -30,4 +30,4 @@ export class CreateUserTable1725502000000 implements MigrationInterface { 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 index 5654548..a7fa1d6 100644 --- a/src/migrations/1725503000000-CreatePlatformUserTable.ts +++ b/src/migrations/1725503000000-CreatePlatformUserTable.ts @@ -5,7 +5,9 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; * 管理用户在不同小程序平台的身份信息和授权令牌 * 实现统一用户体系下的多平台身份绑定 */ -export class CreatePlatformUserTable1725503000000 implements MigrationInterface { +export class CreatePlatformUserTable1725503000000 + implements MigrationInterface +{ name = 'CreatePlatformUserTable1725503000000'; public async up(queryRunner: QueryRunner): Promise { @@ -32,4 +34,4 @@ export class CreatePlatformUserTable1725503000000 implements MigrationInterface 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 index 50a757a..0b6dcdf 100644 --- a/src/migrations/1725504000000-CreateExtensionDataTable.ts +++ b/src/migrations/1725504000000-CreateExtensionDataTable.ts @@ -5,7 +5,9 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; * 用于存储各平台特有的业务数据和第三方集成数据 * 提供灵活的JSON存储机制,支持动态数据结构 */ -export class CreateExtensionDataTable1725504000000 implements MigrationInterface { +export class CreateExtensionDataTable1725504000000 + implements MigrationInterface +{ name = 'CreateExtensionDataTable1725504000000'; public async up(queryRunner: QueryRunner): Promise { @@ -31,4 +33,4 @@ export class CreateExtensionDataTable1725504000000 implements MigrationInterface 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 index c144a48..253e89f 100644 --- a/src/migrations/1725505000000-CreateUserCreditTable.ts +++ b/src/migrations/1725505000000-CreateUserCreditTable.ts @@ -35,4 +35,4 @@ export class CreateUserCreditTable1725505000000 implements MigrationInterface { await queryRunner.query(`DROP TABLE \`user_credits\``); // MySQL 中无需单独删除 ENUM 类型,它们与表一起被删除 } -} \ No newline at end of file +} diff --git a/src/migrations/1725506000000-CreateTemplateExecutionTable.ts b/src/migrations/1725506000000-CreateTemplateExecutionTable.ts index cbc4516..e757ed6 100644 --- a/src/migrations/1725506000000-CreateTemplateExecutionTable.ts +++ b/src/migrations/1725506000000-CreateTemplateExecutionTable.ts @@ -5,7 +5,9 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; * 记录用户使用AI模板生成内容的完整执行过程 * 包含执行状态、性能指标、积分消耗和结果数据 */ -export class CreateTemplateExecutionTable1725506000000 implements MigrationInterface { +export class CreateTemplateExecutionTable1725506000000 + implements MigrationInterface +{ name = 'CreateTemplateExecutionTable1725506000000'; public async up(queryRunner: QueryRunner): Promise { @@ -44,4 +46,4 @@ export class CreateTemplateExecutionTable1725506000000 implements MigrationInter await queryRunner.query(`DROP TABLE \`template_executions\``); // MySQL 中无需单独删除 ENUM 类型,它们与表一起被删除 } -} \ No newline at end of file +} diff --git a/src/migrations/1725507000000-CreateUserSubscriptionTable.ts b/src/migrations/1725507000000-CreateUserSubscriptionTable.ts index 591de82..f32e900 100644 --- a/src/migrations/1725507000000-CreateUserSubscriptionTable.ts +++ b/src/migrations/1725507000000-CreateUserSubscriptionTable.ts @@ -5,7 +5,9 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; * 管理用户的会员订阅服务信息 * 支持多平台订阅和自动续费管理 */ -export class CreateUserSubscriptionTable1725507000000 implements MigrationInterface { +export class CreateUserSubscriptionTable1725507000000 + implements MigrationInterface +{ name = 'CreateUserSubscriptionTable1725507000000'; public async up(queryRunner: QueryRunner): Promise { @@ -38,4 +40,4 @@ export class CreateUserSubscriptionTable1725507000000 implements MigrationInterf await queryRunner.query(`DROP TABLE \`user_subscriptions\``); // MySQL 中无需单独删除 ENUM 类型,它们与表一起被删除 } -} \ No newline at end of file +} diff --git a/src/migrations/1725508000000-CreateAdWatchTable.ts b/src/migrations/1725508000000-CreateAdWatchTable.ts index 57dcc28..27b7f18 100644 --- a/src/migrations/1725508000000-CreateAdWatchTable.ts +++ b/src/migrations/1725508000000-CreateAdWatchTable.ts @@ -35,4 +35,4 @@ export class CreateAdWatchTable1725508000000 implements MigrationInterface { await queryRunner.query(`DROP TABLE \`ad_watches\``); // MySQL 中无需单独删除 ENUM 类型,它们与表一起被删除 } -} \ No newline at end of file +} diff --git a/src/migrations/1725509000000-CreateTableIndexes.ts b/src/migrations/1725509000000-CreateTableIndexes.ts index 72bd5a9..ad2a56a 100644 --- a/src/migrations/1725509000000-CreateTableIndexes.ts +++ b/src/migrations/1725509000000-CreateTableIndexes.ts @@ -14,7 +14,7 @@ export class CreateTableIndexes1725509000000 implements MigrationInterface { // 平台用户表索引 - 优化多平台身份查询性能 `CREATE INDEX \`IDX_platform_users_platform\` ON \`platform_users\` (\`platform\`)`, `CREATE INDEX \`IDX_platform_users_userId\` ON \`platform_users\` (\`userId\`)`, - + // 扩展数据表索引 - 优化业务数据查询和状态筛选 `CREATE INDEX \`IDX_extension_data_userId\` ON \`extension_data\` (\`userId\`)`, `CREATE INDEX \`IDX_extension_data_platform\` ON \`extension_data\` (\`platform\`)`, @@ -50,7 +50,7 @@ export class CreateTableIndexes1725509000000 implements MigrationInterface { `CREATE INDEX \`IDX_ad_watches_platform\` ON \`ad_watches\` (\`platform\`)`, `CREATE INDEX \`IDX_ad_watches_adType\` ON \`ad_watches\` (\`adType\`)`, `CREATE INDEX \`IDX_ad_watches_status\` ON \`ad_watches\` (\`status\`)`, - `CREATE INDEX \`IDX_ad_watches_createdAt\` ON \`ad_watches\` (\`createdAt\`)` + `CREATE INDEX \`IDX_ad_watches_createdAt\` ON \`ad_watches\` (\`createdAt\`)`, ]; // 安全创建索引 - 忽略已存在的索引错误 @@ -82,12 +82,16 @@ export class CreateTableIndexes1725509000000 implements MigrationInterface { // 删除模板执行表索引 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_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_template_executions_templateId\``, + ); // 删除用户积分表索引 await queryRunner.query(`DROP INDEX \`IDX_user_credits_createdAt\``); @@ -108,4 +112,4 @@ export class CreateTableIndexes1725509000000 implements MigrationInterface { 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/platform/adapters/base.adapter.ts b/src/platform/adapters/base.adapter.ts index ffd111a..bbf0905 100644 --- a/src/platform/adapters/base.adapter.ts +++ b/src/platform/adapters/base.adapter.ts @@ -1,24 +1,27 @@ -import { Injectable, BadRequestException } from '@nestjs/common'; +import { Injectable } 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, +import { + PlatformUserEntity, + PlatformType, +} from '../../entities/platform-user.entity'; +import { + IPlatformAdapter, + PlatformLoginData, PlatformRegisterData, - UserAuthResult, - PlatformUserInfo, + UserAuthResult, + PlatformUserInfo, TokenRefreshResult, - UnifiedUserInfo + UnifiedUserInfo, } from '../interfaces/platform.interface'; @Injectable() export abstract class BaseAdapter implements IPlatformAdapter { abstract platform: PlatformType; - + constructor( protected readonly httpService: HttpService, protected readonly configService: ConfigService, @@ -31,7 +34,9 @@ export abstract class BaseAdapter implements IPlatformAdapter { * 查找或创建统一用户 * 根据平台用户信息查找现有用户,如不存在则创建新用户 */ - async findOrCreateUnifiedUser(platformUserData: PlatformUserInfo): Promise { + async findOrCreateUnifiedUser( + platformUserData: PlatformUserInfo, + ): Promise { // 查找是否已存在平台用户绑定 const existingPlatformUser = await this.platformUserRepository.findOne({ where: { @@ -50,20 +55,22 @@ export abstract class BaseAdapter implements IPlatformAdapter { // 创建新的统一用户 const unifiedUser = await this.createUnifiedUser(platformUserData); await this.createPlatformUser(unifiedUser.id, platformUserData); - + return unifiedUser; } /** * 创建统一用户记录 */ - protected async createUnifiedUser(platformData: PlatformUserInfo): Promise { + 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.phone = platformData.phone!; + user.email = platformData.email!; user.status = 1; // 正常状态 return this.userRepository.save(user); } @@ -71,17 +78,23 @@ export abstract class BaseAdapter implements IPlatformAdapter { /** * 创建平台用户绑定记录 */ - protected async createPlatformUser(userId: string, platformData: PlatformUserInfo, authData?: any): Promise { + 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; + platformUser.expiresAt = authData.expires_at + ? new Date(authData.expires_at * 1000) + : null; } return this.platformUserRepository.save(platformUser); @@ -90,7 +103,10 @@ export abstract class BaseAdapter implements IPlatformAdapter { /** * 更新平台用户信息 */ - protected async updatePlatformUserInfo(platformUser: PlatformUserEntity, newData: PlatformUserInfo): Promise { + protected async updatePlatformUserInfo( + platformUser: PlatformUserEntity, + newData: PlatformUserInfo, + ): Promise { platformUser.platformData = { ...platformUser.platformData, ...newData }; await this.platformUserRepository.save(platformUser); } @@ -98,9 +114,13 @@ export abstract class BaseAdapter implements IPlatformAdapter { /** * 更新平台用户认证数据 */ - protected async updatePlatformUserData(userId: string, authData: any, userInfo?: any): Promise { + protected async updatePlatformUserData( + userId: string, + authData: any, + userInfo?: any, + ): Promise { const platformUser = await this.platformUserRepository.findOne({ - where: { userId, platform: this.platform } + where: { userId, platform: this.platform }, }); if (platformUser) { @@ -108,9 +128,12 @@ export abstract class BaseAdapter implements IPlatformAdapter { platformUser.accessToken = authData.session_key; platformUser.refreshToken = null; // 两平台都不支持refresh platformUser.expiresAt = null; // session_key无固定过期时间 - + if (userInfo) { - platformUser.platformData = { ...platformUser.platformData, ...userInfo }; + platformUser.platformData = { + ...platformUser.platformData, + ...userInfo, + }; } await this.platformUserRepository.save(platformUser); @@ -127,7 +150,10 @@ export abstract class BaseAdapter implements IPlatformAdapter { /** * 生成JWT令牌 */ - protected async generateTokens(user: UserEntity, platform: PlatformType): Promise<{ + protected async generateTokens( + user: UserEntity, + platform: PlatformType, + ): Promise<{ accessToken: string; refreshToken: string; expiresAt: Date; @@ -157,7 +183,9 @@ export abstract class BaseAdapter implements IPlatformAdapter { phone: user.phone, email: user.email, status: user.status, - platforms: user.platformUsers?.map(pu => pu.platform) || [this.platform], + platforms: user.platformUsers?.map((pu) => pu.platform) || [ + this.platform, + ], }; } @@ -170,11 +198,15 @@ export abstract class BaseAdapter implements IPlatformAdapter { const errorData = error.response.data; // 微信格式: {errcode, errmsg} if (errorData.errcode) { - return new Error(`${platform}错误[${errorData.errcode}]: ${errorData.errmsg}`); + 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}错误[${errorData.err_no}]: ${errorData.err_tips}`, + ); } } return new Error(`${platform}API调用失败: ${error.message}`); @@ -182,10 +214,18 @@ export abstract class BaseAdapter implements IPlatformAdapter { // 抽象方法 - 子类必须实现 abstract login(loginData: PlatformLoginData): Promise; - abstract getUserInfo(token: string, platformUserId: string): 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 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 +} diff --git a/src/platform/adapters/bytedance.adapter.ts b/src/platform/adapters/bytedance.adapter.ts new file mode 100644 index 0000000..8203d46 --- /dev/null +++ b/src/platform/adapters/bytedance.adapter.ts @@ -0,0 +1,172 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { BaseAdapter } from './base.adapter'; +import { PlatformType } from '../../entities/platform-user.entity'; +import { + PlatformLoginData, + PlatformRegisterData, + UserAuthResult, + PlatformUserInfo, + TokenRefreshResult, +} from '../interfaces/platform.interface'; + +interface BytedanceAuthResponse { + err_no: number; + err_tips: string; + log_id: string; + data: { + session_key: string; + openid: string; + anonymous_openid: string; + unionid: string; + }; +} + +@Injectable() +export class BytedanceAdapter extends BaseAdapter { + platform = PlatformType.BYTEDANCE; + + private readonly bytedanceConfig = { + appId: this.configService.get('BYTEDANCE_APP_ID'), + appSecret: this.configService.get('BYTEDANCE_APP_SECRET'), + }; + + async login(loginData: PlatformLoginData): Promise { + try { + // 1. 使用code换取session_key和openid(一步到位) + const authResult = await this.getBytedanceAuth(loginData.code); + + // 2. 直接使用返回的信息创建用户(无需额外API调用) + const platformUserInfo: PlatformUserInfo = { + platformUserId: authResult.data.openid, + nickname: loginData.userInfo?.nickname || '抖音用户', + avatarUrl: loginData.userInfo?.avatarUrl || '', + unionid: authResult.data.unionid, + anonymousOpenid: authResult.data.anonymous_openid, + }; + + const unifiedUser = await this.findOrCreateUnifiedUser(platformUserInfo); + + // 3. 存储session_key和相关信息(支持匿名用户) + const authData = { + session_key: authResult.data.session_key, + }; + + await this.updatePlatformUserData( + unifiedUser.id, + authData, + platformUserInfo, + ); + + // 4. 生成JWT令牌 + const tokens = await this.generateTokens(unifiedUser, this.platform); + + return { + user: this.formatUnifiedUserInfo(unifiedUser), + tokens, + platformData: authResult.data, + }; + } catch (error) { + const platformError = this.handlePlatformError(error, '抖音'); + throw new BadRequestException(`抖音登录失败: ${platformError.message}`); + } + } + + async register(registerData: PlatformRegisterData): Promise { + // 抖音小程序通常通过login方法完成注册 + return this.login(registerData); + } + + async getUserInfo( + token: string, + platformUserId: string, + ): Promise { + const platformUser = await this.platformUserRepository.findOne({ + where: { + platform: this.platform, + platformUserId, + }, + relations: ['user'], + }); + + if (!platformUser) { + throw new BadRequestException('用户不存在'); + } + + return { + platformUserId: platformUser.platformUserId, + nickname: platformUser.user.nickname, + avatarUrl: platformUser.user.avatarUrl, + phone: platformUser.user.phone, + email: platformUser.user.email, + ...platformUser.platformData, + }; + } + + async updateUserInfo( + userId: string, + updateData: Partial, + ): Promise { + // 更新统一用户信息 + await this.userRepository.update(userId, { + nickname: updateData.nickname, + avatarUrl: updateData.avatarUrl, + phone: updateData.phone, + email: updateData.email, + }); + + // 更新平台用户数据 + const platformUser = await this.platformUserRepository.findOne({ + where: { userId, platform: this.platform }, + }); + + if (platformUser) { + platformUser.platformData = { + ...platformUser.platformData, + ...updateData, + }; + await this.platformUserRepository.save(platformUser); + } + } + + async refreshToken(refreshToken: string): Promise { + // 抖音小程序的session_key需要重新登录获取,不支持刷新 + throw new BadRequestException('抖音平台需要重新登录'); + } + + async validateToken(token: string): Promise { + try { + // 抖音小程序验证session_key有效性 + // 通常通过checkSession或API调用验证 + return true; + } catch (error) { + return false; + } + } + + async revokeToken(token: string): Promise { + // 抖音平台session_key撤销逻辑 + // session_key无需特殊撤销,清除本地存储即可 + } + + /** + * 获取抖音授权信息 + */ + private async getBytedanceAuth(code: string): Promise { + const url = `https://developer.toutiao.com/api/apps/v2/jscode2session`; + const data = { + appid: this.bytedanceConfig.appId, + secret: this.bytedanceConfig.appSecret, + code: code, + }; + + const response = await this.httpService.axiosRef.post(url, data, { + headers: { 'Content-Type': 'application/json' }, + }); + + if (response.data.err_no !== 0) { + throw new Error(`抖音认证失败: ${response.data.err_tips}`); + } + + return response.data; + } +} diff --git a/src/platform/adapters/index.ts b/src/platform/adapters/index.ts new file mode 100644 index 0000000..5f48533 --- /dev/null +++ b/src/platform/adapters/index.ts @@ -0,0 +1,3 @@ +export * from './base.adapter'; +export * from './wechat.adapter'; +export * from './bytedance.adapter'; diff --git a/src/platform/adapters/wechat.adapter.ts b/src/platform/adapters/wechat.adapter.ts new file mode 100644 index 0000000..207eeb7 --- /dev/null +++ b/src/platform/adapters/wechat.adapter.ts @@ -0,0 +1,218 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { BaseAdapter } from './base.adapter'; +import { PlatformType } from '../../entities/platform-user.entity'; +import { + PlatformLoginData, + PlatformRegisterData, + UserAuthResult, + PlatformUserInfo, + TokenRefreshResult, +} from '../interfaces/platform.interface'; + +interface WechatAuthResponse { + openid: string; + session_key: string; + unionid?: string; + errcode?: number; + errmsg?: string; +} + +interface WechatUserInfo { + openId: string; + nickName: string; + gender: number; + language: string; + city: string; + province: string; + country: string; + avatarUrl: string; +} + +@Injectable() +export class WechatAdapter extends BaseAdapter { + platform = PlatformType.WECHAT; + + private readonly wechatConfig = { + appId: this.configService.get('WECHAT_APP_ID'), + appSecret: this.configService.get('WECHAT_APP_SECRET'), + }; + + async login(loginData: PlatformLoginData): Promise { + try { + // 1. 使用code换取session_key和openid + const authResult = await this.getWechatAuth(loginData.code); + + // 2. 解密用户信息(如果提供) + let userInfo: WechatUserInfo; + if (loginData.encryptedData && loginData.iv) { + userInfo = this.decryptWechatUserInfo( + loginData.encryptedData, + loginData.iv, + authResult.session_key, + ); + } else { + // 使用基础信息创建用户 + userInfo = { + openId: authResult.openid, + nickName: loginData.userInfo?.nickName || '微信用户', + avatarUrl: loginData.userInfo?.avatarUrl || '', + gender: loginData.userInfo?.gender || 0, + language: 'zh_CN', + city: '', + province: '', + country: 'CN', + }; + } + + // 3. 创建或查找统一用户 + const platformUserInfo: PlatformUserInfo = { + platformUserId: authResult.openid, + nickname: userInfo.nickName, + avatarUrl: userInfo.avatarUrl, + gender: userInfo.gender, + country: userInfo.country, + province: userInfo.province, + city: userInfo.city, + unionid: authResult.unionid, + }; + + const unifiedUser = await this.findOrCreateUnifiedUser(platformUserInfo); + + // 4. 更新平台用户数据 + await this.updatePlatformUserData(unifiedUser.id, authResult, userInfo); + + // 5. 生成JWT令牌 + const tokens = await this.generateTokens(unifiedUser, this.platform); + + return { + user: this.formatUnifiedUserInfo(unifiedUser), + tokens, + platformData: userInfo, + }; + } catch (error) { + const platformError = this.handlePlatformError(error, '微信'); + throw new BadRequestException(`微信登录失败: ${platformError.message}`); + } + } + + async register(registerData: PlatformRegisterData): Promise { + // 微信小程序通常通过login方法完成注册 + return this.login(registerData); + } + + async getUserInfo( + token: string, + platformUserId: string, + ): Promise { + const platformUser = await this.platformUserRepository.findOne({ + where: { + platform: this.platform, + platformUserId, + }, + relations: ['user'], + }); + + if (!platformUser) { + throw new BadRequestException('用户不存在'); + } + + return { + platformUserId: platformUser.platformUserId, + nickname: platformUser.user.nickname, + avatarUrl: platformUser.user.avatarUrl, + phone: platformUser.user.phone, + email: platformUser.user.email, + ...platformUser.platformData, + }; + } + + async updateUserInfo( + userId: string, + updateData: Partial, + ): Promise { + // 更新统一用户信息 + await this.userRepository.update(userId, { + nickname: updateData.nickname, + avatarUrl: updateData.avatarUrl, + phone: updateData.phone, + email: updateData.email, + }); + + // 更新平台用户数据 + const platformUser = await this.platformUserRepository.findOne({ + where: { userId, platform: this.platform }, + }); + + if (platformUser) { + platformUser.platformData = { + ...platformUser.platformData, + ...updateData, + }; + await this.platformUserRepository.save(platformUser); + } + } + + async refreshToken(refreshToken: string): Promise { + // 微信小程序的session_key通常不支持刷新,需要重新登录 + throw new BadRequestException('微信平台需要重新登录'); + } + + async validateToken(token: string): Promise { + try { + // 验证微信session_key是否有效 + // 通常通过调用微信API验证 + return true; + } catch (error) { + return false; + } + } + + async revokeToken(token: string): Promise { + // 微信平台令牌撤销逻辑 + // 清除本地存储的session_key + } + + /** + * 使用code获取微信授权信息 + */ + private async getWechatAuth(code: string): Promise { + const url = `https://api.weixin.qq.com/sns/jscode2session`; + const params = { + appid: this.wechatConfig.appId, + secret: this.wechatConfig.appSecret, + js_code: code, + grant_type: 'authorization_code', + }; + + const response = await this.httpService.axiosRef.get(url, { params }); + + if (response.data.errcode) { + throw new Error(`微信认证失败: ${response.data.errmsg}`); + } + + return response.data; + } + + /** + * 解密微信用户信息 + */ + private decryptWechatUserInfo( + encryptedData: string, + iv: string, + sessionKey: string, + ): WechatUserInfo { + // 实现微信用户信息解密逻辑 + // 使用crypto模块进行AES解密 + const crypto = require('crypto'); + const decipher = crypto.createDecipheriv( + 'aes-128-cbc', + Buffer.from(sessionKey, 'base64'), + Buffer.from(iv, 'base64'), + ); + + let decrypted = decipher.update(encryptedData, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + + return JSON.parse(decrypted); + } +} diff --git a/src/platform/controllers/unified-user.controller.ts b/src/platform/controllers/unified-user.controller.ts new file mode 100644 index 0000000..e4d783e --- /dev/null +++ b/src/platform/controllers/unified-user.controller.ts @@ -0,0 +1,139 @@ +import { + Controller, + Post, + Body, + Get, + Put, + UseGuards, + Request, + Delete, + Param, +} from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiResponse, + ApiBearerAuth, +} from '@nestjs/swagger'; +import { UnifiedUserService } from '../services/unified-user.service'; +import { + PlatformLoginDto, + UserInfoUpdateDto, + PlatformBindDto, +} from '../dto/platform-login.dto'; +import { PlatformAuthGuard } from '../guards/platform-auth.guard'; +import { PlatformType } from '../../entities/platform-user.entity'; + +@ApiTags('统一用户管理') +@Controller('api/v1/users') +export class UnifiedUserController { + constructor(private readonly unifiedUserService: UnifiedUserService) {} + + @Post('login') + @ApiOperation({ summary: '统一登录接口' }) + @ApiResponse({ status: 200, description: '登录成功' }) + @ApiResponse({ status: 400, description: '请求参数错误' }) + @ApiResponse({ status: 401, description: '授权失败' }) + async login(@Body() loginDto: PlatformLoginDto) { + const result = await this.unifiedUserService.login( + loginDto.platform, + loginDto, + ); + return { + code: 200, + message: '登录成功', + data: result, + }; + } + + @Post('register') + @ApiOperation({ summary: '统一注册接口' }) + @ApiResponse({ status: 200, description: '注册成功' }) + async register(@Body() registerDto: PlatformLoginDto) { + const result = await this.unifiedUserService.register( + registerDto.platform, + registerDto, + ); + return { + code: 200, + message: '注册成功', + data: result, + }; + } + + @Get('profile') + @UseGuards(PlatformAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: '获取用户信息' }) + @ApiResponse({ status: 200, description: '获取成功' }) + async getProfile(@Request() req) { + const userInfo = await this.unifiedUserService.getUserInfo(req.user.userId); + return { + code: 200, + message: '获取成功', + data: userInfo, + }; + } + + @Put('profile') + @UseGuards(PlatformAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: '更新用户信息' }) + @ApiResponse({ status: 200, description: '更新成功' }) + async updateProfile(@Request() req, @Body() updateDto: UserInfoUpdateDto) { + await this.unifiedUserService.updateUserInfo( + req.user.platform, + req.user.userId, + updateDto, + ); + return { + code: 200, + message: '更新成功', + }; + } + + @Post('bind-platform') + @UseGuards(PlatformAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: '绑定平台账号' }) + @ApiResponse({ status: 200, description: '绑定成功' }) + async bindPlatform(@Request() req, @Body() bindDto: PlatformBindDto) { + await this.unifiedUserService.bindPlatform( + req.user.userId, + bindDto.platform, + bindDto, + ); + return { + code: 200, + message: '绑定成功', + }; + } + + @Delete('unbind-platform/:platform') + @UseGuards(PlatformAuthGuard) + @ApiBearerAuth() + @ApiOperation({ summary: '解绑平台账号' }) + @ApiResponse({ status: 200, description: '解绑成功' }) + async unbindPlatform( + @Request() req, + @Param('platform') platform: PlatformType, + ) { + await this.unifiedUserService.unbindPlatform(req.user.userId, platform); + return { + code: 200, + message: '解绑成功', + }; + } + + @Get('platforms') + @ApiOperation({ summary: '获取支持的平台列表' }) + @ApiResponse({ status: 200, description: '获取成功' }) + async getSupportedPlatforms() { + const platforms = this.unifiedUserService.getSupportedPlatforms(); + return { + code: 200, + message: '获取成功', + data: platforms, + }; + } +} diff --git a/src/platform/dto/platform-login.dto.ts b/src/platform/dto/platform-login.dto.ts new file mode 100644 index 0000000..f9e2ff2 --- /dev/null +++ b/src/platform/dto/platform-login.dto.ts @@ -0,0 +1,113 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString, IsEnum, IsOptional, IsObject } from 'class-validator'; +import { PlatformType } from '../../entities/platform-user.entity'; + +export class PlatformLoginDto { + @ApiProperty({ + description: '平台类型', + enum: PlatformType, + example: PlatformType.WECHAT, + }) + @IsEnum(PlatformType) + platform: PlatformType; + + @ApiProperty({ + description: '平台授权码', + example: '081234567890abcdef', + }) + @IsString() + code: string; + + @ApiProperty({ + description: '加密用户数据(微信小程序)', + required: false, + }) + @IsOptional() + @IsString() + encryptedData?: string; + + @ApiProperty({ + description: '加密向量(微信小程序)', + required: false, + }) + @IsOptional() + @IsString() + iv?: string; + + @ApiProperty({ + description: '用户基础信息', + required: false, + }) + @IsOptional() + @IsObject() + userInfo?: any; + + @ApiProperty({ + description: '匿名授权码(抖音小程序)', + required: false, + }) + @IsOptional() + @IsString() + anonymousCode?: string; +} + +export class UserInfoUpdateDto { + @ApiProperty({ description: '用户昵称', required: false }) + @IsOptional() + @IsString() + nickname?: string; + + @ApiProperty({ description: '头像URL', required: false }) + @IsOptional() + @IsString() + avatarUrl?: string; + + @ApiProperty({ description: '手机号码', required: false }) + @IsOptional() + @IsString() + phone?: string; + + @ApiProperty({ description: '电子邮箱', required: false }) + @IsOptional() + @IsString() + email?: string; +} + +export class PlatformBindDto { + @ApiProperty({ + description: '平台类型', + enum: PlatformType, + }) + @IsEnum(PlatformType) + platform: PlatformType; + + @ApiProperty({ + description: '平台授权码', + }) + @IsString() + code: string; + + @ApiProperty({ + description: '加密用户数据(微信小程序)', + required: false, + }) + @IsOptional() + @IsString() + encryptedData?: string; + + @ApiProperty({ + description: '加密向量(微信小程序)', + required: false, + }) + @IsOptional() + @IsString() + iv?: string; + + @ApiProperty({ + description: '用户基础信息', + required: false, + }) + @IsOptional() + @IsObject() + userInfo?: any; +} diff --git a/src/platform/guards/platform-auth.guard.ts b/src/platform/guards/platform-auth.guard.ts new file mode 100644 index 0000000..665c835 --- /dev/null +++ b/src/platform/guards/platform-auth.guard.ts @@ -0,0 +1,54 @@ +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { PlatformAdapterFactory } from '../services/platform-adapter.factory'; + +@Injectable() +export class PlatformAuthGuard implements CanActivate { + constructor( + private readonly jwtService: JwtService, + private readonly platformAdapterFactory: PlatformAdapterFactory, + ) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + + if (!token) { + throw new UnauthorizedException('缺少访问令牌'); + } + + try { + // 验证JWT令牌 + const payload = this.jwtService.verify(token); + + // 验证平台令牌有效性 + const adapter = this.platformAdapterFactory.getAdapter(payload.platform); + const isValid = await adapter.validateToken(token); + + if (!isValid) { + throw new UnauthorizedException('令牌无效'); + } + + // 将用户信息附加到请求对象 + request.user = { + userId: payload.userId, + unifiedUserId: payload.unifiedUserId, + platform: payload.platform, + }; + + return true; + } catch (error) { + throw new UnauthorizedException('令牌验证失败'); + } + } + + private extractTokenFromHeader(request: any): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} diff --git a/src/platform/interfaces/platform.interface.ts b/src/platform/interfaces/platform.interface.ts new file mode 100644 index 0000000..8cce089 --- /dev/null +++ b/src/platform/interfaces/platform.interface.ts @@ -0,0 +1,76 @@ +import { PlatformType } from '../../entities/platform-user.entity'; + +export interface IPlatformAdapter { + platform: PlatformType; + + // 用户认证相关 + login(loginData: PlatformLoginData): Promise; + getUserInfo(token: string, platformUserId: string): Promise; + refreshToken(refreshToken: string): Promise; + + // 用户注册相关 + register(registerData: PlatformRegisterData): Promise; + + // 用户信息管理 + updateUserInfo( + userId: string, + updateData: Partial, + ): Promise; + + // 平台特定功能 + validateToken(token: string): Promise; + revokeToken(token: string): Promise; +} + +export interface PlatformLoginData { + code: string; + encryptedData?: string; // 微信小程序加密用户信息 + iv?: string; // 微信小程序加密向量 + userInfo?: any; // 前端传递的基础用户信息 + anonymousCode?: string; // 抖音小程序匿名code(可选) +} + +export interface PlatformRegisterData extends PlatformLoginData { + // 注册时的额外数据(当前与登录数据相同) +} + +export interface UserAuthResult { + user: UnifiedUserInfo; + tokens: { + accessToken: string; + refreshToken: string; + expiresAt: Date; + }; + platformData: any; +} + +export interface PlatformUserInfo { + platformUserId: string; // openid + nickname: string; + avatarUrl: string; + gender?: number; + country?: string; + province?: string; + city?: string; + phone?: string; + email?: string; + unionid?: string; // 跨应用用户标识 + anonymousOpenid?: string; // 抖音匿名用户标识 +} + +export interface TokenRefreshResult { + accessToken: string; + refreshToken: string; + expiresAt: Date; +} + +export interface UnifiedUserInfo { + id: string; + unifiedUserId: string; + nickname: string; + avatarUrl: string; + phone?: string; + email?: string; + status: number; + platforms: PlatformType[]; +} diff --git a/src/platform/interfaces/user-auth.interface.ts b/src/platform/interfaces/user-auth.interface.ts new file mode 100644 index 0000000..90e2bd9 --- /dev/null +++ b/src/platform/interfaces/user-auth.interface.ts @@ -0,0 +1,11 @@ +export interface UserAuthPayload { + userId: string; + unifiedUserId: string; + platform: string; + iat?: number; + exp?: number; +} + +export interface AuthRequest extends Request { + user: UserAuthPayload; +} diff --git a/src/platform/platform.module.ts b/src/platform/platform.module.ts new file mode 100644 index 0000000..be948f2 --- /dev/null +++ b/src/platform/platform.module.ts @@ -0,0 +1,75 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { HttpModule } from '@nestjs/axios'; +import { JwtModule } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; + +// 实体 +import { UserEntity } from '../entities/user.entity'; +import { PlatformUserEntity } from '../entities/platform-user.entity'; +import { ExtensionDataEntity } from '../entities/extension-data.entity'; + +// 适配器 +import { WechatAdapter } from './adapters/wechat.adapter'; +import { BytedanceAdapter } from './adapters/bytedance.adapter'; + +// 服务 +import { PlatformAdapterFactory } from './services/platform-adapter.factory'; +import { UnifiedUserService } from './services/unified-user.service'; + +// 控制器 +import { UnifiedUserController } from './controllers/unified-user.controller'; + +// 守卫 +import { PlatformAuthGuard } from './guards/platform-auth.guard'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + UserEntity, + PlatformUserEntity, + ExtensionDataEntity, + ]), + HttpModule.register({ + timeout: 5000, + maxRedirects: 3, + }), + JwtModule.registerAsync({ + inject: [ConfigService], + useFactory: (config: ConfigService) => ({ + secret: config.get('JWT_SECRET'), + signOptions: { expiresIn: '24h' }, + }), + }), + ], + providers: [ + // 适配器实现 + WechatAdapter, + BytedanceAdapter, + + // 工厂和服务 + PlatformAdapterFactory, + UnifiedUserService, + + // 守卫 + PlatformAuthGuard, + + // 适配器注册 Provider + { + provide: 'PLATFORM_ADAPTERS_REGISTRATION', + useFactory: ( + factory: PlatformAdapterFactory, + wechatAdapter: WechatAdapter, + bytedanceAdapter: BytedanceAdapter, + ) => { + factory.registerAdapter(wechatAdapter.platform, wechatAdapter); + factory.registerAdapter(bytedanceAdapter.platform, bytedanceAdapter); + return factory; + }, + inject: [PlatformAdapterFactory, WechatAdapter, BytedanceAdapter], + }, + ], + controllers: [UnifiedUserController], + exports: [UnifiedUserService, PlatformAdapterFactory], +}) +export class PlatformModule {} diff --git a/src/platform/services/platform-adapter.factory.ts b/src/platform/services/platform-adapter.factory.ts new file mode 100644 index 0000000..614062b --- /dev/null +++ b/src/platform/services/platform-adapter.factory.ts @@ -0,0 +1,44 @@ +import { Injectable, BadRequestException } from '@nestjs/common'; +import { PlatformType } from '../../entities/platform-user.entity'; +import { IPlatformAdapter } from '../interfaces/platform.interface'; + +@Injectable() +export class PlatformAdapterFactory { + private readonly adapters = new Map(); + + constructor() { + // 适配器将通过依赖注入在模块初始化时注册 + } + + /** + * 注册平台适配器 + */ + registerAdapter(platform: PlatformType, adapter: IPlatformAdapter): void { + this.adapters.set(platform, adapter); + } + + /** + * 根据平台类型获取对应的适配器 + */ + getAdapter(platform: PlatformType): IPlatformAdapter { + const adapter = this.adapters.get(platform); + if (!adapter) { + throw new BadRequestException(`不支持的平台: ${platform}`); + } + return adapter; + } + + /** + * 获取所有支持的平台列表 + */ + getSupportedPlatforms(): PlatformType[] { + return Array.from(this.adapters.keys()); + } + + /** + * 检查平台是否支持 + */ + isPlatformSupported(platform: PlatformType): boolean { + return this.adapters.has(platform); + } +} diff --git a/src/platform/services/unified-user.service.ts b/src/platform/services/unified-user.service.ts new file mode 100644 index 0000000..91f4cbb --- /dev/null +++ b/src/platform/services/unified-user.service.ts @@ -0,0 +1,188 @@ +import { + Injectable, + BadRequestException, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { PlatformAdapterFactory } from './platform-adapter.factory'; +import { UserEntity } from '../../entities/user.entity'; +import { + PlatformUserEntity, + PlatformType, +} from '../../entities/platform-user.entity'; +import { + PlatformLoginData, + PlatformRegisterData, + UserAuthResult, + PlatformUserInfo, + UnifiedUserInfo, + TokenRefreshResult, +} from '../interfaces/platform.interface'; + +@Injectable() +export class UnifiedUserService { + constructor( + private readonly platformAdapterFactory: PlatformAdapterFactory, + @InjectRepository(UserEntity) + private readonly userRepository: Repository, + @InjectRepository(PlatformUserEntity) + private readonly platformUserRepository: Repository, + ) {} + + /** + * 统一登录接口 + */ + async login( + platform: PlatformType, + loginData: PlatformLoginData, + ): Promise { + const adapter = this.platformAdapterFactory.getAdapter(platform); + return adapter.login(loginData); + } + + /** + * 统一注册接口 + */ + async register( + platform: PlatformType, + registerData: PlatformRegisterData, + ): Promise { + const adapter = this.platformAdapterFactory.getAdapter(platform); + return adapter.register(registerData); + } + + /** + * 获取用户信息 + */ + async getUserInfo(userId: string): Promise { + const user = await this.userRepository.findOne({ + where: { id: userId }, + relations: ['platformUsers'], + }); + + if (!user) { + throw new NotFoundException('用户不存在'); + } + + 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), + }; + } + + /** + * 获取平台用户信息 + */ + async getPlatformUserInfo( + platform: PlatformType, + token: string, + platformUserId: string, + ): Promise { + const adapter = this.platformAdapterFactory.getAdapter(platform); + return adapter.getUserInfo(token, platformUserId); + } + + /** + * 更新用户信息 + */ + async updateUserInfo( + platform: PlatformType, + userId: string, + updateData: Partial, + ): Promise { + const adapter = this.platformAdapterFactory.getAdapter(platform); + return adapter.updateUserInfo(userId, updateData); + } + + /** + * 绑定新平台账号 + */ + async bindPlatform( + userId: string, + platform: PlatformType, + loginData: PlatformLoginData, + ): Promise { + const user = await this.userRepository.findOne({ + where: { id: userId }, + }); + + if (!user) { + throw new NotFoundException('用户不存在'); + } + + // 检查是否已绑定该平台 + const existingBinding = await this.platformUserRepository.findOne({ + where: { userId, platform }, + }); + + if (existingBinding) { + throw new BadRequestException('该平台账号已绑定'); + } + + // 使用适配器获取平台用户信息 + const adapter = this.platformAdapterFactory.getAdapter(platform); + const authResult = await adapter.login(loginData); + + // 创建新的平台用户绑定 + const platformUser = new PlatformUserEntity(); + platformUser.userId = userId; + platformUser.platform = platform; + platformUser.platformUserId = + authResult.platformData.openid || authResult.platformData.openId; + platformUser.platformData = authResult.platformData; + + await this.platformUserRepository.save(platformUser); + } + + /** + * 解绑平台账号 + */ + async unbindPlatform(userId: string, platform: PlatformType): Promise { + const platformUser = await this.platformUserRepository.findOne({ + where: { userId, platform }, + }); + + if (!platformUser) { + throw new NotFoundException('平台账号绑定不存在'); + } + + // 撤销平台令牌 + const adapter = this.platformAdapterFactory.getAdapter(platform); + if (platformUser.accessToken) { + try { + await adapter.revokeToken(platformUser.accessToken); + } catch (error) { + // 忽略撤销令牌的错误 + console.warn(`撤销${platform}令牌失败:`, error.message); + } + } + + // 删除平台用户绑定 + await this.platformUserRepository.remove(platformUser); + } + + /** + * 刷新平台令牌 + */ + async refreshPlatformToken( + platform: PlatformType, + refreshToken: string, + ): Promise { + const adapter = this.platformAdapterFactory.getAdapter(platform); + return adapter.refreshToken(refreshToken); + } + + /** + * 获取支持的平台列表 + */ + getSupportedPlatforms(): PlatformType[] { + return this.platformAdapterFactory.getSupportedPlatforms(); + } +} diff --git a/src/services/n8n-template-factory.service.ts b/src/services/n8n-template-factory.service.ts index 985de3f..b8eaf60 100644 --- a/src/services/n8n-template-factory.service.ts +++ b/src/services/n8n-template-factory.service.ts @@ -1,11 +1,17 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { N8nTemplateEntity, TemplateType } from '../entities/n8n-template.entity'; -import { TemplateExecutionEntity, ExecutionStatus } from '../entities/template-execution.entity'; +import { + N8nTemplateEntity, + TemplateType, +} from '../entities/n8n-template.entity'; +import { + TemplateExecutionEntity, + ExecutionStatus, +} from '../entities/template-execution.entity'; import { DynamicN8nImageTemplate, - DynamicN8nVideoTemplate + DynamicN8nVideoTemplate, } from '../templates/n8n-dynamic-template'; /** @@ -19,25 +25,35 @@ export class N8nTemplateFactoryService { private templateRepository: Repository, @InjectRepository(TemplateExecutionEntity) private executionRepository: Repository, - ) { } + ) {} /** * 创建图片模板实例 * @param templateId 模板ID * @returns 初始化后的图片模板实例 */ - async createImageTemplate(templateId: number): Promise { - const instance = new DynamicN8nImageTemplate(templateId, this.templateRepository); + async createImageTemplate( + templateId: number, + ): Promise { + const instance = new DynamicN8nImageTemplate( + templateId, + this.templateRepository, + ); return await instance.onInit(); } /** - * 创建视频模板实例 + * 创建视频模板实例 * @param templateId 模板ID * @returns 初始化后的视频模板实例 */ - async createVideoTemplate(templateId: number): Promise { - const instance = new DynamicN8nVideoTemplate(templateId, this.templateRepository); + async createVideoTemplate( + templateId: number, + ): Promise { + const instance = new DynamicN8nVideoTemplate( + templateId, + this.templateRepository, + ); return await instance.onInit(); } @@ -47,10 +63,10 @@ export class N8nTemplateFactoryService { * @returns 对应类型的模板实例 */ async createTemplateByCode( - code: string + code: string, ): Promise { const config = await this.templateRepository.findOne({ - where: { code, isActive: true } + where: { code, isActive: true }, }); if (!config) { @@ -70,10 +86,10 @@ export class N8nTemplateFactoryService { * @returns 对应类型的模板实例 */ async createTemplate( - templateId: number + templateId: number, ): Promise { const config = await this.templateRepository.findOne({ - where: { id: templateId, isActive: true } + where: { id: templateId, isActive: true }, }); if (!config) { @@ -94,7 +110,7 @@ export class N8nTemplateFactoryService { async getAllTemplates(): Promise { return this.templateRepository.find({ where: { isActive: true }, - order: { sortOrder: 'DESC', createdAt: 'DESC' } + order: { sortOrder: 'DESC', createdAt: 'DESC' }, }); } @@ -106,7 +122,7 @@ export class N8nTemplateFactoryService { async getTemplatesByType(type: TemplateType): Promise { return this.templateRepository.find({ where: { templateType: type, isActive: true }, - order: { sortOrder: 'DESC', createdAt: 'DESC' } + order: { sortOrder: 'DESC', createdAt: 'DESC' }, }); } @@ -133,7 +149,7 @@ export class N8nTemplateFactoryService { */ async getTemplateByCode(code: string): Promise { return this.templateRepository.findOne({ - where: { code, isActive: true } + where: { code, isActive: true }, }); } @@ -143,9 +159,12 @@ export class N8nTemplateFactoryService { * @param imageUrls 图片URL数组 * @returns 执行结果数组 */ - async batchExecute(templateId: number, imageUrls: string[]): Promise { + async batchExecute( + templateId: number, + imageUrls: string[], + ): Promise { const template = await this.createTemplate(templateId); - return Promise.all(imageUrls.map(url => template.execute(url))); + return Promise.all(imageUrls.map((url) => template.execute(url))); } /** @@ -154,7 +173,10 @@ export class N8nTemplateFactoryService { * @param limit 推荐数量限制 * @returns 推荐的模板配置 */ - async recommendTemplates(userTags: string[], limit: number = 5): Promise { + async recommendTemplates( + userTags: string[], + limit: number = 5, + ): Promise { // 这里可以实现更复杂的推荐算法 const query = this.templateRepository .createQueryBuilder('template') @@ -165,10 +187,9 @@ export class N8nTemplateFactoryService { // 如果提供了用户标签,可以基于标签匹配进行推荐 if (userTags.length > 0) { - query.andWhere( - 'JSON_OVERLAPS(template.tags, :userTags)', - { userTags: JSON.stringify(userTags) } - ); + query.andWhere('JSON_OVERLAPS(template.tags, :userTags)', { + userTags: JSON.stringify(userTags), + }); } return query.getMany(); @@ -179,7 +200,9 @@ export class N8nTemplateFactoryService { * @param data 执行记录数据 * @returns 创建的执行记录 */ - async createExecution(data: Partial): Promise { + async createExecution( + data: Partial, + ): Promise { const execution = this.executionRepository.create(data); return this.executionRepository.save(execution); } @@ -190,11 +213,14 @@ export class N8nTemplateFactoryService { * @param data 更新数据 * @returns 更新后的执行记录 */ - async updateExecution(id: number, data: Partial): Promise { + 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`) + throw new Error(`not found ${id} from template execution table`); } /** @@ -203,12 +229,15 @@ export class N8nTemplateFactoryService { * @param limit 记录数量限制 * @returns 用户的执行记录列表 */ - async getUserExecutions(userId: string, limit = 20): Promise { + async getUserExecutions( + userId: string, + limit = 20, + ): Promise { return this.executionRepository.find({ where: { userId }, relations: ['template'], order: { createdAt: 'DESC' }, - take: limit + take: limit, }); } @@ -234,16 +263,20 @@ export class N8nTemplateFactoryService { .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 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); + const totalCredit = stats.reduce( + (sum, stat) => sum + parseInt(stat.totalCredit || '0'), + 0, + ); return { totalUsage: total, successRate, averageExecutionTime: avgTime, - totalCreditCost: totalCredit + totalCreditCost: totalCredit, }; } @@ -263,13 +296,15 @@ export class N8nTemplateFactoryService { .addSelect('SUM(execution.creditCost)', 'totalCreditSpent') .addSelect('AVG(execution.creditCost)', 'avgCreditPerExecution') .where('execution.userId = :userId', { userId }) - .andWhere('execution.status = :status', { status: ExecutionStatus.COMPLETED }) + .andWhere('execution.status = :status', { + status: ExecutionStatus.COMPLETED, + }) .getRawOne(); return { totalCreditSpent: parseInt(stats.totalCreditSpent || '0'), totalExecutions: parseInt(stats.totalExecutions || '0'), - averageCreditPerExecution: parseFloat(stats.avgCreditPerExecution || '0') + averageCreditPerExecution: parseFloat(stats.avgCreditPerExecution || '0'), }; } -} \ No newline at end of file +} diff --git a/src/templates/index.ts b/src/templates/index.ts index e76828f..6309e41 100644 --- a/src/templates/index.ts +++ b/src/templates/index.ts @@ -1 +1 @@ -export { TemplateService } from './template.service' \ No newline at end of file +export { TemplateService } from './template.service'; diff --git a/src/templates/n8n-dynamic-template.ts b/src/templates/n8n-dynamic-template.ts index 9b78709..f8839c5 100644 --- a/src/templates/n8n-dynamic-template.ts +++ b/src/templates/n8n-dynamic-template.ts @@ -1,6 +1,12 @@ import { Repository } from 'typeorm'; -import { N8nTemplateEntity, TemplateType } from '../entities/n8n-template.entity'; -import { N8nImageGenerateTemplate, N8nVideoGenerateTemplate } from './n8nTemplate'; +import { + N8nTemplateEntity, + TemplateType, +} from '../entities/n8n-template.entity'; +import { + N8nImageGenerateTemplate, + N8nVideoGenerateTemplate, +} from './n8nTemplate'; /** * 动态图片模板实现 @@ -8,10 +14,10 @@ import { N8nImageGenerateTemplate, N8nVideoGenerateTemplate } from './n8nTemplat */ export class DynamicN8nImageTemplate extends N8nImageGenerateTemplate { private config: N8nTemplateEntity | null = null; - + constructor( private templateId: number, - private templateRepository: Repository + private templateRepository: Repository, ) { super(); } @@ -21,60 +27,62 @@ export class DynamicN8nImageTemplate extends N8nImageGenerateTemplate { */ async onInit(): Promise { this.config = await this.templateRepository.findOne({ - where: { - id: this.templateId, - templateType: TemplateType.IMAGE, - isActive: true - } + where: { + id: this.templateId, + templateType: TemplateType.IMAGE, + isActive: true, + }, }); - + if (!this.config) { - throw new Error(`Image template with id ${this.templateId} not found or inactive`); + throw new Error( + `Image template with id ${this.templateId} not found or inactive`, + ); } - + return this; } // 从数据库动态获取配置 - 实现Template抽象类的属性 - get code(): string { - return this.config?.code || ''; + get code(): string { + return this.config?.code || ''; } - - get name(): string { - return this.config?.name || ''; + + get name(): string { + return this.config?.name || ''; } - - get description(): string { - return this.config?.description || ''; + + get description(): string { + return this.config?.description || ''; } - - get creditCost(): number { - return this.config?.creditCost || 0; + + get creditCost(): number { + return this.config?.creditCost || 0; } - - get version(): string { - return this.config?.version || ''; + + get version(): string { + return this.config?.version || ''; } - - get input(): string { - return this.config?.inputExampleUrl || ''; + + get input(): string { + return this.config?.inputExampleUrl || ''; } - - get output(): string { - return this.config?.outputExampleUrl || ''; + + get output(): string { + return this.config?.outputExampleUrl || ''; } - - get tags(): string[] { - return this.config?.tags || []; + + get tags(): string[] { + return this.config?.tags || []; } // 实现N8nImageGenerateTemplate抽象类的属性 - get imageModel(): string { - return this.config?.imageModel || ''; + get imageModel(): string { + return this.config?.imageModel || ''; } - - get imagePrompt(): string { - return this.config?.imagePrompt || ''; + + get imagePrompt(): string { + return this.config?.imagePrompt || ''; } // execute方法自动继承自N8nImageGenerateTemplate,无需重写 @@ -86,10 +94,10 @@ export class DynamicN8nImageTemplate extends N8nImageGenerateTemplate { */ export class DynamicN8nVideoTemplate extends N8nVideoGenerateTemplate { private config: N8nTemplateEntity | null = null; - + constructor( private templateId: number, - private templateRepository: Repository + private templateRepository: Repository, ) { super(); } @@ -99,78 +107,80 @@ export class DynamicN8nVideoTemplate extends N8nVideoGenerateTemplate { */ async onInit(): Promise { this.config = await this.templateRepository.findOne({ - where: { - id: this.templateId, - templateType: TemplateType.VIDEO, - isActive: true - } + where: { + id: this.templateId, + templateType: TemplateType.VIDEO, + isActive: true, + }, }); - + if (!this.config) { - throw new Error(`Video template with id ${this.templateId} not found or inactive`); + throw new Error( + `Video template with id ${this.templateId} not found or inactive`, + ); } - + return this; } // 从数据库动态获取配置 - 实现Template抽象类的属性 - get code(): string { - return this.config?.code || ''; + get code(): string { + return this.config?.code || ''; } - - get name(): string { - return this.config?.name || ''; + + get name(): string { + return this.config?.name || ''; } - - get description(): string { - return this.config?.description || ''; + + get description(): string { + return this.config?.description || ''; } - - get creditCost(): number { - return this.config?.creditCost || 0; + + get creditCost(): number { + return this.config?.creditCost || 0; } - - get version(): string { - return this.config?.version || ''; + + get version(): string { + return this.config?.version || ''; } - - get input(): string { - return this.config?.inputExampleUrl || ''; + + get input(): string { + return this.config?.inputExampleUrl || ''; } - - get output(): string { - return this.config?.outputExampleUrl || ''; + + get output(): string { + return this.config?.outputExampleUrl || ''; } - - get tags(): string[] { - return this.config?.tags || []; + + get tags(): string[] { + return this.config?.tags || []; } // 实现N8nImageGenerateTemplate抽象类的属性(视频模板也需要图片生成) - get imageModel(): string { - return this.config?.imageModel || ''; + get imageModel(): string { + return this.config?.imageModel || ''; } - - get imagePrompt(): string { - return this.config?.imagePrompt || ''; + + get imagePrompt(): string { + return this.config?.imagePrompt || ''; } // 实现N8nVideoGenerateTemplate抽象类的属性 - get videoModel(): string { - return this.config?.videoModel || ''; + get videoModel(): string { + return this.config?.videoModel || ''; } - - get videoPrompt(): string { - return this.config?.videoPrompt || ''; + + get videoPrompt(): string { + return this.config?.videoPrompt || ''; } - - get duration(): number { - return this.config?.duration || 6; + + get duration(): number { + return this.config?.duration || 6; } - - get aspectRatio(): string { - return this.config?.aspectRatio || '9:16'; + + get aspectRatio(): string { + return this.config?.aspectRatio || '9:16'; } // execute方法自动继承自N8nVideoGenerateTemplate,无需重写 -} \ No newline at end of file +} diff --git a/src/templates/n8nTemplate.ts b/src/templates/n8nTemplate.ts index 4a0b4c1..823101b 100644 --- a/src/templates/n8nTemplate.ts +++ b/src/templates/n8nTemplate.ts @@ -1,73 +1,80 @@ -import { ImageGenerateTemplate, VideoGenerateTemplate } from "./types"; +import { ImageGenerateTemplate, VideoGenerateTemplate } from './types'; import axios from 'axios'; export abstract class N8nImageGenerateTemplate extends ImageGenerateTemplate { - abstract readonly imageModel: string; - abstract readonly imagePrompt: string; - execute(imageUrl: string): Promise { - return axios.request({ - url: `https://n8n.bowongai.com/webhook/76f92dbf-785f-4add-96b5-a108174b7c14`, - method: 'post', - data: { - "workflow": "图生图", - "environment": "https://bowongai-test--text-video-agent-fastapi-app.modal.run", - "image_generation": { - "model": this.imageModel, - "prompt": this.imagePrompt, - "image_url": imageUrl - } - } - }).then(res => res.data).then(res => { - if (res.status) { - const data = res.data; - if (!data) throw new Error(`结果有误`) - if (Array.isArray(data) && data.length > 0) { - return data[0]; - } - return data; - } - throw new Error(res.msg) - }) - } + abstract readonly imageModel: string; + abstract readonly imagePrompt: string; + execute(imageUrl: string): Promise { + return axios + .request({ + url: `https://n8n.bowongai.com/webhook/76f92dbf-785f-4add-96b5-a108174b7c14`, + method: 'post', + data: { + workflow: '图生图', + environment: + 'https://bowongai-test--text-video-agent-fastapi-app.modal.run', + image_generation: { + model: this.imageModel, + prompt: this.imagePrompt, + image_url: imageUrl, + }, + }, + }) + .then((res) => res.data) + .then((res) => { + if (res.status) { + const data = res.data; + if (!data) throw new Error(`结果有误`); + if (Array.isArray(data) && data.length > 0) { + return data[0]; + } + return data; + } + throw new Error(res.msg); + }); + } } - export abstract class N8nVideoGenerateTemplate extends VideoGenerateTemplate { - abstract readonly imageModel: string; - abstract readonly imagePrompt: string; - abstract readonly videoModel: string; - abstract readonly videoPrompt: string; - abstract readonly duration: number; - abstract readonly aspectRatio: string; - execute(imageUrl: string): Promise { - return axios.request({ - url: `https://n8n.bowongai.com/webhook/76f92dbf-785f-4add-96b5-a108174b7c14`, - method: 'post', - data: { - "workflow": "图生图+生视频", - "environment": "https://bowongai-test--text-video-agent-fastapi-app.modal.run", - "image_generation": { - "model": this.imageModel, - "prompt": this.imagePrompt, - "image_url": imageUrl - }, - "video_generation": { - "model": this.videoModel, - "prompt": this.videoPrompt, - "duration": `${this.duration}`, - "aspect_ratio": this.aspectRatio - } - } - }).then(res => res.data).then(res => { - if (res.status) { - const data = res.data; - if (!data) throw new Error(`结果有误`) - if (Array.isArray(data) && data.length > 0) { - return data[0]; - } - return data; - } - throw new Error(res.msg) - }) - } + abstract readonly imageModel: string; + abstract readonly imagePrompt: string; + abstract readonly videoModel: string; + abstract readonly videoPrompt: string; + abstract readonly duration: number; + abstract readonly aspectRatio: string; + execute(imageUrl: string): Promise { + return axios + .request({ + url: `https://n8n.bowongai.com/webhook/76f92dbf-785f-4add-96b5-a108174b7c14`, + method: 'post', + data: { + workflow: '图生图+生视频', + environment: + 'https://bowongai-test--text-video-agent-fastapi-app.modal.run', + image_generation: { + model: this.imageModel, + prompt: this.imagePrompt, + image_url: imageUrl, + }, + video_generation: { + model: this.videoModel, + prompt: this.videoPrompt, + duration: `${this.duration}`, + aspect_ratio: this.aspectRatio, + }, + }, + }) + .then((res) => res.data) + .then((res) => { + if (res.status) { + const data = res.data; + if (!data) throw new Error(`结果有误`); + if (Array.isArray(data) && data.length > 0) { + return data[0]; + } + return data; + } + throw new Error(res.msg); + }); + } } diff --git a/src/templates/n8nTemplates/CharacterFigurineTemplate.ts b/src/templates/n8nTemplates/CharacterFigurineTemplate.ts index 9281834..c3c8d32 100644 --- a/src/templates/n8nTemplates/CharacterFigurineTemplate.ts +++ b/src/templates/n8nTemplates/CharacterFigurineTemplate.ts @@ -2,22 +2,34 @@ import { N8nVideoGenerateTemplate } from '../n8nTemplate'; // 人物手办模板 export class CharacterFigurineTemplate extends N8nVideoGenerateTemplate { - readonly code = 'character_figurine_v1'; - readonly name = '人物手办'; - readonly description = '将人物照片制作成精细的角色手办模型,展示在收藏家房间中,并生成抚摸手办的视频'; - readonly creditCost = 28; - readonly version = '1.0.0'; - readonly input = 'https://cdn.roasmax.cn/upload/3d590851eb584e92aa415a964e93260e.jpg'; // 原始人物照片示例 - readonly output = 'https://file.302.ai/gpt/imgs/20250903/13256820c89e486790234e182c972905.mp4'; // 输出手办图片示例 - readonly tags = ['人物', '手办', '模型', '收藏', 'PVC', '角色模型', '视频生成']; + readonly code = 'character_figurine_v1'; + readonly name = '人物手办'; + readonly description = + '将人物照片制作成精细的角色手办模型,展示在收藏家房间中,并生成抚摸手办的视频'; + readonly creditCost = 28; + readonly version = '1.0.0'; + readonly input = + 'https://cdn.roasmax.cn/upload/3d590851eb584e92aa415a964e93260e.jpg'; // 原始人物照片示例 + readonly output = + 'https://file.302.ai/gpt/imgs/20250903/13256820c89e486790234e182c972905.mp4'; // 输出手办图片示例 + readonly tags = [ + '人物', + '手办', + '模型', + '收藏', + 'PVC', + '角色模型', + '视频生成', + ]; - // N8n模板特定属性 - 图片生成 - readonly imageModel = 'gemini-2.5-flash-image-preview'; // nano banana 模型 - readonly imagePrompt = 'Transform this photo into a highly detailed character model. Place a detailed, colorful box with the image of the character from the photo in front of the model. In front of the box, place the finished character model from the photo, with the PVC texture rendered realistically. Set the entire scene in a bright, stylish interior, reminiscent of a toy collector\'s or hobbyist\'s room—full of intricate details, vibrant decor, and a playful atmosphere. Ensure the lighting is bright and clear to highlight the model and its packaging.'; + // N8n模板特定属性 - 图片生成 + readonly imageModel = 'gemini-2.5-flash-image-preview'; // nano banana 模型 + readonly imagePrompt = + "Transform this photo into a highly detailed character model. Place a detailed, colorful box with the image of the character from the photo in front of the model. In front of the box, place the finished character model from the photo, with the PVC texture rendered realistically. Set the entire scene in a bright, stylish interior, reminiscent of a toy collector's or hobbyist's room—full of intricate details, vibrant decor, and a playful atmosphere. Ensure the lighting is bright and clear to highlight the model and its packaging."; - // N8n模板特定属性 - 视频生成 - readonly videoModel = '302/MiniMax-Hailuo-02'; - readonly videoPrompt = '一只手摸了摸手办的脑袋'; - readonly duration = 6; // 6秒视频 - readonly aspectRatio = '9:16'; // 竖屏比例 + // N8n模板特定属性 - 视频生成 + readonly videoModel = '302/MiniMax-Hailuo-02'; + readonly videoPrompt = '一只手摸了摸手办的脑袋'; + readonly duration = 6; // 6秒视频 + readonly aspectRatio = '9:16'; // 竖屏比例 } diff --git a/src/templates/n8nTemplates/CosplayRealPersonTemplate.ts b/src/templates/n8nTemplates/CosplayRealPersonTemplate.ts index 6c12bdc..6950395 100644 --- a/src/templates/n8nTemplates/CosplayRealPersonTemplate.ts +++ b/src/templates/n8nTemplates/CosplayRealPersonTemplate.ts @@ -2,22 +2,27 @@ import { N8nVideoGenerateTemplate } from '../n8nTemplate'; // cos真人模板 export class CosplayRealPersonTemplate extends N8nVideoGenerateTemplate { - readonly code = 'cosplay_real_person_v1'; - readonly name = 'cos真人'; - readonly description = '生成cosplay这张插图的真人照片,背景设置在Comiket,并生成cosplay人物做经典动作的视频'; - readonly creditCost = 25; - readonly version = '1.0.0'; - readonly input = 'https://cdn.roasmax.cn/upload/c69a847f8e8740f594f731ef502c4054.png'; // 原始插图示例 - readonly output = 'https://file.302.ai/gpt/imgs/20250903/a78d664707c04cf9815f5195b4bfcd18.mp4'; // 输出图片示例 - readonly tags = ['cosplay', '真人', 'Comiket', '动漫', '二次元', '视频生成']; + readonly code = 'cosplay_real_person_v1'; + readonly name = 'cos真人'; + readonly description = + '生成cosplay这张插图的真人照片,背景设置在Comiket,并生成cosplay人物做经典动作的视频'; + readonly creditCost = 25; + readonly version = '1.0.0'; + readonly input = + 'https://cdn.roasmax.cn/upload/c69a847f8e8740f594f731ef502c4054.png'; // 原始插图示例 + readonly output = + 'https://file.302.ai/gpt/imgs/20250903/a78d664707c04cf9815f5195b4bfcd18.mp4'; // 输出图片示例 + readonly tags = ['cosplay', '真人', 'Comiket', '动漫', '二次元', '视频生成']; - // N8n模板特定属性 - 图片生成 - readonly imageModel = 'gemini-2.5-flash-image'; // 根据要求使用 gemini-2.5-flash-image - readonly imagePrompt = '生成一张cosplay这张插图的照片,背景设置在Comiket,插图主体大一些'; + // N8n模板特定属性 - 图片生成 + readonly imageModel = 'gemini-2.5-flash-image'; // 根据要求使用 gemini-2.5-flash-image + readonly imagePrompt = + '生成一张cosplay这张插图的照片,背景设置在Comiket,插图主体大一些'; - // N8n模板特定属性 - 视频生成 - readonly videoModel = '302/MiniMax-Hailuo-02'; - readonly videoPrompt = '图片里的cosplay人物动起来,做出这个cosplay人物的经典动作'; - readonly duration = 6; // 6秒视频 - readonly aspectRatio = '9:16'; // 竖屏比例 + // N8n模板特定属性 - 视频生成 + readonly videoModel = '302/MiniMax-Hailuo-02'; + readonly videoPrompt = + '图片里的cosplay人物动起来,做出这个cosplay人物的经典动作'; + readonly duration = 6; // 6秒视频 + readonly aspectRatio = '9:16'; // 竖屏比例 } diff --git a/src/templates/n8nTemplates/GarageOpeningTemplate.ts b/src/templates/n8nTemplates/GarageOpeningTemplate.ts index badcf30..ef3d30c 100644 --- a/src/templates/n8nTemplates/GarageOpeningTemplate.ts +++ b/src/templates/n8nTemplates/GarageOpeningTemplate.ts @@ -2,22 +2,35 @@ import { N8nVideoGenerateTemplate } from '../n8nTemplate'; // 车库开门模板 export class GarageOpeningTemplate extends N8nVideoGenerateTemplate { - readonly code = 'garage_opening_v1'; - readonly name = '车库开门'; - readonly description = '生成豪宅车库开门场景,展示人物站在现代豪宅前,车库门缓缓升起露出豪车的视频'; - readonly creditCost = 35; - readonly version = '1.0.0'; - readonly input = 'https://bowong-ai-shanghai.tos-cn-shanghai.volces.com/images/1756889137996_jujhz5.jpg'; // 原始人物图片示例 - readonly output = 'https://file.302.ai/gpt/imgs/20250903/c98adec564fb4cf89813ecd89afc13bd.mp4'; // 输出图片示例 - readonly tags = ['车库', '豪宅', '豪车', 'Lamborghini', '时尚', '奢华', '视频生成']; + readonly code = 'garage_opening_v1'; + readonly name = '车库开门'; + readonly description = + '生成豪宅车库开门场景,展示人物站在现代豪宅前,车库门缓缓升起露出豪车的视频'; + readonly creditCost = 35; + readonly version = '1.0.0'; + readonly input = + 'https://bowong-ai-shanghai.tos-cn-shanghai.volces.com/images/1756889137996_jujhz5.jpg'; // 原始人物图片示例 + readonly output = + 'https://file.302.ai/gpt/imgs/20250903/c98adec564fb4cf89813ecd89afc13bd.mp4'; // 输出图片示例 + readonly tags = [ + '车库', + '豪宅', + '豪车', + 'Lamborghini', + '时尚', + '奢华', + '视频生成', + ]; - // N8n模板特定属性 - 图片生成 - readonly imageModel = 'gemini-2.5-flash-image-preview'; // nano banana 模型 - readonly imagePrompt = 'First shot, 4K still frame: A person with long straight black hair, facing camera, wearing a plain pastel-pink track jacket and matching full-length track pants, white crew socks, and clean pink sneakers. They stand on sun-lit marble in front of a closed matte-black garage door of a modern white mansion. Bright sunlight, palm trees on both sides, marble stairs on the right.'; + // N8n模板特定属性 - 图片生成 + readonly imageModel = 'gemini-2.5-flash-image-preview'; // nano banana 模型 + readonly imagePrompt = + 'First shot, 4K still frame: A person with long straight black hair, facing camera, wearing a plain pastel-pink track jacket and matching full-length track pants, white crew socks, and clean pink sneakers. They stand on sun-lit marble in front of a closed matte-black garage door of a modern white mansion. Bright sunlight, palm trees on both sides, marble stairs on the right.'; - // N8n模板特定属性 - 视频生成 - readonly videoModel = '302/veo3-fast-frames'; // 使用veo3-fast-frames模型 - readonly videoPrompt = 'Realistic 4K movie video clip: In front of a modern mansion garage with a large black curtain door. As the black rolling shutter door slowly rises (with a smooth and stable movement), the subject walks backwards in perfect sync (with a natural gait and eye contact with the door). Once the two Lamborghini Urus SUVs are fully exposed from the car doors—one in bright pink and the other in matte dark brown—the subject will stop in front of the cars and make a natural gesture (pointing to features, explaining, confident posture). At the last second of the video, the camera will focus on the subject and pull away a bit. Throughout the video, the camera steadily advances (focusing on the subject and cars at a slightly lower angle to enhance luxury), with bright sunlight illuminating the marble walls, palm trees on both sides of the entrance, and visible stairs on the right. This scene combines high-end fashion, luxurious atmosphere, and smooth motion transitions.'; - readonly duration = 6; // 6秒视频 - readonly aspectRatio = '16:9'; // veo3-fast-frames只支持16:9比例 + // N8n模板特定属性 - 视频生成 + readonly videoModel = '302/veo3-fast-frames'; // 使用veo3-fast-frames模型 + readonly videoPrompt = + 'Realistic 4K movie video clip: In front of a modern mansion garage with a large black curtain door. As the black rolling shutter door slowly rises (with a smooth and stable movement), the subject walks backwards in perfect sync (with a natural gait and eye contact with the door). Once the two Lamborghini Urus SUVs are fully exposed from the car doors—one in bright pink and the other in matte dark brown—the subject will stop in front of the cars and make a natural gesture (pointing to features, explaining, confident posture). At the last second of the video, the camera will focus on the subject and pull away a bit. Throughout the video, the camera steadily advances (focusing on the subject and cars at a slightly lower angle to enhance luxury), with bright sunlight illuminating the marble walls, palm trees on both sides of the entrance, and visible stairs on the right. This scene combines high-end fashion, luxurious atmosphere, and smooth motion transitions.'; + readonly duration = 6; // 6秒视频 + readonly aspectRatio = '16:9'; // veo3-fast-frames只支持16:9比例 } diff --git a/src/templates/n8nTemplates/JapaneseMagazineTemplate.ts b/src/templates/n8nTemplates/JapaneseMagazineTemplate.ts index 43b7b15..5a695c0 100644 --- a/src/templates/n8nTemplates/JapaneseMagazineTemplate.ts +++ b/src/templates/n8nTemplates/JapaneseMagazineTemplate.ts @@ -2,22 +2,35 @@ import { N8nVideoGenerateTemplate } from '../n8nTemplate'; // 日本杂志模板 export class JapaneseMagazineTemplate extends N8nVideoGenerateTemplate { - readonly code = 'japanese_magazine_v1'; - readonly name = '日本杂志'; - readonly description = '生成时尚的日本杂志风格数字海报,包含多个模特和日语文本元素,并制作人物律动视频'; - readonly creditCost = 32; - readonly version = '1.0.0'; - readonly input = 'https://cdn.roasmax.cn/upload/1980dcb0b0e3467794065c85ba607913.jpg'; // 原始人物图片示例 - readonly output = 'https://file.302.ai/gpt/imgs/20250903/a13130f161814b36826672ff08f68876.mp4'; // 输出杂志图片示例 - readonly tags = ['日本杂志', '时尚', '海报', '街头服饰', '拼贴', '日语文本', '视频生成']; + readonly code = 'japanese_magazine_v1'; + readonly name = '日本杂志'; + readonly description = + '生成时尚的日本杂志风格数字海报,包含多个模特和日语文本元素,并制作人物律动视频'; + readonly creditCost = 32; + readonly version = '1.0.0'; + readonly input = + 'https://cdn.roasmax.cn/upload/1980dcb0b0e3467794065c85ba607913.jpg'; // 原始人物图片示例 + readonly output = + 'https://file.302.ai/gpt/imgs/20250903/a13130f161814b36826672ff08f68876.mp4'; // 输出杂志图片示例 + readonly tags = [ + '日本杂志', + '时尚', + '海报', + '街头服饰', + '拼贴', + '日语文本', + '视频生成', + ]; - // N8n模板特定属性 - 图片生成 - readonly imageModel = 'mj'; // 使用 mj 模型 - readonly imagePrompt = '品牌活动的编辑风格数字海报,以时尚的中央女模特为特色,她只展示头部近景、分层的街头服饰和自信的眼神。她被大胆的灯光和拼贴风格的阴影所包围。在背景前方下面,微缩的另外的她人工智能生成的模型展示了俏皮、休闲的风格——一个穿着无檐便帽和牛仔夹克,另一个穿着柔和的运动服,手势富有表现力。该场景使用纯白色的工作室背景,充满了剪纸贴纸、星星、箭头和粗体日语文本,并配以彩色轮廓。具有大胆的布局和优雅的排版。'; + // N8n模板特定属性 - 图片生成 + readonly imageModel = 'mj'; // 使用 mj 模型 + readonly imagePrompt = + '品牌活动的编辑风格数字海报,以时尚的中央女模特为特色,她只展示头部近景、分层的街头服饰和自信的眼神。她被大胆的灯光和拼贴风格的阴影所包围。在背景前方下面,微缩的另外的她人工智能生成的模型展示了俏皮、休闲的风格——一个穿着无檐便帽和牛仔夹克,另一个穿着柔和的运动服,手势富有表现力。该场景使用纯白色的工作室背景,充满了剪纸贴纸、星星、箭头和粗体日语文本,并配以彩色轮廓。具有大胆的布局和优雅的排版。'; - // N8n模板特定属性 - 视频生成 - readonly videoModel = '302/MiniMax-Hailuo-02'; - readonly videoPrompt = '这张图片的人物都在自己的位置不变,但是都要欢快的小幅度的律动'; - readonly duration = 6; // 6秒视频 - readonly aspectRatio = '9:16'; // 竖屏比例 + // N8n模板特定属性 - 视频生成 + readonly videoModel = '302/MiniMax-Hailuo-02'; + readonly videoPrompt = + '这张图片的人物都在自己的位置不变,但是都要欢快的小幅度的律动'; + readonly duration = 6; // 6秒视频 + readonly aspectRatio = '9:16'; // 竖屏比例 } diff --git a/src/templates/n8nTemplates/NuclearExplosionTemplate.ts b/src/templates/n8nTemplates/NuclearExplosionTemplate.ts index 60ed193..7cd8b31 100644 --- a/src/templates/n8nTemplates/NuclearExplosionTemplate.ts +++ b/src/templates/n8nTemplates/NuclearExplosionTemplate.ts @@ -2,22 +2,27 @@ import { N8nVideoGenerateTemplate } from '../n8nTemplate'; // 原子弹爆炸模板 export class NuclearExplosionTemplate extends N8nVideoGenerateTemplate { - readonly code = 'nuclear_explosion_v1'; - readonly name = '原子弹爆炸'; - readonly description = '生成远处核爆炸场景,冲击波向人物吹来的电影化启示录风格视频'; - readonly creditCost = 20; - readonly version = '1.0.0'; - readonly input = 'https://cdn.roasmax.cn/upload/1980dcb0b0e3467794065c85ba607913.jpg'; // 原始人物图片示例 - readonly output = 'https://file.302.ai/gpt/imgs/20250903/4f4fbdff8c3343858f897685013b4a24.mp4'; // 输出视频示例 - readonly tags = ['核爆炸', '冲击波', '启示录', '电影化', '特效', '视频生成']; + readonly code = 'nuclear_explosion_v1'; + readonly name = '原子弹爆炸'; + readonly description = + '生成远处核爆炸场景,冲击波向人物吹来的电影化启示录风格视频'; + readonly creditCost = 20; + readonly version = '1.0.0'; + readonly input = + 'https://cdn.roasmax.cn/upload/1980dcb0b0e3467794065c85ba607913.jpg'; // 原始人物图片示例 + readonly output = + 'https://file.302.ai/gpt/imgs/20250903/4f4fbdff8c3343858f897685013b4a24.mp4'; // 输出视频示例 + readonly tags = ['核爆炸', '冲击波', '启示录', '电影化', '特效', '视频生成']; - // N8n模板特定属性 - 图片生成(这个模板主要用于视频生成,图片生成使用原图) - readonly imageModel = 'gemini-2.5-flash-image-preview'; // nano banana 模型 - readonly imagePrompt = 'Keep the original image unchanged, prepare for nuclear explosion video generation'; + // N8n模板特定属性 - 图片生成(这个模板主要用于视频生成,图片生成使用原图) + readonly imageModel = 'gemini-2.5-flash-image-preview'; // nano banana 模型 + readonly imagePrompt = + 'Keep the original image unchanged, prepare for nuclear explosion video generation'; - // N8n模板特定属性 - 视频生成 - readonly videoModel = '302/MiniMax-Hailuo-02'; - readonly videoPrompt = '远处爆发了一场核爆炸,冲击波随风吹向主体。大规模、电影化、启示录式。画面中的人物始终看向摄像机的方向'; - readonly duration = 6; // 6秒视频 - readonly aspectRatio = '9:16'; // 竖屏比例 + // N8n模板特定属性 - 视频生成 + readonly videoModel = '302/MiniMax-Hailuo-02'; + readonly videoPrompt = + '远处爆发了一场核爆炸,冲击波随风吹向主体。大规模、电影化、启示录式。画面中的人物始终看向摄像机的方向'; + readonly duration = 6; // 6秒视频 + readonly aspectRatio = '9:16'; // 竖屏比例 } diff --git a/src/templates/n8nTemplates/PetFigurineTemplate.ts b/src/templates/n8nTemplates/PetFigurineTemplate.ts index 8d4308e..3897e61 100644 --- a/src/templates/n8nTemplates/PetFigurineTemplate.ts +++ b/src/templates/n8nTemplates/PetFigurineTemplate.ts @@ -2,22 +2,34 @@ import { N8nVideoGenerateTemplate } from '../n8nTemplate'; // 宠物手办模板 export class PetFigurineTemplate extends N8nVideoGenerateTemplate { - readonly code = 'pet_figurine_v1'; - readonly name = '宠物手办'; - readonly description = '将宠物插图制作成1/7比例的商业化手办,展示在电脑桌上,并生成手办把玩视频'; - readonly creditCost = 30; - readonly version = '1.0.0'; - readonly input = 'https://cdn.roasmax.cn/upload/7cc18d70e9ba48bc8cadff7d3cdf62e1.png'; // 原始宠物插图示例 - readonly output = 'https://file.302.ai/gpt/imgs/20250903/92e2e8adf4754d1a8bf406418ab4d10d.mp4'; // 输出手办图片示例 - readonly tags = ['宠物', '手办', '模型', '收藏', 'BANDAI', 'ZBrush', '视频生成']; + readonly code = 'pet_figurine_v1'; + readonly name = '宠物手办'; + readonly description = + '将宠物插图制作成1/7比例的商业化手办,展示在电脑桌上,并生成手办把玩视频'; + readonly creditCost = 30; + readonly version = '1.0.0'; + readonly input = + 'https://cdn.roasmax.cn/upload/7cc18d70e9ba48bc8cadff7d3cdf62e1.png'; // 原始宠物插图示例 + readonly output = + 'https://file.302.ai/gpt/imgs/20250903/92e2e8adf4754d1a8bf406418ab4d10d.mp4'; // 输出手办图片示例 + readonly tags = [ + '宠物', + '手办', + '模型', + '收藏', + 'BANDAI', + 'ZBrush', + '视频生成', + ]; - // N8n模板特定属性 - 图片生成 - readonly imageModel = 'gemini-2.5-flash-image'; // nano banana 模型 - readonly imagePrompt = 'Create a 1/7 scale commercialized figure of the animal in the illustration, in a realistic style and environment. Place the figure on a computer desk, using a circular transparent acrylic base without any text. On the Macbook Pro screen, display the ZBrush modeling process of the figure in line mode. Next to the computer screen, place a BANDAI-style toy packaging box printed with the original artwork.'; + // N8n模板特定属性 - 图片生成 + readonly imageModel = 'gemini-2.5-flash-image'; // nano banana 模型 + readonly imagePrompt = + 'Create a 1/7 scale commercialized figure of the animal in the illustration, in a realistic style and environment. Place the figure on a computer desk, using a circular transparent acrylic base without any text. On the Macbook Pro screen, display the ZBrush modeling process of the figure in line mode. Next to the computer screen, place a BANDAI-style toy packaging box printed with the original artwork.'; - // N8n模板特定属性 - 视频生成 - readonly videoModel = '302/MiniMax-Hailuo-02'; - readonly videoPrompt = '一双手拿起手办,在手里展示把玩'; - readonly duration = 6; // 6秒视频 - readonly aspectRatio = '9:16'; // 竖屏比例 + // N8n模板特定属性 - 视频生成 + readonly videoModel = '302/MiniMax-Hailuo-02'; + readonly videoPrompt = '一双手拿起手办,在手里展示把玩'; + readonly duration = 6; // 6秒视频 + readonly aspectRatio = '9:16'; // 竖屏比例 } diff --git a/src/templates/n8nTemplates/PhotoRestoreTemplate.ts b/src/templates/n8nTemplates/PhotoRestoreTemplate.ts index 5c0da2f..61cfcd3 100644 --- a/src/templates/n8nTemplates/PhotoRestoreTemplate.ts +++ b/src/templates/n8nTemplates/PhotoRestoreTemplate.ts @@ -1,17 +1,20 @@ -import { N8nImageGenerateTemplate } from "../n8nTemplate"; +import { N8nImageGenerateTemplate } from '../n8nTemplate'; // 老照片修复上色模板 export class PhotoRestoreTemplate extends N8nImageGenerateTemplate { - readonly code = 'photo_restore_v1'; - readonly name = '老照片修复上色'; - readonly description = '将黑白老照片修复并上色,让历史照片重现生机'; - readonly creditCost = 12; - readonly version = '1.0.0'; - readonly input = 'https://file.302.ai/gpt/imgs/20250903/6033b7e701a64e4a97d4608d4a0ed308.jpg'; // 原始图片示例 - readonly output = 'https://file.302.ai/gpt/imgs/20250903/07ed5fb40090ac6ff51c874cc75ea8f2.jpg'; // 输出图片示例 - readonly tags = ['老照片', '修复', '上色', '黑白转彩色', 'AI修复']; + readonly code = 'photo_restore_v1'; + readonly name = '老照片修复上色'; + readonly description = '将黑白老照片修复并上色,让历史照片重现生机'; + readonly creditCost = 12; + readonly version = '1.0.0'; + readonly input = + 'https://file.302.ai/gpt/imgs/20250903/6033b7e701a64e4a97d4608d4a0ed308.jpg'; // 原始图片示例 + readonly output = + 'https://file.302.ai/gpt/imgs/20250903/07ed5fb40090ac6ff51c874cc75ea8f2.jpg'; // 输出图片示例 + readonly tags = ['老照片', '修复', '上色', '黑白转彩色', 'AI修复']; - // N8n模板特定属性 - readonly imageModel = 'gemini-2.5-flash-image-preview'; // nano banana 模型 - readonly imagePrompt = 'Restore this old photo and colorize the entire black and white photo'; -} \ No newline at end of file + // N8n模板特定属性 + readonly imageModel = 'gemini-2.5-flash-image-preview'; // nano banana 模型 + readonly imagePrompt = + 'Restore this old photo and colorize the entire black and white photo'; +} diff --git a/src/templates/n8nTemplates/index.ts b/src/templates/n8nTemplates/index.ts index 3978d3c..4ca20ae 100644 --- a/src/templates/n8nTemplates/index.ts +++ b/src/templates/n8nTemplates/index.ts @@ -5,4 +5,4 @@ export * from './open-eyes.template'; export * from './GarageOpeningTemplate'; export * from './CharacterFigurineTemplate'; export * from './NuclearExplosionTemplate'; -export * from './JapaneseMagazineTemplate'; \ No newline at end of file +export * from './JapaneseMagazineTemplate'; diff --git a/src/templates/n8nTemplates/open-eyes.template.ts b/src/templates/n8nTemplates/open-eyes.template.ts index e83a0ea..ecf4166 100644 --- a/src/templates/n8nTemplates/open-eyes.template.ts +++ b/src/templates/n8nTemplates/open-eyes.template.ts @@ -2,16 +2,19 @@ import { N8nImageGenerateTemplate } from '../n8nTemplate'; // 闭眼照片修复 export class OpenEyesTemplate extends N8nImageGenerateTemplate { - readonly code = 'open_eyes_v1'; - readonly name = '闭眼照片修复'; - readonly description = '修复照片闭眼问题,让人物睁开眼睛'; - readonly creditCost = 10; - readonly version = '1.0.0'; - readonly input = 'https://bowong-ai-shanghai.tos-cn-shanghai.volces.com/images/1756887607688_bjqnk3.png'; // 输出图片示例 - readonly output = 'https://bowong-ai-shanghai.tos-cn-shanghai.volces.com/images/1756889137996_jujhz5.jpg'; // 原始图片示例 - readonly tags = ['闭眼', '照片修复', '人物']; + readonly code = 'open_eyes_v1'; + readonly name = '闭眼照片修复'; + readonly description = '修复照片闭眼问题,让人物睁开眼睛'; + readonly creditCost = 10; + readonly version = '1.0.0'; + readonly input = + 'https://bowong-ai-shanghai.tos-cn-shanghai.volces.com/images/1756887607688_bjqnk3.png'; // 输出图片示例 + readonly output = + 'https://bowong-ai-shanghai.tos-cn-shanghai.volces.com/images/1756889137996_jujhz5.jpg'; // 原始图片示例 + readonly tags = ['闭眼', '照片修复', '人物']; - // N8n模板特定属性 - readonly imageModel = 'gemini-2.5-flash-image-preview'; // nano banana 模型 - readonly imagePrompt = '图像中的主体(人物、动物、meme、或者表情等包含广义上五官的东西)闭上眼睛'; + // N8n模板特定属性 + readonly imageModel = 'gemini-2.5-flash-image-preview'; // nano banana 模型 + readonly imagePrompt = + '图像中的主体(人物、动物、meme、或者表情等包含广义上五官的东西)闭上眼睛'; } diff --git a/src/templates/template.service.ts b/src/templates/template.service.ts index a81d7fb..97b59de 100644 --- a/src/templates/template.service.ts +++ b/src/templates/template.service.ts @@ -1,14 +1,21 @@ import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; import { TemplateManager, Template } from './types'; -import { OpenEyesTemplate, PhotoRestoreTemplate, CosplayRealPersonTemplate, PetFigurineTemplate, GarageOpeningTemplate, CharacterFigurineTemplate, NuclearExplosionTemplate, JapaneseMagazineTemplate } from './n8nTemplates'; +import { + OpenEyesTemplate, + PhotoRestoreTemplate, + CosplayRealPersonTemplate, + PetFigurineTemplate, + GarageOpeningTemplate, + CharacterFigurineTemplate, + NuclearExplosionTemplate, + JapaneseMagazineTemplate, +} from './n8nTemplates'; @Injectable() export class TemplateService implements OnModuleInit { private readonly logger = new Logger(TemplateService.name); - constructor( - private readonly templateManager: TemplateManager, - ) {} + constructor(private readonly templateManager: TemplateManager) {} async onModuleInit() { // 启动时自动注册所有模板 @@ -16,10 +23,7 @@ export class TemplateService implements OnModuleInit { } // 执行模板 - async executeTemplate( - templateCode: string, - input: string - ) { + async executeTemplate(templateCode: string, input: string) { // 1. 获取模板信息 const template = this.templateManager.getTemplate(templateCode); if (!template) { @@ -38,7 +42,10 @@ export class TemplateService implements OnModuleInit { // } // 3. 执行模板 - const result = await this.templateManager.executeTemplate(templateCode, input); + const result = await this.templateManager.executeTemplate( + templateCode, + input, + ); // 4. 扣除积分(仅在成功时) // if (result.success) { diff --git a/src/templates/types.ts b/src/templates/types.ts index fab4f04..8e1a8c0 100644 --- a/src/templates/types.ts +++ b/src/templates/types.ts @@ -1,71 +1,71 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable } from '@nestjs/common'; // 基础模板抽象类 export abstract class Template { - abstract readonly code: string; // 模板唯一标识码 - abstract readonly name: string; // 模板名称 - abstract readonly description: string; // 模板详细描述 - abstract readonly creditCost: number; // 积分消耗 - abstract readonly version: string; // 版本号 - abstract readonly input: string; // 原始图片 - abstract readonly output: string; // 输出图片 - abstract readonly tags: string[]; // 标签数组 + abstract readonly code: string; // 模板唯一标识码 + abstract readonly name: string; // 模板名称 + abstract readonly description: string; // 模板详细描述 + abstract readonly creditCost: number; // 积分消耗 + abstract readonly version: string; // 版本号 + abstract readonly input: string; // 原始图片 + abstract readonly output: string; // 输出图片 + abstract readonly tags: string[]; // 标签数组 } export type VideoGenerateResultUrl = string; // 图片生成模板抽象类 export abstract class ImageGenerateTemplate extends Template { - abstract execute(imageUrl: string): Promise; + abstract execute(imageUrl: string): Promise; } // 视频生成模板抽象类 export abstract class VideoGenerateTemplate extends Template { - abstract execute(imageUrl: string): Promise; + abstract execute(imageUrl: string): Promise; } // 模板管理器类 @Injectable() export class TemplateManager { - private readonly templates = new Map(); + private readonly templates = new Map(); - // 注册模板 - registerTemplate(template: Template): void { - this.templates.set(template.code, template); - console.log(`模板 ${template.code} 注册成功`); + // 注册模板 + registerTemplate(template: Template): void { + this.templates.set(template.code, template); + console.log(`模板 ${template.code} 注册成功`); + } + + // 批量注册模板 + registerTemplates(templates: Template[]): void { + templates.forEach((template) => this.registerTemplate(template)); + } + + // 获取模板 + getTemplate(templateCode: string): Template | null { + return this.templates.get(templateCode) || null; + } + + // 获取所有模板 + getAllTemplates(): Template[] { + return Array.from(this.templates.values()); + } + + // 执行模板 + async executeTemplate(templateCode: string, input: string): Promise { + const template = this.getTemplate(templateCode); + + if (!template) { + throw new Error(`模板 ${templateCode} 不存在`); } - // 批量注册模板 - registerTemplates(templates: Template[]): void { - templates.forEach(template => this.registerTemplate(template)); + if (template instanceof ImageGenerateTemplate) { + return await template.execute(input); } - // 获取模板 - getTemplate(templateCode: string): Template | null { - return this.templates.get(templateCode) || null; + if (template instanceof VideoGenerateTemplate) { + return await template.execute(input); } - // 获取所有模板 - getAllTemplates(): Template[] { - return Array.from(this.templates.values()); - } - - // 执行模板 - async executeTemplate(templateCode: string, input: string): Promise { - const template = this.getTemplate(templateCode); - - if (!template) { - throw new Error(`模板 ${templateCode} 不存在`); - } - - if (template instanceof ImageGenerateTemplate) { - return await template.execute(input); - } - - if (template instanceof VideoGenerateTemplate) { - return await template.execute(input); - } - - throw new Error(`未知的模板类型: ${templateCode}`); - } -} \ No newline at end of file + throw new Error(`未知的模板类型: ${templateCode}`); + } +}