sms_util.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import enum
  2. import os
  3. import json
  4. from typing import Optional, Dict, Any
  5. from collections import namedtuple
  6. from functools import lru_cache
  7. from alibabacloud_credentials.client import Client as CredentialClient
  8. from alibabacloud_credentials.models import Config as CredentialConfig
  9. from alibabacloud_tea_openapi import models as open_api_models
  10. from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
  11. from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
  12. from alibabacloud_tea_util import models as util_models
  13. from pydantic import BaseModel, Field, AliasChoices
  14. from pydantic_settings import SettingsConfigDict, BaseSettings
  15. from app.config.path_conf import ENV_DIR
  16. from app.core.logger import log
  17. from app.core.exceptions import CustomException
  18. _SmsTemplate = namedtuple("_SmsTemplate", ["template_name", "template_code", "template_param_fn"])
  19. class SmsTemplateEnum(enum.Enum):
  20. VERIFICATION_CODE = _SmsTemplate(
  21. template_name="verify",
  22. template_code="SMS_333796424",
  23. template_param_fn=lambda code: f'{{"code": "{code}"}}'
  24. )
  25. @staticmethod
  26. def get_template_by_name(template_name: str) -> "_SmsTemplate":
  27. for template in SmsTemplateEnum:
  28. if template.value.template_name == template_name:
  29. return template.value
  30. raise CustomException(f"未找到模板: template_name={ template_name}")
  31. class SendSmsRequest(BaseModel):
  32. phone_numbers: str = Field(..., description="支持向不同的手机号码发送短信,手机号码之间以半角逗号(,)分隔。"
  33. "上限为 1000 个手机号码。批量发送相对于单条发送,及时性稍有延迟。"
  34. "验证码类型的短信,建议单条发送。")
  35. sign_name: str = Field(default="湖南钱程似锦技术服务", description="短信签名名称。")
  36. template_code: str = Field(..., description="短信模板 Code。")
  37. template_param: Optional[str] = Field(default=None, description="短信模板变量对应的实际值,请传入JSON 字符串。"
  38. "当您选择的模板内容含有变量时,此参数必填。参数个数应与模板内变量个数一致。")
  39. class AliyunConfig(BaseSettings):
  40. model_config = SettingsConfigDict(
  41. env_file=ENV_DIR / f".env.{os.getenv('ENVIRONMENT')}",
  42. env_file_encoding="utf-8",
  43. extra="ignore",
  44. case_sensitive=True,
  45. )
  46. access_key_id: str = Field(
  47. default=None, validation_alias=AliasChoices("ALIBABA_CLOUD_ACCESS_KEY_ID"), description="必填参数,从环境变量中获取AccessKey ID ")
  48. access_key_secret: str = Field(
  49. default=None, validation_alias=AliasChoices("ALIBABA_CLOUD_ACCESS_KEY_SECRET"), description="必填参数,从环境变量中获取AccessKey Secret")
  50. @staticmethod
  51. @lru_cache(maxsize=1)
  52. def get_credential_config() -> "AliyunConfig":
  53. """"""
  54. return AliyunConfig()
  55. class SmsSender:
  56. @classmethod
  57. def get_client(cls) -> Dysmsapi20170525Client:
  58. credentials_config = CredentialConfig(
  59. type='access_key',
  60. access_key_id=AliyunConfig.get_credential_config().access_key_id,
  61. access_key_secret=AliyunConfig.get_credential_config().access_key_secret,
  62. )
  63. credential = CredentialClient(config=credentials_config)
  64. config = open_api_models.Config(
  65. credential=credential,
  66. endpoint='dysmsapi.aliyuncs.com'
  67. )
  68. return Dysmsapi20170525Client(config)
  69. @classmethod
  70. async def send_sms(cls, sms_request: SendSmsRequest) -> bool:
  71. """
  72. 发送短信
  73. 参数:
  74. - sms_request (SendSmsRequest): 短信发送请求对象
  75. 返回:
  76. - Dict[str, Any]: 短信发送响应结果
  77. 异常:
  78. - Exception: 发送失败时抛出异常
  79. """
  80. client = SmsSender.get_client()
  81. send_sms_request = dysmsapi_20170525_models.SendSmsRequest(**sms_request.model_dump(exclude_none=True))
  82. runtime = util_models.RuntimeOptions()
  83. phone_numbers = sms_request.phone_numbers
  84. log.info(f"开始发送短信: 手机号={phone_numbers}, 模板={sms_request.template_code}, 参数={sms_request.template_param}")
  85. try:
  86. resp = await client.send_sms_with_options_async(send_sms_request, runtime)
  87. # 将响应转换为字典
  88. resp_dict = json.loads(json.dumps(resp, default=str))
  89. # 检查发送结果
  90. if hasattr(resp.body, "code") and resp.body.code == 'OK':
  91. log.info("短信发送成功: 手机号={}, BizID={}, 请求ID={}",
  92. phone_numbers, resp.body.biz_id, resp.body.request_id)
  93. return True
  94. log.warning("短信发送异常: 手机号={}, 响应={}", phone_numbers, resp_dict)
  95. raise CustomException(f"{resp.body.message}")
  96. except Exception as error:
  97. # 打印异常栈
  98. error_message = str(error.message) if hasattr(error, 'message') else str(error)
  99. raise CustomException(f"短信发送失败: {error_message}")