bw-mini-app-server/src/platform/adapters/wechat.adapter.ts

243 lines
6.7 KiB
TypeScript
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.

import { Injectable, BadRequestException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { HttpService } from '@nestjs/axios';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { BaseAdapter } from './base.adapter';
import { UserEntity } from '../../entities/user.entity';
import { PlatformUserEntity, PlatformType } from '../../entities/platform-user.entity';
import {
PlatformLoginData,
PlatformRegisterData,
UserAuthResult,
PlatformUserInfo,
TokenRefreshResult,
} from '../interfaces/platform.interface';
import crypto from 'crypto';
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;
constructor(
httpService: HttpService,
configService: ConfigService,
@InjectRepository(UserEntity)
userRepository: Repository<UserEntity>,
@InjectRepository(PlatformUserEntity)
platformUserRepository: Repository<PlatformUserEntity>,
jwtService: JwtService,
) {
super(
httpService,
configService,
userRepository,
platformUserRepository,
jwtService,
);
}
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,
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<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 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);
}
}