133 lines
4.2 KiB
Python
133 lines
4.2 KiB
Python
"""
|
||
阿里云短信服务模块
|
||
|
||
提供短信验证码发送功能。
|
||
"""
|
||
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
|