短信验证绑定日志内容
This commit is contained in:
@@ -15,6 +15,7 @@ import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import { RedisService } from '../../common/redis/redis.service';
|
||||
import { SMS_PROVIDER, WECHAT_PROVIDER } from '../../integrations/integrations.constants';
|
||||
import { ISmsProvider } from '../../integrations/sms/sms.interface';
|
||||
import type { SmsActorRef } from '../../integrations/sms/sms.interface';
|
||||
import { SmsCodeStore } from '../../integrations/sms/sms-code.store';
|
||||
import type { IWechatProvider } from '../../integrations/wechat/wechat.interface';
|
||||
import { serializeBigInt } from '../../common/decorators/current-user.decorator';
|
||||
@@ -69,18 +70,144 @@ export class AuthService {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
async sendSms(phone: string, scene: string) {
|
||||
private maskPhone(phone: string) {
|
||||
return `${phone.slice(0, 3)}****${phone.slice(-4)}`;
|
||||
}
|
||||
|
||||
private clientAppForScene(scene: string): ClientApp {
|
||||
switch (scene) {
|
||||
case SmsScene.STORE_LOGIN:
|
||||
return ClientApp.SHOP_H5;
|
||||
case SmsScene.PARTNER_LOGIN:
|
||||
case SmsScene.PARTNER_STAFF_ADD:
|
||||
return ClientApp.PARTNER_H5;
|
||||
case SmsScene.HQ_LOGIN:
|
||||
return ClientApp.HQ_WEB;
|
||||
default:
|
||||
return ClientApp.USER_H5;
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveSmsActorRef(
|
||||
phone: string,
|
||||
scene: string,
|
||||
guestUserId?: bigint,
|
||||
): Promise<SmsActorRef | undefined> {
|
||||
switch (scene) {
|
||||
case SmsScene.USER_LOGIN:
|
||||
case SmsScene.BIND_PHONE: {
|
||||
if (guestUserId) return { refType: 'USER', refId: guestUserId };
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { phone },
|
||||
select: { id: true },
|
||||
});
|
||||
return user ? { refType: 'USER', refId: user.id } : undefined;
|
||||
}
|
||||
case SmsScene.STORE_LOGIN: {
|
||||
const account = await this.prisma.storeAccount.findUnique({
|
||||
where: { phone },
|
||||
select: { id: true },
|
||||
});
|
||||
return account ? { refType: 'STORE', refId: account.id } : undefined;
|
||||
}
|
||||
case SmsScene.PARTNER_LOGIN:
|
||||
case SmsScene.PARTNER_STAFF_ADD: {
|
||||
const account = await this.prisma.partnerAccount.findUnique({
|
||||
where: { phone },
|
||||
select: { id: true },
|
||||
});
|
||||
return account ? { refType: 'PARTNER', refId: account.id } : undefined;
|
||||
}
|
||||
case SmsScene.HQ_LOGIN: {
|
||||
const account = await this.prisma.hqAccount.findUnique({
|
||||
where: { phone },
|
||||
select: { id: true },
|
||||
});
|
||||
return account ? { refType: 'HQ', refId: account.id } : undefined;
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private trackSmsUserEvent(
|
||||
userId: bigint | undefined,
|
||||
clientApp: ClientApp | string,
|
||||
eventName: string,
|
||||
extraJson: Record<string, unknown>,
|
||||
thirdPartyLogId?: bigint,
|
||||
) {
|
||||
if (!userId) return;
|
||||
this.analyticsService.trackOneSafe(userId, clientApp, {
|
||||
eventName,
|
||||
refType: thirdPartyLogId ? 'THIRD_PARTY_LOG' : undefined,
|
||||
refId: thirdPartyLogId,
|
||||
extraJson,
|
||||
});
|
||||
}
|
||||
|
||||
private async verifySmsForUser(
|
||||
phone: string,
|
||||
code: string,
|
||||
scene: SmsScene.USER_LOGIN | SmsScene.BIND_PHONE,
|
||||
clientApp: ClientApp,
|
||||
userId?: bigint,
|
||||
) {
|
||||
try {
|
||||
await this.smsProvider.verify(phone, code, scene);
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestException) {
|
||||
this.trackSmsUserEvent(userId, clientApp, 'sms_verify_fail', {
|
||||
scene,
|
||||
phone: this.maskPhone(phone),
|
||||
reason: err.message,
|
||||
});
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async sendSms(
|
||||
phone: string,
|
||||
scene: string,
|
||||
opts?: { guestUserId?: bigint; clientApp?: ClientApp },
|
||||
) {
|
||||
const normalizedPhone = this.assertMobilePhone(phone);
|
||||
if (!Object.values(SmsScene).includes(scene as SmsScene)) {
|
||||
throw new BadRequestException('无效的验证码场景');
|
||||
}
|
||||
const clientApp = opts?.clientApp ?? this.clientAppForScene(scene);
|
||||
const actorRef = await this.resolveSmsActorRef(normalizedPhone, scene, opts?.guestUserId);
|
||||
const userId = actorRef?.refType === 'USER' ? actorRef.refId : opts?.guestUserId;
|
||||
|
||||
await this.smsCodeStore.assertSendCooldown(normalizedPhone);
|
||||
try {
|
||||
await this.smsProvider.send(normalizedPhone, scene);
|
||||
const result = await this.smsProvider.send(normalizedPhone, scene, actorRef);
|
||||
await this.smsCodeStore.setSendCooldown(normalizedPhone);
|
||||
this.trackSmsUserEvent(
|
||||
userId,
|
||||
clientApp,
|
||||
'sms_send',
|
||||
{
|
||||
scene,
|
||||
phone: this.maskPhone(normalizedPhone),
|
||||
status: result.ok ? 'success' : 'failed',
|
||||
...(result.errorMessage ? { message: result.errorMessage } : {}),
|
||||
},
|
||||
result.logId,
|
||||
);
|
||||
if (!result.ok) {
|
||||
throw new BadRequestException(result.errorMessage ?? '短信发送失败');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof BadRequestException) throw err;
|
||||
const message = err instanceof Error ? err.message : '短信发送失败';
|
||||
this.trackSmsUserEvent(userId, clientApp, 'sms_send', {
|
||||
scene,
|
||||
phone: this.maskPhone(normalizedPhone),
|
||||
status: 'failed',
|
||||
message,
|
||||
});
|
||||
throw new BadRequestException(message);
|
||||
}
|
||||
return { sent: true };
|
||||
@@ -138,7 +265,17 @@ export class AuthService {
|
||||
|
||||
async loginUser(phone: string, code: string, clientApp: ClientApp, guestId?: bigint) {
|
||||
const normalizedPhone = this.assertMobilePhone(phone);
|
||||
await this.smsProvider.verify(normalizedPhone, code, SmsScene.USER_LOGIN);
|
||||
const existingUser = await this.prisma.user.findUnique({
|
||||
where: { phone: normalizedPhone },
|
||||
select: { id: true },
|
||||
});
|
||||
await this.verifySmsForUser(
|
||||
normalizedPhone,
|
||||
code,
|
||||
SmsScene.USER_LOGIN,
|
||||
clientApp,
|
||||
guestId ?? existingUser?.id,
|
||||
);
|
||||
let user: UserRow | null = await this.prisma.user.findUnique({
|
||||
where: { phone: normalizedPhone },
|
||||
include: { avatar: true },
|
||||
@@ -211,7 +348,7 @@ export class AuthService {
|
||||
|
||||
async bindPhone(actorId: bigint, phone: string, code: string, clientApp: ClientApp) {
|
||||
const normalizedPhone = this.assertMobilePhone(phone);
|
||||
await this.smsProvider.verify(normalizedPhone, code, SmsScene.BIND_PHONE);
|
||||
await this.verifySmsForUser(normalizedPhone, code, SmsScene.BIND_PHONE, clientApp, actorId);
|
||||
const guest = await this.assertActiveUser(actorId);
|
||||
|
||||
if (guest.phone && guest.phoneVerifiedAt) {
|
||||
@@ -243,6 +380,10 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
this.trackSmsUserEvent(targetUser.id, clientApp, 'bind_phone', {
|
||||
phone: this.maskPhone(normalizedPhone),
|
||||
});
|
||||
|
||||
return this.buildSessionResponse(targetUser, clientApp, targetUser.deviceKey);
|
||||
}
|
||||
|
||||
@@ -420,12 +561,12 @@ export class AuthService {
|
||||
clientApp: ClientApp,
|
||||
) {
|
||||
this.assertWechatEnabled();
|
||||
await this.smsProvider.verify(phone, code, SmsScene.BIND_PHONE);
|
||||
|
||||
const wxSession = await this.redis.getJson<WxSessionPayload>(`wx:session:${wxSessionKey}`);
|
||||
if (!wxSession) throw new BadRequestException('微信会话已过期,请重新授权');
|
||||
|
||||
const guestId = wxSession.guestId ? BigInt(wxSession.guestId) : undefined;
|
||||
await this.verifySmsForUser(phone, code, SmsScene.BIND_PHONE, clientApp, guestId);
|
||||
const wxUser = await this.prisma.user.findFirst({
|
||||
where: { wxOpenId: wxSession.openId, status: 1, mergedIntoUserId: null },
|
||||
include: { avatar: true },
|
||||
@@ -508,6 +649,10 @@ export class AuthService {
|
||||
if (!targetUserId) throw new BadRequestException('绑定失败');
|
||||
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' },
|
||||
|
||||
Reference in New Issue
Block a user