# 平台适配器模块设计方案 ## 🏗️ 核心架构设计 基于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最佳实践,采用依赖注入和模块化架构,确保代码质量和扩展性。*