短信验证绑定日志内容

This commit is contained in:
2026-07-06 13:26:31 +08:00
parent 1c978b8adc
commit 67af6d7d53
8 changed files with 226 additions and 42 deletions
@@ -3,7 +3,7 @@ import Dysmsapi20170525, { SendSmsRequest } from '@alicloud/dysmsapi20170525';
import * as OpenApi from '@alicloud/openapi-client';
import { loadAppConfig } from '@dukang/shared-types';
import { PrismaService } from '../../common/prisma/prisma.module';
import { ISmsProvider } from './sms.interface';
import type { ISmsProvider, SmsActorRef, SmsSendResult } from './sms.interface';
import { SmsCodeStore } from './sms-code.store';
function serializeSmsResponseBody(body: unknown): Record<string, string> | undefined {
@@ -19,6 +19,10 @@ function serializeSmsResponseBody(body: unknown): Record<string, string> | undef
return Object.keys(out).length ? out : undefined;
}
function maskPhone(phone: string) {
return `${phone.slice(0, 3)}****${phone.slice(-4)}`;
}
@Injectable()
export class SmsAliyunProvider implements ISmsProvider {
private readonly logger = new Logger(SmsAliyunProvider.name);
@@ -54,8 +58,9 @@ export class SmsAliyunProvider implements ISmsProvider {
return this.client;
}
async send(phone: string, scene: string): Promise<void> {
async send(phone: string, scene: string, actorRef?: SmsActorRef): Promise<SmsSendResult> {
const code = await this.smsCodeStore.generateAndStore(phone, scene);
const masked = maskPhone(phone);
const request = new SendSmsRequest({
phoneNumbers: phone,
signName: this.config.aliyunSmsSignName,
@@ -63,43 +68,53 @@ export class SmsAliyunProvider implements ISmsProvider {
templateParam: JSON.stringify({ code }),
});
let logged = false;
const baseLog = {
provider: 'SMS' as const,
scene,
refType: actorRef?.refType,
refId: actorRef?.refId,
requestBody: {
phone: masked,
templateCode: this.config.aliyunSmsTemplateCode,
signName: this.config.aliyunSmsSignName,
},
};
try {
const response = await this.getClient().sendSms(request);
const bizId = response.body?.bizId ?? undefined;
const ok = response.body?.code === 'OK';
const responseBody = serializeSmsResponseBody(response.body);
await this.prisma.logThirdParty.create({
const log = await this.prisma.logThirdParty.create({
data: {
provider: 'SMS',
scene,
requestBody: { phone, templateCode: this.config.aliyunSmsTemplateCode, signName: this.config.aliyunSmsSignName },
...baseLog,
responseBody,
externalNo: bizId,
status: ok ? 'SUCCESS' : 'FAILED',
errorMessage: ok ? undefined : response.body?.message ?? 'SMS send failed',
},
});
logged = true;
if (!ok) {
throw new Error(response.body?.message ?? '短信发送失败');
if (ok) {
this.logger.log(`Aliyun SMS sent to ${masked} scene=${scene} bizId=${bizId ?? '-'}`);
} else {
this.logger.warn(`Aliyun SMS failed for ${masked} scene=${scene}: ${response.body?.message}`);
}
this.logger.log(`Aliyun SMS sent to ${phone.slice(0, 3)}****${phone.slice(-4)} scene=${scene} bizId=${bizId ?? '-'}`);
return {
logId: log.id,
ok,
errorMessage: ok ? undefined : response.body?.message ?? '短信发送失败',
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.logger.error(`Aliyun SMS send failed: ${message}`);
if (!logged) {
await this.prisma.logThirdParty.create({
data: {
provider: 'SMS',
scene,
requestBody: { phone, templateCode: this.config.aliyunSmsTemplateCode, signName: this.config.aliyunSmsSignName },
status: 'FAILED',
errorMessage: message.slice(0, 512),
},
});
}
throw err;
const log = await this.prisma.logThirdParty.create({
data: {
...baseLog,
status: 'FAILED',
errorMessage: message.slice(0, 512),
},
});
return { logId: log.id, ok: false, errorMessage: message };
}
}
@@ -1,4 +1,15 @@
export type SmsActorRef = {
refType: string;
refId: bigint;
};
export type SmsSendResult = {
logId: bigint;
ok: boolean;
errorMessage?: string;
};
export interface ISmsProvider {
send(phone: string, scene: string): Promise<void>;
send(phone: string, scene: string, actorRef?: SmsActorRef): Promise<SmsSendResult>;
verify(phone: string, code: string, scene: string): Promise<void>;
}
@@ -1,8 +1,12 @@
import { Injectable, Logger } from '@nestjs/common';
import { PrismaService } from '../../common/prisma/prisma.module';
import { ISmsProvider } from './sms.interface';
import type { ISmsProvider, SmsActorRef, SmsSendResult } from './sms.interface';
import { SmsCodeStore } from './sms-code.store';
function maskPhone(phone: string) {
return `${phone.slice(0, 3)}****${phone.slice(-4)}`;
}
@Injectable()
export class SmsMockProvider implements ISmsProvider {
private readonly logger = new Logger(SmsMockProvider.name);
@@ -12,20 +16,23 @@ export class SmsMockProvider implements ISmsProvider {
private readonly prisma: PrismaService,
) {}
async send(phone: string, scene: string): Promise<void> {
async send(phone: string, scene: string, actorRef?: SmsActorRef): Promise<SmsSendResult> {
const code = await this.smsCodeStore.generateAndStore(phone, scene);
const masked = `${phone.slice(0, 3)}****${phone.slice(-4)}`;
const masked = maskPhone(phone);
this.logger.log(`Mock SMS → ${masked} scene=${scene} code=${code}`);
await this.prisma.logThirdParty.create({
const log = await this.prisma.logThirdParty.create({
data: {
provider: 'SMS',
scene,
refType: actorRef?.refType,
refId: actorRef?.refId,
requestBody: { phone: masked, mode: 'MOCK', scene },
responseBody: { mock: true, hint: 'use MOCK_SMS_CODE or check server log' },
status: 'SUCCESS',
},
});
return { logId: log.id, ok: true };
}
async verify(phone: string, code: string, scene: string): Promise<void> {