117 lines
3.8 KiB
TypeScript
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',
|
|
},
|
|
)
|
|
}
|
|
}
|
|
}
|