sms_service.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. """
  2. 阿里云短信服务
  3. 提供验证码发送功能
  4. """
  5. import os
  6. import json
  7. import random
  8. import logging
  9. from typing import Optional
  10. logger = logging.getLogger(__name__)
  11. SMS_ACCESS_KEY_ID = os.getenv("SMS_ACCESS_KEY_ID", os.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID", ""))
  12. SMS_ACCESS_KEY_SECRET = os.getenv("SMS_ACCESS_KEY_SECRET", os.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET", ""))
  13. SMS_SIGN_NAME = os.getenv("SMS_SIGN_NAME", "智创空间")
  14. SMS_TEMPLATE_VERIFY = os.getenv("SMS_TEMPLATE_CODE_VERIFY", "") # 验证码模板
  15. # 验证码有效期(秒)
  16. CODE_TTL = 300 # 5分钟
  17. # 同一手机号发送间隔(秒)
  18. SEND_INTERVAL = 60
  19. def _get_client():
  20. from alibabacloud_dysmsapi20170525.client import Client
  21. from alibabacloud_tea_openapi import models as open_api_models
  22. config = open_api_models.Config(
  23. access_key_id=SMS_ACCESS_KEY_ID,
  24. access_key_secret=SMS_ACCESS_KEY_SECRET,
  25. )
  26. config.endpoint = "dysmsapi.aliyuncs.com"
  27. return Client(config)
  28. def send_sms(phone: str, template_code: str, params: dict) -> bool:
  29. """发送短信,返回是否成功"""
  30. try:
  31. from alibabacloud_dysmsapi20170525 import models as sms_models
  32. client = _get_client()
  33. request = sms_models.SendSmsRequest(
  34. phone_numbers=phone,
  35. sign_name=SMS_SIGN_NAME,
  36. template_code=template_code,
  37. template_param=json.dumps(params, ensure_ascii=False),
  38. )
  39. resp = client.send_sms(request)
  40. if resp.body.code == "OK":
  41. logger.info(f"短信发送成功: phone={phone}, template={template_code}")
  42. return True
  43. else:
  44. logger.warning(f"短信发送失败: phone={phone}, code={resp.body.code}, msg={resp.body.message}")
  45. return False
  46. except Exception as e:
  47. logger.error(f"短信发送异常: phone={phone}, error={e}")
  48. return False
  49. class SmsCodeService:
  50. """验证码服务(基于 Redis)"""
  51. REDIS_PREFIX_CODE = "sms:code:"
  52. REDIS_PREFIX_INTERVAL = "sms:interval:"
  53. def __init__(self):
  54. from app.core.redis import redis_manager
  55. self._redis_manager = redis_manager
  56. @property
  57. def redis(self):
  58. return self._redis_manager.get_client()
  59. async def send_code(self, phone: str) -> tuple[bool, str]:
  60. """
  61. 发送验证码
  62. 返回 (success, message)
  63. """
  64. if not self.redis:
  65. return False, "短信服务暂不可用"
  66. # 检查发送间隔
  67. interval_key = f"{self.REDIS_PREFIX_INTERVAL}{phone}"
  68. if await self.redis.exists(interval_key):
  69. ttl = await self.redis.ttl(interval_key)
  70. return False, f"请 {ttl} 秒后再试"
  71. # 生成6位验证码
  72. code = str(random.randint(100000, 999999))
  73. # 发送短信
  74. if not SMS_TEMPLATE_VERIFY:
  75. logger.warning("未配置验证码短信模板 SMS_TEMPLATE_CODE_VERIFY")
  76. # 开发模式:不实际发送,直接存储
  77. else:
  78. ok = send_sms(phone, SMS_TEMPLATE_VERIFY, {"code": code})
  79. if not ok:
  80. return False, "短信发送失败,请稍后重试"
  81. # 存储验证码
  82. code_key = f"{self.REDIS_PREFIX_CODE}{phone}"
  83. await self.redis.setex(code_key, CODE_TTL, code)
  84. # 设置发送间隔
  85. await self.redis.setex(interval_key, SEND_INTERVAL, "1")
  86. logger.info(f"验证码已发送: phone={phone}, code={code}")
  87. return True, "验证码已发送"
  88. async def verify_code(self, phone: str, code: str, delete_after: bool = True) -> bool:
  89. """验证验证码,验证成功后默认删除"""
  90. if not self.redis:
  91. logger.warning(f"Redis 不可用,验证码验证失败: phone={phone}")
  92. return False
  93. code_key = f"{self.REDIS_PREFIX_CODE}{phone}"
  94. stored = await self.redis.get(code_key)
  95. logger.info(f"验证码校验: phone={phone}, input={code}, stored={stored}")
  96. if stored and stored == code:
  97. if delete_after:
  98. await self.redis.delete(code_key)
  99. return True
  100. return False
  101. # 全局单例
  102. sms_code_service = SmsCodeService()