huoyan-enterprise/backend/services/sms_service.py

133 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
阿里云短信服务模块
提供短信验证码发送功能。
"""
import random
import string
from typing import Optional
from alibabacloud_dysmsapi20170525.client import Client as DysmsapiClient
from alibabacloud_dysmsapi20170525 import models as dysmsapi_models
from alibabacloud_tea_openapi import models as open_api_models
from core.config import settings
from core.redis import RedisService
from logger.logging import get_logger
logger = get_logger(__name__)
# 验证码有效期(秒)
SMS_CODE_EXPIRE = 300 # 5分钟
# 验证码发送间隔(秒)
SMS_CODE_INTERVAL = 60 # 1分钟
class SmsService:
"""短信服务类"""
_client: Optional[DysmsapiClient] = None
@classmethod
def _get_client(cls) -> DysmsapiClient:
"""获取阿里云短信客户端"""
if cls._client is None:
config = open_api_models.Config(
access_key_id=settings.sms_access_key_id,
access_key_secret=settings.sms_access_key_secret,
)
config.endpoint = "dysmsapi.aliyuncs.com"
cls._client = DysmsapiClient(config)
return cls._client
@staticmethod
def _generate_code(length: int = 6) -> str:
"""生成随机验证码"""
return ''.join(random.choices(string.digits, k=length))
@staticmethod
def _get_code_key(phone: str, scene: str = "login") -> str:
"""获取验证码存储键"""
return f"sms:code:{scene}:{phone}"
@staticmethod
def _get_interval_key(phone: str, scene: str = "login") -> str:
"""获取发送间隔存储键"""
return f"sms:interval:{scene}:{phone}"
@classmethod
async def send_code(cls, phone: str, scene: str = "login") -> dict:
"""
发送短信验证码
Args:
phone: 手机号
scene: 场景login/register/reset
Returns:
dict: {"success": bool, "message": str}
"""
# 检查发送间隔
interval_key = cls._get_interval_key(phone, scene)
if await RedisService.exists(interval_key):
ttl = await RedisService.ttl(interval_key)
return {"success": False, "message": f"{ttl}秒后再试"}
# 生成验证码
code = cls._generate_code()
# 发送短信
try:
client = cls._get_client()
request = dysmsapi_models.SendSmsRequest(
phone_numbers=phone,
sign_name=settings.sms_sign_name,
template_code=settings.sms_template_code,
template_param=f'{{"code":"{code}"}}'
)
response = client.send_sms(request)
if response.body.code != "OK":
logger.error(f"短信发送失败: {response.body.message}")
return {"success": False, "message": "短信发送失败,请稍后重试"}
# 存储验证码
code_key = cls._get_code_key(phone, scene)
await RedisService.set(code_key, code, SMS_CODE_EXPIRE)
# 设置发送间隔
await RedisService.set(interval_key, "1", SMS_CODE_INTERVAL)
logger.info(f"短信验证码已发送: phone={phone}, scene={scene}")
return {"success": True, "message": "验证码已发送"}
except Exception as e:
logger.exception(f"短信发送异常: {e}")
return {"success": False, "message": "短信发送失败,请稍后重试"}
@classmethod
async def verify_code(cls, phone: str, code: str, scene: str = "login") -> bool:
"""
验证短信验证码
Args:
phone: 手机号
code: 验证码
scene: 场景
Returns:
bool: 验证是否成功
"""
code_key = cls._get_code_key(phone, scene)
stored_code = await RedisService.get(code_key)
if stored_code is None:
return False
if stored_code != code:
return False
# 验证成功后删除验证码
await RedisService.delete(code_key)
return True