|
|
@@ -0,0 +1,152 @@
|
|
|
+import axios from 'axios';
|
|
|
+import { UserService } from '../users/user.service';
|
|
|
+import { AuthService } from '../auth/auth.service';
|
|
|
+import { UserEntity as User } from '../users/user.entity';
|
|
|
+import debug from 'debug';
|
|
|
+
|
|
|
+const logger = {
|
|
|
+ info: debug('backend:wechat:info'),
|
|
|
+ error: debug('backend:wechat:error')
|
|
|
+};
|
|
|
+
|
|
|
+export class WechatAuthService {
|
|
|
+ private readonly appId: string = process.env.WECHAT_MP_APP_ID || '';
|
|
|
+ private readonly appSecret: string = process.env.WECHAT_MP_APP_SECRET || '';
|
|
|
+
|
|
|
+ constructor(
|
|
|
+ private readonly userService: UserService,
|
|
|
+ private readonly authService: AuthService
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ // 获取授权URL
|
|
|
+ getAuthorizationUrl(redirectUri: string, scope: 'snsapi_base' | 'snsapi_userinfo' = 'snsapi_userinfo'): string {
|
|
|
+ const state = Math.random().toString(36).substring(2);
|
|
|
+ return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${this.appId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 通过code获取access_token
|
|
|
+ async getAccessToken(code: string): Promise<any> {
|
|
|
+ const url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${this.appId}&secret=${this.appSecret}&code=${code}&grant_type=authorization_code`;
|
|
|
+ const response = await axios.get(url);
|
|
|
+ return response.data;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取用户信息
|
|
|
+ async getUserInfo(accessToken: string, openid: string): Promise<any> {
|
|
|
+ const url = `https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=${openid}&lang=zh_CN`;
|
|
|
+ const response = await axios.get(url);
|
|
|
+ return response.data;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 微信登录/注册
|
|
|
+ async wechatLogin(code: string): Promise<{ token: string; user: User }> {
|
|
|
+ try {
|
|
|
+ // 1. 获取access_token
|
|
|
+ const tokenData = await this.getAccessToken(code);
|
|
|
+ if (tokenData.errcode) {
|
|
|
+ throw new Error(`微信认证失败: ${tokenData.errmsg}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const { access_token, openid, unionid } = tokenData;
|
|
|
+
|
|
|
+ // 2. 检查用户是否存在
|
|
|
+ let user = await this.userService.getUserByWechatOpenid(openid);
|
|
|
+
|
|
|
+ if (!user) {
|
|
|
+ // 3. 获取用户信息(首次登录)
|
|
|
+ const userInfo = await this.getUserInfo(access_token, openid);
|
|
|
+
|
|
|
+ // 4. 创建新用户
|
|
|
+ user = await this.userService.createUser({
|
|
|
+ username: `wx_${openid.substring(0, 8)}`,
|
|
|
+ password: Math.random().toString(36).substring(2),
|
|
|
+ wechatOpenid: openid,
|
|
|
+ wechatUnionid: unionid,
|
|
|
+ wechatNickname: userInfo.nickname,
|
|
|
+ wechatAvatar: userInfo.headimgurl,
|
|
|
+ wechatSex: userInfo.sex,
|
|
|
+ wechatProvince: userInfo.province,
|
|
|
+ wechatCity: userInfo.city,
|
|
|
+ wechatCountry: userInfo.country,
|
|
|
+ nickname: userInfo.nickname
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 生成JWT token
|
|
|
+ const token = this.authService.generateToken(user);
|
|
|
+
|
|
|
+ return { token, user };
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('微信登录失败:', error);
|
|
|
+ throw new Error(`微信登录失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绑定微信账号到现有用户
|
|
|
+ async bindWechatToUser(userId: number, code: string): Promise<User> {
|
|
|
+ try {
|
|
|
+ // 1. 获取access_token
|
|
|
+ const tokenData = await this.getAccessToken(code);
|
|
|
+ if (tokenData.errcode) {
|
|
|
+ throw new Error(`微信认证失败: ${tokenData.errmsg}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const { openid, unionid } = tokenData;
|
|
|
+
|
|
|
+ // 2. 检查微信账号是否已被绑定
|
|
|
+ const existingUser = await this.userService.getUserByWechatOpenid(openid);
|
|
|
+ if (existingUser && existingUser.id !== userId) {
|
|
|
+ throw new Error('该微信账号已被其他用户绑定');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 获取用户信息
|
|
|
+ const userInfo = await this.getUserInfo(tokenData.access_token, openid);
|
|
|
+
|
|
|
+ // 4. 更新用户信息
|
|
|
+ const user = await this.userService.updateUser(userId, {
|
|
|
+ wechatOpenid: openid,
|
|
|
+ wechatUnionid: unionid,
|
|
|
+ wechatNickname: userInfo.nickname,
|
|
|
+ wechatAvatar: userInfo.headimgurl,
|
|
|
+ wechatSex: userInfo.sex,
|
|
|
+ wechatProvince: userInfo.province,
|
|
|
+ wechatCity: userInfo.city,
|
|
|
+ wechatCountry: userInfo.country
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!user) {
|
|
|
+ throw new Error('用户不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ return user;
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('绑定微信账号失败:', error);
|
|
|
+ throw new Error(`绑定微信账号失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 解绑微信账号
|
|
|
+ async unbindWechatFromUser(userId: number): Promise<User> {
|
|
|
+ try {
|
|
|
+ const user = await this.userService.updateUser(userId, {
|
|
|
+ wechatOpenid: null,
|
|
|
+ wechatUnionid: null,
|
|
|
+ wechatNickname: null,
|
|
|
+ wechatAvatar: null,
|
|
|
+ wechatSex: null,
|
|
|
+ wechatProvince: null,
|
|
|
+ wechatCity: null,
|
|
|
+ wechatCountry: null
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!user) {
|
|
|
+ throw new Error('用户不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ return user;
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('解绑微信账号失败:', error);
|
|
|
+ throw new Error(`解绑微信账号失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|