bw-mini-app-server/docs/platform-adapter-design.md

1280 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 平台适配器模块设计方案
## 🏗️ 核心架构设计
基于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<UserAuthResult>;
getUserInfo(token: string, platformUserId: string): Promise<PlatformUserInfo>;
refreshToken(refreshToken: string): Promise<TokenRefreshResult>;
// 用户注册相关
register(registerData: PlatformRegisterData): Promise<UserAuthResult>;
// 用户信息管理
updateUserInfo(userId: string, updateData: Partial<PlatformUserInfo>): Promise<void>;
// 平台特定功能
validateToken(token: string): Promise<boolean>;
revokeToken(token: string): Promise<void>;
}
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<User>,
protected readonly platformUserRepository: Repository<PlatformUser>,
protected readonly jwtService: JwtService,
) {}
/**
* 查找或创建统一用户
* 根据平台用户信息查找现有用户,如不存在则创建新用户
*/
async findOrCreateUnifiedUser(platformUserData: PlatformUserInfo): Promise<User> {
// 查找是否已存在平台用户绑定
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<User> {
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<PlatformUser> {
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<void> {
platformUser.platformData = { ...platformUser.platformData, ...newData };
await this.platformUserRepository.save(platformUser);
}
/**
* 更新平台用户认证数据
*/
protected async updatePlatformUserData(userId: string, authData: any, userInfo?: any): Promise<void> {
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<UserAuthResult>;
abstract getUserInfo(token: string, platformUserId: string): Promise<PlatformUserInfo>;
abstract refreshToken(refreshToken: string): Promise<TokenRefreshResult>;
abstract register(registerData: PlatformRegisterData): Promise<UserAuthResult>;
abstract updateUserInfo(userId: string, updateData: Partial<PlatformUserInfo>): Promise<void>;
abstract validateToken(token: string): Promise<boolean>;
abstract revokeToken(token: string): Promise<void>;
}
```
## 📱 微信适配器实现
```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<UserAuthResult> {
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<UserAuthResult> {
// 微信小程序通常通过login方法完成注册
return this.login(registerData);
}
async getUserInfo(token: string, platformUserId: string): Promise<PlatformUserInfo> {
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<PlatformUserInfo>): Promise<void> {
// 更新统一用户信息
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<TokenRefreshResult> {
// 微信小程序的session_key通常不支持刷新需要重新登录
throw new BadRequestException('微信平台需要重新登录');
}
async validateToken(token: string): Promise<boolean> {
try {
// 验证微信session_key是否有效
// 通常通过调用微信API验证
return true;
} catch (error) {
return false;
}
}
async revokeToken(token: string): Promise<void> {
// 微信平台令牌撤销逻辑
// 清除本地存储的session_key
}
/**
* 使用code获取微信授权信息
*/
private async getWechatAuth(code: string): Promise<WechatAuthResponse> {
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<UserAuthResult> {
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<UserAuthResult> {
// 抖音小程序通常通过login方法完成注册
return this.login(registerData);
}
async getUserInfo(token: string, platformUserId: string): Promise<PlatformUserInfo> {
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<PlatformUserInfo>): Promise<void> {
// 更新统一用户信息
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<TokenRefreshResult> {
// 抖音小程序的session_key需要重新登录获取不支持刷新
throw new BadRequestException('抖音平台需要重新登录');
}
async validateToken(token: string): Promise<boolean> {
try {
// 抖音小程序验证session_key有效性
// 通常通过checkSession或API调用验证
return true;
} catch (error) {
return false;
}
}
async revokeToken(token: string): Promise<void> {
// 抖音平台session_key撤销逻辑
// session_key无需特殊撤销清除本地存储即可
}
/**
* 获取抖音授权信息
*/
private async getBytedanceAuth(code: string): Promise<BytedanceAuthResponse> {
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<PlatformType, IPlatformAdapter>();
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<User>,
@InjectRepository(PlatformUser)
private readonly platformUserRepository: Repository<PlatformUser>,
) {}
/**
* 统一登录接口
*/
async login(platform: PlatformType, loginData: PlatformLoginData): Promise<UserAuthResult> {
const adapter = this.platformAdapterFactory.getAdapter(platform);
return adapter.login(loginData);
}
/**
* 统一注册接口
*/
async register(platform: PlatformType, registerData: PlatformRegisterData): Promise<UserAuthResult> {
const adapter = this.platformAdapterFactory.getAdapter(platform);
return adapter.register(registerData);
}
/**
* 获取用户信息
*/
async getUserInfo(userId: string): Promise<UnifiedUserInfo> {
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<PlatformUserInfo> {
const adapter = this.platformAdapterFactory.getAdapter(platform);
return adapter.getUserInfo(token, platformUserId);
}
/**
* 更新用户信息
*/
async updateUserInfo(platform: PlatformType, userId: string, updateData: Partial<PlatformUserInfo>): Promise<void> {
const adapter = this.platformAdapterFactory.getAdapter(platform);
return adapter.updateUserInfo(userId, updateData);
}
/**
* 绑定新平台账号
*/
async bindPlatform(userId: string, platform: PlatformType, loginData: PlatformLoginData): Promise<void> {
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<void> {
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<TokenRefreshResult> {
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<boolean> {
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最佳实践采用依赖注入和模块化架构确保代码质量和扩展性。*