Files
order_site/apps/backend/src/services/sms/index.ts
T

117 lines
3.8 KiB
TypeScript

import { createRequire } from 'node:module'
import { runtimeConfig } from '../../config/runtime.js'
import { createHttpError } from '../../utils/http.js'
import { logWarn } from '../../utils/logger.js'
const require = createRequire(import.meta.url)
const Dysmsapi = require('@alicloud/dysmsapi20170525') as {
default: new (config: unknown) => AliyunSmsClient
SendSmsRequest: new (input: Record<string, unknown>) => unknown
}
const OpenApiClient = require('@alicloud/openapi-client') as {
Config: new (config: Record<string, unknown>) => unknown
}
type AliyunSmsClient = {
sendSmsWithOptions(
request: unknown,
runtime: unknown,
): Promise<{ body?: { code?: string; message?: string } }>
}
export interface SmsProvider {
sendCode(phone: string, code: string): Promise<void>
}
const ALIYUN_DEFAULT_ENDPOINT = 'dysmsapi.aliyuncs.com'
export function getSmsProvider(): SmsProvider {
const config = runtimeConfig.worker.sms
if (String(config.provider || '').trim() === 'aliyun') {
const accessKeyId = String(config.accessKeyId || '').trim()
const accessKeySecret = String(config.accessKeySecret || '').trim()
const signName = String(config.signName || '').trim()
const templateCode = String(config.templateCode || '').trim()
if (!accessKeyId || !accessKeySecret || !signName || !templateCode) {
logWarn('[sms]', '阿里云短信配置不完整,已降级为 Mock 模式', {
hasAccessKeyId: Boolean(accessKeyId),
hasAccessKeySecret: Boolean(accessKeySecret),
hasSignName: Boolean(signName),
hasTemplateCode: Boolean(templateCode),
})
return new MockSmsProvider()
}
return new AliyunSmsProvider({ accessKeyId, accessKeySecret, signName, templateCode })
}
return new MockSmsProvider()
}
export class MockSmsProvider implements SmsProvider {
async sendCode(phone: string, code: string) {
logWarn('[sms/mock]', `【开发模式】手机验证码:${phone} → ${code}`, {
phone,
code,
hint: '生产环境请配置 WORKER_SMS_PROVIDER=aliyun 后使用真实短信',
})
}
}
export class AliyunSmsProvider implements SmsProvider {
private readonly client: AliyunSmsClient
private readonly signName: string
private readonly templateCode: string
constructor(input: {
accessKeyId: string
accessKeySecret: string
signName: string
templateCode: string
endpoint?: string
}) {
const Config = OpenApiClient.Config
this.client = new Dysmsapi.default(
new Config({
accessKeyId: input.accessKeyId,
accessKeySecret: input.accessKeySecret,
endpoint: (input.endpoint || ALIYUN_DEFAULT_ENDPOINT).replace(/^https?:\/\//, ''),
}),
) as AliyunSmsClient
this.signName = input.signName
this.templateCode = input.templateCode
}
async sendCode(phone: string, code: string) {
try {
const request = new Dysmsapi.SendSmsRequest({
phoneNumbers: phone,
signName: this.signName,
templateCode: this.templateCode,
templateParam: JSON.stringify({ code }),
})
const response = await this.client.sendSmsWithOptions(request, {})
const resultCode = String(response?.body?.code || '')
if (resultCode !== 'OK') {
throw createHttpError(
`短信发送失败:${String(response?.body?.message || resultCode || '未知错误').trim()}`,
{
statusCode: 502,
errorCode: 'worker_sms_send_failed',
},
)
}
} catch (error) {
if (error instanceof Error && 'errorCode' in error) {
throw error
}
throw createHttpError(
`短信发送失败:${error instanceof Error ? error.message : '未知错误'}`,
{
statusCode: 502,
errorCode: 'worker_sms_send_failed',
},
)
}
}
}