191 lines
7.7 KiB
Python
191 lines
7.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
|
||
import json
|
||
import random
|
||
import string
|
||
from typing import Dict, Any
|
||
from alibabacloud_dysmsapi20170525.client import Client as DysmsapiClient
|
||
from alibabacloud_tea_openapi import models as open_api_models
|
||
from alibabacloud_dysmsapi20170525 import models as dysmsapi_models
|
||
from alibabacloud_tea_util import models as util_models
|
||
|
||
from app.core.logger import log
|
||
from app.core.exceptions import CustomException
|
||
|
||
|
||
class SMSUtil:
|
||
"""阿里云短信服务工具类"""
|
||
|
||
def __init__(self):
|
||
"""初始化阿里云短信客户端"""
|
||
# 阿里云短信配置信息
|
||
self.access_key_id = "LTAI5t7ox6LSot4bTXQiU39R"
|
||
self.access_key_secret = "X4Z5K3ZSrZcXzcc5HgWZNmMUmTvK8N"
|
||
self.region_id = "cn-hangzhou"
|
||
self.sign_name = "山东实战派网络科技"
|
||
self.international_sign_name = "Shando Tech"
|
||
self.enable_international = True
|
||
self.domestic_endpoint = "dysmsapi.aliyuncs.com"
|
||
self.international_endpoint = "dysmsapi.aliyuncs.com"
|
||
|
||
# 短信模板配置
|
||
self.templates = {
|
||
# 国内短信模板
|
||
"register": "SMS_486390015", # 注册验证码模板
|
||
"resetpwd": "SMS_486445022", # 重置密码验证码模板
|
||
"changepwd": "SMS_486450016", # 修改密码验证码模板
|
||
"changemobile": "SMS_487250049", # 修改手机号验证码模板
|
||
"mobilelogin": "SMS_487410035", # 手机登录验证码模板
|
||
|
||
# 国际短信模板
|
||
"international_register": "SMS_INTL_486390015",
|
||
"international_resetpwd": "SMS_INTL_486445022",
|
||
"international_changepwd": "SMS_INTL_486450016",
|
||
"international_changemobile": "SMS_INTL_487250049",
|
||
"international_mobilelogin": "SMS_INTL_487410035"
|
||
}
|
||
|
||
def _create_client(self, is_international: bool = False) -> DysmsapiClient:
|
||
"""创建阿里云短信客户端"""
|
||
config = open_api_models.Config(
|
||
access_key_id=self.access_key_id,
|
||
access_key_secret=self.access_key_secret
|
||
)
|
||
|
||
# 设置访问的域名
|
||
if is_international:
|
||
config.endpoint = self.international_endpoint
|
||
else:
|
||
config.endpoint = self.domestic_endpoint
|
||
|
||
return DysmsapiClient(config)
|
||
|
||
def _is_international_mobile(self, mobile: str) -> bool:
|
||
"""判断是否为国际手机号"""
|
||
# 简单判断:中国大陆手机号以1开头,11位数字
|
||
if mobile.startswith('+'):
|
||
return True
|
||
if len(mobile) == 11 and mobile.startswith('1') and mobile.isdigit():
|
||
return False
|
||
return True
|
||
|
||
def generate_verification_code(self, length: int = 6) -> str:
|
||
"""生成验证码"""
|
||
return ''.join(random.choices(string.digits, k=length))
|
||
|
||
async def send_sms(
|
||
self,
|
||
mobile: str,
|
||
template_type: str,
|
||
template_params: Dict[str, Any] = None
|
||
) -> bool:
|
||
"""
|
||
发送短信
|
||
|
||
参数:
|
||
- mobile: 手机号
|
||
- template_type: 模板类型 (register, resetpwd, changepwd, changemobile, mobilelogin)
|
||
- template_params: 模板参数,如 {"code": "123456"}
|
||
|
||
返回:
|
||
- bool: 发送是否成功
|
||
"""
|
||
try:
|
||
# 判断是否为国际手机号
|
||
is_international = self._is_international_mobile(mobile)
|
||
|
||
# 获取对应的模板ID和签名
|
||
if is_international and self.enable_international:
|
||
template_id = self.templates.get(f"international_{template_type}")
|
||
sign_name = self.international_sign_name
|
||
else:
|
||
template_id = self.templates.get(template_type)
|
||
sign_name = self.sign_name
|
||
|
||
if not template_id:
|
||
raise CustomException(msg=f"未找到模板类型: {template_type}")
|
||
|
||
# 创建客户端
|
||
client = self._create_client(is_international)
|
||
|
||
# 构建请求
|
||
send_sms_request = dysmsapi_models.SendSmsRequest(
|
||
phone_numbers=mobile,
|
||
sign_name=sign_name,
|
||
template_code=template_id,
|
||
template_param=json.dumps(template_params) if template_params else None
|
||
)
|
||
|
||
# 发送短信
|
||
runtime = util_models.RuntimeOptions()
|
||
response = client.send_sms_with_options(send_sms_request, runtime)
|
||
|
||
# 检查响应
|
||
if response.body.code == "OK":
|
||
log.info(f"短信发送成功: {mobile}, 模板: {template_type}")
|
||
return True
|
||
else:
|
||
error_code = response.body.code
|
||
error_message = response.body.message
|
||
log.error(f"短信发送失败: {mobile}, 错误码: {error_code}, 错误信息: {error_message}")
|
||
|
||
# 根据错误码抛出不同的异常信息
|
||
if error_code == "isv.BUSINESS_LIMIT_CONTROL":
|
||
if "小时级流控" in error_message:
|
||
raise CustomException(msg="发送过于频繁,同一手机号1小时内最多发送5条短信,请稍后再试")
|
||
elif "天级流控" in error_message:
|
||
raise CustomException(msg="今日短信发送次数已达上限,请明天再试")
|
||
else:
|
||
raise CustomException(msg="短信发送频率超限,请稍后再试")
|
||
elif error_code == "isv.SMS_SIGNATURE_ILLEGAL":
|
||
raise CustomException(msg="短信签名配置错误,请联系管理员")
|
||
elif error_code == "isv.SMS_TEMPLATE_ILLEGAL":
|
||
raise CustomException(msg="短信模板配置错误,请联系管理员")
|
||
elif error_code == "isv.INVALID_PARAMETERS":
|
||
raise CustomException(msg="手机号格式错误")
|
||
elif error_code == "isv.MOBILE_NUMBER_ILLEGAL":
|
||
raise CustomException(msg="手机号格式不正确或为空号")
|
||
elif error_code == "isv.AMOUNT_NOT_ENOUGH":
|
||
raise CustomException(msg="短信余额不足,请联系管理员")
|
||
else:
|
||
raise CustomException(msg=f"短信发送失败: {error_message}")
|
||
|
||
return False
|
||
|
||
except CustomException:
|
||
# 重新抛出业务异常
|
||
raise
|
||
except Exception as e:
|
||
log.error(f"短信发送异常: {mobile}, 错误: {str(e)}")
|
||
raise CustomException(msg="短信服务异常,请稍后重试")
|
||
|
||
async def send_verification_code(
|
||
self,
|
||
mobile: str,
|
||
code_type: str = "register"
|
||
) -> tuple[bool, str]:
|
||
"""
|
||
发送验证码短信
|
||
|
||
参数:
|
||
- mobile: 手机号
|
||
- code_type: 验证码类型 (register, resetpwd, changepwd, changemobile, mobilelogin)
|
||
|
||
返回:
|
||
- tuple[bool, str]: (是否成功, 验证码)
|
||
"""
|
||
# 生成验证码
|
||
verification_code = self.generate_verification_code()
|
||
|
||
# 发送短信(如果发生异常会直接抛出,不会返回False)
|
||
try:
|
||
success = await self.send_sms(
|
||
mobile=mobile,
|
||
template_type=code_type,
|
||
template_params={"code": verification_code}
|
||
)
|
||
return success, verification_code if success else ""
|
||
except CustomException:
|
||
# 重新抛出业务异常,让上层处理
|
||
raise
|