Some checks failed
Build and Deploy Backend / build-and-deploy (push) Failing after 1m36s
166 lines
4.5 KiB
Python
166 lines
4.5 KiB
Python
"""
|
|
阿里云短信服务工具类
|
|
"""
|
|
import json
|
|
import random
|
|
import logging
|
|
from datetime import timedelta
|
|
from django.conf import settings
|
|
from django.utils import timezone
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
try:
|
|
from alibabacloud_dysmsapi20170525.client import Client
|
|
from alibabacloud_tea_openapi.models import Config
|
|
from alibabacloud_tea_util.models import RuntimeOptions
|
|
from alibabacloud_dysmsapi20170525.models import SendSmsRequest
|
|
SMS_SDK_AVAILABLE = True
|
|
except ImportError:
|
|
SMS_SDK_AVAILABLE = False
|
|
|
|
|
|
class SMSClient:
|
|
"""阿里云短信客户端"""
|
|
|
|
_instance = None
|
|
|
|
def __new__(cls):
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
cls._instance._initialized = False
|
|
return cls._instance
|
|
|
|
def __init__(self):
|
|
if self._initialized:
|
|
return
|
|
|
|
if not SMS_SDK_AVAILABLE:
|
|
self.client = None
|
|
self._initialized = True
|
|
return
|
|
|
|
sms_config = settings.ALIYUN_SMS
|
|
if not sms_config.get('ACCESS_KEY_ID'):
|
|
self.client = None
|
|
self._initialized = True
|
|
return
|
|
|
|
config = Config(
|
|
access_key_id=sms_config['ACCESS_KEY_ID'],
|
|
access_key_secret=sms_config['ACCESS_KEY_SECRET'],
|
|
endpoint='dysmsapi.aliyuncs.com',
|
|
)
|
|
self.client = Client(config)
|
|
self.sign_name = sms_config['SIGN_NAME']
|
|
self.template_code = sms_config['TEMPLATE_CODE']
|
|
self._initialized = True
|
|
|
|
def send_code(self, phone, code):
|
|
"""
|
|
发送验证码短信
|
|
:param phone: 手机号
|
|
:param code: 验证码
|
|
:return: (success: bool, error_msg: str)
|
|
"""
|
|
if not self.client:
|
|
logger.warning('SMS SDK 未配置,验证码: %s -> %s', phone, code)
|
|
return True, ''
|
|
|
|
try:
|
|
request = SendSmsRequest(
|
|
phone_numbers=phone,
|
|
sign_name=self.sign_name,
|
|
template_code=self.template_code,
|
|
template_param=json.dumps({'code': code}),
|
|
)
|
|
runtime = RuntimeOptions()
|
|
response = self.client.send_sms_with_options(request, runtime)
|
|
|
|
if response.body.code == 'OK':
|
|
logger.info('短信发送成功: %s', phone)
|
|
return True, ''
|
|
else:
|
|
msg = response.body.message or response.body.code
|
|
logger.error('短信发送失败: %s, %s', phone, msg)
|
|
return False, msg
|
|
except Exception as e:
|
|
logger.error('短信发送异常: %s, %s', phone, str(e))
|
|
return False, str(e)
|
|
|
|
|
|
def get_sms_client():
|
|
"""获取短信客户端单例"""
|
|
return SMSClient()
|
|
|
|
|
|
def generate_code():
|
|
"""生成随机验证码"""
|
|
length = settings.ALIYUN_SMS.get('CODE_LENGTH', 6)
|
|
return ''.join([str(random.randint(0, 9)) for _ in range(length)])
|
|
|
|
|
|
def send_sms_code(phone):
|
|
"""
|
|
发送短信验证码的完整流程
|
|
:param phone: 手机号
|
|
:return: (success: bool, error_msg: str)
|
|
"""
|
|
from apps.users.models import SmsCode
|
|
|
|
sms_config = settings.ALIYUN_SMS
|
|
interval = sms_config.get('SEND_INTERVAL', 60)
|
|
expire_seconds = sms_config.get('CODE_EXPIRE', 300)
|
|
|
|
# 检查发送频率
|
|
recent = SmsCode.objects.filter(
|
|
phone=phone,
|
|
created_at__gte=timezone.now() - timedelta(seconds=interval),
|
|
).exists()
|
|
if recent:
|
|
return False, '验证码发送过于频繁,请稍后再试'
|
|
|
|
# 生成验证码
|
|
code = generate_code()
|
|
|
|
# 发送短信
|
|
client = get_sms_client()
|
|
ok, err = client.send_code(phone, code)
|
|
if not ok:
|
|
return False, err or '短信发送失败'
|
|
|
|
# 保存到数据库
|
|
SmsCode.objects.create(
|
|
phone=phone,
|
|
code=code,
|
|
expire_at=timezone.now() + timedelta(seconds=expire_seconds),
|
|
)
|
|
|
|
return True, ''
|
|
|
|
|
|
def verify_sms_code(phone, code):
|
|
"""
|
|
校验短信验证码
|
|
:param phone: 手机号
|
|
:param code: 验证码
|
|
:return: (valid: bool, error_msg: str)
|
|
"""
|
|
from apps.users.models import SmsCode
|
|
|
|
record = SmsCode.objects.filter(
|
|
phone=phone,
|
|
code=code,
|
|
is_used=False,
|
|
expire_at__gt=timezone.now(),
|
|
).order_by('-created_at').first()
|
|
|
|
if not record:
|
|
return False, '验证码无效或已过期'
|
|
|
|
# 标记已使用
|
|
record.is_used = True
|
|
record.save(update_fields=['is_used'])
|
|
|
|
return True, ''
|