微信授权逻辑修改

This commit is contained in:
2026-07-06 20:56:25 +08:00
parent b12ee13232
commit 9d221cfcd2
12 changed files with 475 additions and 28 deletions
@@ -3,6 +3,7 @@ import type { Request } from 'express';
import { AuthService } from './auth.service';
import {
BindPhoneDto,
BindWechatDto,
BindWechatPhoneDto,
BootstrapSessionDto,
LoginSmsDto,
@@ -78,6 +79,17 @@ export class UserAuthController {
);
}
@Post('auth/wechat/bind')
@UseGuards(JwtAuthGuard)
bindWechat(@CurrentUser() user: AuthUser, @Body() dto: BindWechatDto) {
return this.authService.bindUserWechat(
user.actorId,
{ code: dto.code, wxSessionKey: dto.wxSessionKey },
ClientApp.USER_H5,
dto.platform ?? 'h5',
);
}
@Get('auth/me')
@UseGuards(JwtAuthGuard)
me(@CurrentUser() user: AuthUser) {
@@ -28,6 +28,7 @@ type WxSessionPayload = {
openId: string;
unionId?: string;
sessionKey?: string;
accessToken?: string;
clientApp: ClientApp;
guestId?: string;
};
@@ -512,6 +513,10 @@ export class AuthService {
},
include: { avatar: true },
});
if (session.accessToken) {
const synced = await this.syncWechatUserProfile(activeUser.id, session.accessToken, session.openId);
if (synced) activeUser = synced as UserRow;
}
this.analyticsService.trackOneSafe(activeUser.id, clientApp, {
eventName: 'wechat_login',
extraJson: { platform },
@@ -523,6 +528,18 @@ export class AuthService {
return this.buildSessionResponse(activeUser, clientApp, activeUser.deviceKey);
}
if (guestId) {
try {
const guest = await this.assertActiveUser(guestId);
if (guest.phoneVerifiedAt && !guest.wxOpenId) {
const activeUser = await this.attachWechatToUser(guest.id, session, clientApp, platform);
return this.buildSessionResponse(activeUser, clientApp, activeUser.deviceKey);
}
} catch (e) {
if (e instanceof BadRequestException) throw e;
}
}
const wxSessionKey = randomUUID();
await this.redis.setJson(
`wx:session:${wxSessionKey}`,
@@ -530,6 +547,7 @@ export class AuthService {
openId: session.openId,
unionId: session.unionId,
sessionKey: 'sessionKey' in session ? session.sessionKey : undefined,
accessToken: session.accessToken,
clientApp,
guestId: guestId?.toString(),
} satisfies WxSessionPayload,
@@ -647,6 +665,9 @@ export class AuthService {
}
if (!targetUserId) throw new BadRequestException('绑定失败');
if (wxSession.accessToken) {
await this.syncWechatUserProfile(targetUserId, wxSession.accessToken, wxSession.openId);
}
const user = await this.assertActiveUser(targetUserId);
await this.redis.del(`wx:session:${wxSessionKey}`);
this.trackSmsUserEvent(user.id, clientApp, 'bind_phone', {
@@ -664,6 +685,53 @@ export class AuthService {
return this.buildSessionResponse(user, clientApp, user.deviceKey);
}
async bindUserWechat(
userId: bigint,
input: { code?: string; wxSessionKey?: string },
clientApp: ClientApp,
platform: 'h5' | 'mini' = 'h5',
) {
this.assertWechatEnabled();
if (!input.code && !input.wxSessionKey) {
throw new BadRequestException('请提供微信授权 code 或会话');
}
let openId: string;
let unionId: string | undefined;
let accessToken: string | undefined;
const actorRef = { refType: 'USER', refId: userId };
if (input.code) {
const session =
platform === 'mini'
? await this.wechatProvider.code2Session(input.code, actorRef)
: await this.wechatProvider.oauth2AccessToken(input.code, actorRef);
openId = session.openId;
unionId = session.unionId;
accessToken = session.accessToken;
} else {
const wxSession = await this.redis.getJson<WxSessionPayload>(`wx:session:${input.wxSessionKey}`);
if (!wxSession) throw new BadRequestException('微信会话已过期,请重新授权');
openId = wxSession.openId;
unionId = wxSession.unionId;
accessToken = wxSession.accessToken;
await this.redis.del(`wx:session:${input.wxSessionKey}`);
}
const user = await this.attachWechatToUser(
userId,
{ openId, unionId, accessToken },
clientApp,
platform,
{ skipLoginEvents: true },
);
this.analyticsService.trackOneSafe(userId, clientApp, {
eventName: 'wechat_bind',
extraJson: { platform },
});
return this.buildSessionResponse(user, clientApp, user.deviceKey);
}
async loginStoreWechat(code: string, clientApp: ClientApp, platform: 'h5' | 'mini' = 'h5') {
this.assertWechatEnabled();
const session =
@@ -871,6 +939,113 @@ export class AuthService {
return this.issueToken('USER', user.id, clientApp, phoneVerified, this.formatUserProfile(user), undefined, undefined, deviceKey);
}
private isDefaultNickname(nickname: string | null | undefined) {
if (!nickname || nickname === '访客') return true;
return /^用户\d{4}$/.test(nickname);
}
private async syncWechatUserProfile(
userId: bigint,
accessToken: string,
openId: string,
): Promise<UserRow | null> {
try {
const info = await this.wechatProvider.fetchOAuthUserInfo(accessToken, openId, {
refType: 'USER',
refId: userId,
});
const current = await this.prisma.user.findUnique({
where: { id: userId },
include: { avatar: true },
});
if (!current) return null;
const data: {
nickname?: string;
avatarResourceId?: bigint;
} = {};
if (info.nickname && this.isDefaultNickname(current.nickname)) {
data.nickname = info.nickname;
}
if (info.headImgUrl && !current.avatarResourceId) {
const avatar = await this.prisma.commonResource.create({
data: {
ownerType: 'USER',
ownerId: userId,
bizType: 'AVATAR',
mediaType: 'IMAGE',
ossBucket: 'wechat',
ossKey: `wx-avatar/${openId}`,
url: info.headImgUrl,
status: 'ACTIVE',
},
});
data.avatarResourceId = avatar.id;
}
if (!data.nickname && !data.avatarResourceId) {
return current as UserRow;
}
return this.prisma.user.update({
where: { id: userId },
data,
include: { avatar: true },
});
} catch {
return null;
}
}
private async attachWechatToUser(
userId: bigint,
session: { openId: string; unionId?: string; accessToken?: string },
clientApp: ClientApp,
platform: string,
options?: { skipLoginEvents?: boolean },
): Promise<UserRow> {
const conflict = await this.prisma.user.findFirst({
where: {
wxOpenId: session.openId,
id: { not: userId },
status: 1,
mergedIntoUserId: null,
},
});
if (conflict) {
throw new BadRequestException('该微信已绑定其他账号');
}
let user = (await this.prisma.user.update({
where: { id: userId },
data: {
wxOpenId: session.openId,
wxUnionId: session.unionId ?? undefined,
},
include: { avatar: true },
})) as UserRow;
if (session.accessToken) {
const synced = await this.syncWechatUserProfile(userId, session.accessToken, session.openId);
if (synced) user = synced as UserRow;
}
if (!options?.skipLoginEvents) {
this.analyticsService.trackOneSafe(userId, clientApp, {
eventName: 'wechat_login',
extraJson: { platform },
});
this.analyticsService.trackOneSafe(userId, clientApp, {
eventName: 'login_success',
extraJson: { method: 'wechat', platform },
});
}
return user;
}
private formatUserProfile(user: UserRow) {
return {
id: user.id.toString(),
@@ -68,3 +68,18 @@ export class BindWechatPhoneDto {
@IsNotEmpty()
code: string;
}
export class BindWechatDto {
@IsString()
@IsOptional()
code?: string;
@IsString()
@IsOptional()
wxSessionKey?: string;
@IsString()
@IsIn(['h5', 'mini'])
@IsOptional()
platform?: 'h5' | 'mini';
}