fix(auth): transfer WeChat id on merge; stop false phone prompts
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
Merge used to drop wxOpenId so OAuth created a new guest without phone every time. Now migrate WeChat identity, recover legacy merged openIds, return accountMerged and force page reload, and only soft-prompt when phone is truly unbound.
This commit is contained in:
@@ -784,6 +784,15 @@ export class AuthService {
|
||||
}
|
||||
if (guestId && guestId !== user.id) {
|
||||
user = await this.mergeUsers(guestId, user.id);
|
||||
this.analyticsService.trackOneSafe(user.id, clientApp, {
|
||||
eventName: 'sms_login',
|
||||
extraJson: { method: 'sms', accountMerged: true },
|
||||
});
|
||||
this.analyticsService.trackOneSafe(user.id, clientApp, {
|
||||
eventName: 'login_success',
|
||||
extraJson: { method: 'sms', accountMerged: true },
|
||||
});
|
||||
return this.buildSessionResponse(user, clientApp, user.deviceKey, { accountMerged: true });
|
||||
} else {
|
||||
await this.assertActiveUser(user.id);
|
||||
}
|
||||
@@ -817,6 +826,7 @@ export class AuthService {
|
||||
|
||||
const existing = await this.prisma.user.findUnique({ where: { phone: normalizedPhone } });
|
||||
let targetUser: UserRow;
|
||||
let accountMerged = false;
|
||||
|
||||
if (!existing) {
|
||||
targetUser = await this.prisma.user.update({
|
||||
@@ -834,6 +844,14 @@ export class AuthService {
|
||||
targetUser = existing;
|
||||
} else {
|
||||
targetUser = await this.mergeUsers(guest.id, existing.id);
|
||||
accountMerged = true;
|
||||
if (!targetUser.phoneVerifiedAt) {
|
||||
targetUser = await this.prisma.user.update({
|
||||
where: { id: targetUser.id },
|
||||
data: { phoneVerifiedAt: new Date() },
|
||||
include: { avatar: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -841,7 +859,9 @@ export class AuthService {
|
||||
phone: this.maskPhone(normalizedPhone),
|
||||
});
|
||||
|
||||
return this.buildSessionResponse(targetUser, clientApp, targetUser.deviceKey);
|
||||
return this.buildSessionResponse(targetUser, clientApp, targetUser.deviceKey, {
|
||||
accountMerged,
|
||||
});
|
||||
}
|
||||
|
||||
async loginStore(phone: string, code: string, clientApp: ClientApp) {
|
||||
@@ -1061,8 +1081,14 @@ export class AuthService {
|
||||
|
||||
// 微信登录不再强制绑定手机号;phoneVerified=false 也可签发会话,下单页仅提示可选绑定
|
||||
if (user) {
|
||||
let activeUser: UserRow =
|
||||
guestId && guestId !== user.id ? await this.mergeUsers(guestId, user.id) : (user as UserRow);
|
||||
let accountMerged = false;
|
||||
let activeUser: UserRow;
|
||||
if (guestId && guestId !== user.id) {
|
||||
activeUser = await this.mergeUsers(guestId, user.id);
|
||||
accountMerged = true;
|
||||
} else {
|
||||
activeUser = user as UserRow;
|
||||
}
|
||||
activeUser = await this.prisma.user.update({
|
||||
where: { id: activeUser.id },
|
||||
data: {
|
||||
@@ -1076,13 +1102,54 @@ export class AuthService {
|
||||
}
|
||||
this.analyticsService.trackOneSafe(activeUser.id, clientApp, {
|
||||
eventName: 'wechat_login',
|
||||
extraJson: { platform },
|
||||
extraJson: { platform, accountMerged },
|
||||
});
|
||||
this.analyticsService.trackOneSafe(activeUser.id, clientApp, {
|
||||
eventName: 'login_success',
|
||||
extraJson: { method: 'wechat', platform },
|
||||
extraJson: { method: 'wechat', platform, accountMerged },
|
||||
});
|
||||
return this.buildSessionResponse(activeUser, clientApp, activeUser.deviceKey);
|
||||
return this.buildSessionResponse(activeUser, clientApp, activeUser.deviceKey, { accountMerged });
|
||||
}
|
||||
|
||||
// 历史脏数据:openId 仍在已合并访客上 → 跟随主账号并补挂微信身份
|
||||
const legacyMerged = await this.prisma.user.findFirst({
|
||||
where: { wxOpenId: session.openId, mergedIntoUserId: { not: null } },
|
||||
select: { mergedIntoUserId: true },
|
||||
});
|
||||
if (legacyMerged?.mergedIntoUserId) {
|
||||
let primary = await this.assertActiveUser(legacyMerged.mergedIntoUserId);
|
||||
if (!primary.wxOpenId) {
|
||||
primary = await this.prisma.user.update({
|
||||
where: { id: primary.id },
|
||||
data: {
|
||||
wxOpenId: session.openId,
|
||||
wxUnionId: session.unionId ?? primary.wxUnionId,
|
||||
},
|
||||
include: { avatar: true },
|
||||
});
|
||||
}
|
||||
await this.prisma.user.updateMany({
|
||||
where: { wxOpenId: session.openId, id: { not: primary.id } },
|
||||
data: { wxOpenId: null, wxUnionId: null },
|
||||
});
|
||||
let accountMerged = false;
|
||||
if (guestId && guestId !== primary.id) {
|
||||
primary = await this.mergeUsers(guestId, primary.id);
|
||||
accountMerged = true;
|
||||
}
|
||||
if (session.accessToken) {
|
||||
const synced = await this.syncWechatUserProfile(primary.id, session.accessToken, session.openId);
|
||||
if (synced) primary = synced as UserRow;
|
||||
}
|
||||
this.analyticsService.trackOneSafe(primary.id, clientApp, {
|
||||
eventName: 'wechat_login',
|
||||
extraJson: { platform, accountMerged, recoveredFromMerge: true },
|
||||
});
|
||||
this.analyticsService.trackOneSafe(primary.id, clientApp, {
|
||||
eventName: 'login_success',
|
||||
extraJson: { method: 'wechat', platform, accountMerged },
|
||||
});
|
||||
return this.buildSessionResponse(primary, clientApp, primary.deviceKey, { accountMerged });
|
||||
}
|
||||
|
||||
if (guestId) {
|
||||
@@ -1201,9 +1268,29 @@ export class AuthService {
|
||||
},
|
||||
});
|
||||
targetUserId = updated.id;
|
||||
let accountMerged = false;
|
||||
if (guestId && guestId !== updated.id) {
|
||||
targetUserId = (await this.mergeUsers(guestId, updated.id)).id;
|
||||
accountMerged = true;
|
||||
}
|
||||
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', {
|
||||
phone: this.maskPhone(phone),
|
||||
method: 'wechat',
|
||||
});
|
||||
this.analyticsService.trackOneSafe(user.id, clientApp, {
|
||||
eventName: 'wechat_phone',
|
||||
extraJson: { method: 'bind_phone', accountMerged },
|
||||
});
|
||||
this.analyticsService.trackOneSafe(user.id, clientApp, {
|
||||
eventName: 'login_success',
|
||||
extraJson: { method: 'wechat_bind', accountMerged },
|
||||
});
|
||||
return this.buildSessionResponse(user, clientApp, user.deviceKey, { accountMerged });
|
||||
} else if (wxUser) {
|
||||
if (wxUser.phone && wxUser.phone !== phone) {
|
||||
throw new BadRequestException('手机号已被其他账号占用');
|
||||
@@ -1531,6 +1618,30 @@ export class AuthService {
|
||||
throw new BadRequestException('目标账号无效');
|
||||
}
|
||||
|
||||
// 先迁微信身份到主账号,再清空访客,否则下次 OAuth 找不到 openId 又会建无手机号访客
|
||||
const guestWxOpenId = guest.wxOpenId;
|
||||
const guestWxUnionId = guest.wxUnionId;
|
||||
if (guestWxOpenId || guestWxUnionId) {
|
||||
await tx.user.update({
|
||||
where: { id: guestId },
|
||||
data: { wxOpenId: null, wxUnionId: null },
|
||||
});
|
||||
if (guestWxOpenId && !primary.wxOpenId) {
|
||||
await tx.user.update({
|
||||
where: { id: primaryId },
|
||||
data: {
|
||||
wxOpenId: guestWxOpenId,
|
||||
wxUnionId: guestWxUnionId ?? primary.wxUnionId,
|
||||
},
|
||||
});
|
||||
} else if (guestWxUnionId && !primary.wxUnionId) {
|
||||
await tx.user.update({
|
||||
where: { id: primaryId },
|
||||
data: { wxUnionId: guestWxUnionId },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await tx.order.updateMany({ where: { userId: guestId }, data: { userId: primaryId } });
|
||||
await tx.userAddress.updateMany({ where: { userId: guestId }, data: { userId: primaryId } });
|
||||
await tx.benefitCoupon.updateMany({ where: { userId: guestId }, data: { userId: primaryId } });
|
||||
@@ -1588,12 +1699,34 @@ export class AuthService {
|
||||
await tx.user.update({ where: { id: primaryId }, data: { deviceKey: deviceKeyToTransfer } });
|
||||
}
|
||||
|
||||
if (!primary.avatarResourceId && guest.avatarResourceId) {
|
||||
await tx.user.update({
|
||||
where: { id: primaryId },
|
||||
data: { avatarResourceId: guest.avatarResourceId },
|
||||
});
|
||||
await tx.user.update({
|
||||
where: { id: guestId },
|
||||
data: { avatarResourceId: null },
|
||||
});
|
||||
}
|
||||
|
||||
if ((!primary.nickname || primary.nickname === '访客' || /^用户\d{4}$/.test(primary.nickname))
|
||||
&& guest.nickname
|
||||
&& guest.nickname !== '访客') {
|
||||
await tx.user.update({
|
||||
where: { id: primaryId },
|
||||
data: { nickname: guest.nickname },
|
||||
});
|
||||
}
|
||||
|
||||
await tx.user.update({
|
||||
where: { id: guestId },
|
||||
data: {
|
||||
mergedIntoUserId: primaryId,
|
||||
status: 0,
|
||||
deviceKey: null,
|
||||
wxOpenId: null,
|
||||
wxUnionId: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1617,9 +1750,26 @@ export class AuthService {
|
||||
return user;
|
||||
}
|
||||
|
||||
private buildSessionResponse(user: UserRow, clientApp: ClientApp, deviceKey: string | null) {
|
||||
private buildSessionResponse(
|
||||
user: UserRow,
|
||||
clientApp: ClientApp,
|
||||
deviceKey: string | null,
|
||||
extras?: { accountMerged?: boolean },
|
||||
) {
|
||||
const phoneVerified = !!user.phoneVerifiedAt;
|
||||
return this.issueToken('USER', user.id, clientApp, phoneVerified, this.formatUserProfile(user), undefined, undefined, deviceKey);
|
||||
return {
|
||||
...this.issueToken(
|
||||
'USER',
|
||||
user.id,
|
||||
clientApp,
|
||||
phoneVerified,
|
||||
this.formatUserProfile(user),
|
||||
undefined,
|
||||
undefined,
|
||||
deviceKey,
|
||||
),
|
||||
...(extras?.accountMerged ? { accountMerged: true as const } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
private isDefaultNickname(nickname: string | null | undefined) {
|
||||
|
||||
Reference in New Issue
Block a user