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) => unknown } const OpenApiClient = require('@alicloud/openapi-client') as { Config: new (config: Record) => unknown } type AliyunSmsClient = { sendSmsWithOptions( request: unknown, runtime: unknown, ): Promise<{ body?: { code?: string; message?: string } }> } export interface SmsProvider { sendCode(phone: string, code: string): Promise } 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', }, ) } } }