| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124 |
- """
- 阿里云短信服务
- 提供验证码发送功能
- """
- import os
- import json
- import random
- import logging
- from typing import Optional
- logger = logging.getLogger(__name__)
- SMS_ACCESS_KEY_ID = os.getenv("SMS_ACCESS_KEY_ID", os.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID", ""))
- SMS_ACCESS_KEY_SECRET = os.getenv("SMS_ACCESS_KEY_SECRET", os.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET", ""))
- SMS_SIGN_NAME = os.getenv("SMS_SIGN_NAME", "智创空间")
- SMS_TEMPLATE_VERIFY = os.getenv("SMS_TEMPLATE_CODE_VERIFY", "") # 验证码模板
- # 验证码有效期(秒)
- CODE_TTL = 300 # 5分钟
- # 同一手机号发送间隔(秒)
- SEND_INTERVAL = 60
- def _get_client():
- from alibabacloud_dysmsapi20170525.client import Client
- from alibabacloud_tea_openapi import models as open_api_models
- config = open_api_models.Config(
- access_key_id=SMS_ACCESS_KEY_ID,
- access_key_secret=SMS_ACCESS_KEY_SECRET,
- )
- config.endpoint = "dysmsapi.aliyuncs.com"
- return Client(config)
- def send_sms(phone: str, template_code: str, params: dict) -> bool:
- """发送短信,返回是否成功"""
- try:
- from alibabacloud_dysmsapi20170525 import models as sms_models
- client = _get_client()
- request = sms_models.SendSmsRequest(
- phone_numbers=phone,
- sign_name=SMS_SIGN_NAME,
- template_code=template_code,
- template_param=json.dumps(params, ensure_ascii=False),
- )
- resp = client.send_sms(request)
- if resp.body.code == "OK":
- logger.info(f"短信发送成功: phone={phone}, template={template_code}")
- return True
- else:
- logger.warning(f"短信发送失败: phone={phone}, code={resp.body.code}, msg={resp.body.message}")
- return False
- except Exception as e:
- logger.error(f"短信发送异常: phone={phone}, error={e}")
- return False
- class SmsCodeService:
- """验证码服务(基于 Redis)"""
- REDIS_PREFIX_CODE = "sms:code:"
- REDIS_PREFIX_INTERVAL = "sms:interval:"
- def __init__(self):
- from app.core.redis import redis_manager
- self._redis_manager = redis_manager
- @property
- def redis(self):
- return self._redis_manager.get_client()
- async def send_code(self, phone: str) -> tuple[bool, str]:
- """
- 发送验证码
- 返回 (success, message)
- """
- if not self.redis:
- return False, "短信服务暂不可用"
- # 检查发送间隔
- interval_key = f"{self.REDIS_PREFIX_INTERVAL}{phone}"
- if await self.redis.exists(interval_key):
- ttl = await self.redis.ttl(interval_key)
- return False, f"请 {ttl} 秒后再试"
- # 生成6位验证码
- code = str(random.randint(100000, 999999))
- # 发送短信
- if not SMS_TEMPLATE_VERIFY:
- logger.warning("未配置验证码短信模板 SMS_TEMPLATE_CODE_VERIFY")
- # 开发模式:不实际发送,直接存储
- else:
- ok = send_sms(phone, SMS_TEMPLATE_VERIFY, {"code": code})
- if not ok:
- return False, "短信发送失败,请稍后重试"
- # 存储验证码
- code_key = f"{self.REDIS_PREFIX_CODE}{phone}"
- await self.redis.setex(code_key, CODE_TTL, code)
- # 设置发送间隔
- await self.redis.setex(interval_key, SEND_INTERVAL, "1")
- logger.info(f"验证码已发送: phone={phone}, code={code}")
- return True, "验证码已发送"
- async def verify_code(self, phone: str, code: str, delete_after: bool = True) -> bool:
- """验证验证码,验证成功后默认删除"""
- if not self.redis:
- logger.warning(f"Redis 不可用,验证码验证失败: phone={phone}")
- return False
- code_key = f"{self.REDIS_PREFIX_CODE}{phone}"
- stored = await self.redis.get(code_key)
- logger.info(f"验证码校验: phone={phone}, input={code}, stored={stored}")
- if stored and stored == code:
- if delete_after:
- await self.redis.delete(code_key)
- return True
- return False
- # 全局单例
- sms_code_service = SmsCodeService()
|