sms_service.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import random
  2. import string
  3. import json
  4. from app.core.cache import redis_client
  5. from app.core.config import settings
  6. # Aliyun SDK imports
  7. try:
  8. # Use dypnsapi (Number Auth) as requested
  9. from alibabacloud_dypnsapi20170525.client import Client as DypnsApiClient
  10. from alibabacloud_tea_openapi import models as open_api_models
  11. from alibabacloud_dypnsapi20170525 import models as dypns_api_models
  12. from alibabacloud_tea_util import models as util_models
  13. except ImportError:
  14. DypnsApiClient = None
  15. class SmsService:
  16. _client = None
  17. @classmethod
  18. def get_client(cls):
  19. if cls._client:
  20. return cls._client
  21. if not settings.ALIYUN_ACCESS_KEY_ID or not settings.ALIYUN_ACCESS_KEY_SECRET:
  22. raise ValueError("Aliyun AccessKey config missing")
  23. config = open_api_models.Config(
  24. access_key_id=settings.ALIYUN_ACCESS_KEY_ID,
  25. access_key_secret=settings.ALIYUN_ACCESS_KEY_SECRET
  26. )
  27. # Endpoint for dypns (SMS Authentication Service)
  28. config.endpoint = 'dypnsapi.aliyuncs.com'
  29. cls._client = DypnsApiClient(config)
  30. return cls._client
  31. @staticmethod
  32. def send_code(mobile: str) -> str:
  33. """
  34. Generates and 'sends' a code.
  35. Returns the code for dev/mock purposes (or logs it).
  36. """
  37. # 1. Check Rate Limit (1 minute)
  38. limit_key = f"SMS_LIMIT:{mobile}"
  39. if redis_client.exists(limit_key):
  40. # Using local import to avoid circular dependency if any,
  41. # though HTTPException is from fastapi which is fine.
  42. from fastapi import HTTPException
  43. raise HTTPException(status_code=400, detail="发送过于频繁,请1分钟后再试")
  44. if settings.SMS_PROVIDER == "aliyun":
  45. code = SmsService.send_aliyun_sms(mobile)
  46. else:
  47. code = SmsService.send_mock_sms(mobile)
  48. # 2. Set Rate Limit (60 seconds)
  49. redis_client.setex(limit_key, 60, "1")
  50. return code
  51. @staticmethod
  52. def send_mock_sms(mobile: str) -> str:
  53. code = ''.join(random.choices(string.digits, k=6))
  54. # Store in Redis: SMS:{mobile} -> code
  55. key = f"SMS:{mobile}"
  56. # Expire in 5 minutes
  57. redis_client.setex(key, 300, code)
  58. # In production, call actual SMS provider here.
  59. print(f"=======================================")
  60. print(f" [MOCK SMS] To: {mobile}, Code: {code}")
  61. print(f"=======================================")
  62. return code
  63. @staticmethod
  64. def send_aliyun_sms(mobile: str) -> str:
  65. if DypnsApiClient is None:
  66. print("Aliyun Dypns SDK not installed, falling back to mock")
  67. return SmsService.send_mock_sms(mobile)
  68. try:
  69. client = SmsService.get_client()
  70. # Use SendSmsVerifyCode API from SMS Authentication Service
  71. # We request the code to be returned so we can store it in Redis
  72. # TemplateParam logic:
  73. # - code: mapped to ##code## (Dypns generates this)
  74. # - min: mapped to expiration time in minutes
  75. expire_minutes = str(int(settings.CAPTCHA_EXPIRE_SECONDS / 60))
  76. template_param = json.dumps({"code": "##code##", "min": expire_minutes})
  77. request = dypns_api_models.SendSmsVerifyCodeRequest(
  78. phone_number=mobile,
  79. sign_name=settings.ALIYUN_SMS_SIGN_NAME,
  80. template_code=settings.ALIYUN_SMS_TEMPLATE_CODE,
  81. template_param=template_param,
  82. return_verify_code=True,
  83. valid_time=settings.CAPTCHA_EXPIRE_SECONDS
  84. )
  85. runtime = util_models.RuntimeOptions()
  86. response = client.send_sms_verify_code_with_options(request, runtime)
  87. if response.body.code == 'OK':
  88. # For Dypns SendSmsVerifyCode, the code is in the Model
  89. verify_code = response.body.model.verify_code
  90. request_id = response.body.request_id
  91. biz_id = response.body.model.biz_id
  92. # Store in Redis
  93. key = f"SMS:{mobile}"
  94. redis_client.setex(key, settings.CAPTCHA_EXPIRE_SECONDS, verify_code)
  95. print(f" [ALIYUN SMS AUTH] To: {mobile}, Code: {verify_code} (Sent via SendSmsVerifyCode)")
  96. print(f" [ALIYUN SMS AUTH DEBUG] RequestId: {request_id}, BizId: {biz_id}")
  97. return verify_code
  98. else:
  99. error_msg = response.body.message or "Unknown error"
  100. request_id = response.body.request_id
  101. print(f" [ALIYUN SMS ERROR] {error_msg} | RequestId: {request_id}")
  102. raise Exception(f"Aliyun SMS failed: {error_msg}")
  103. except Exception as error:
  104. print(f" [ALIYUN SMS EXCEPTION] {error}")
  105. raise error
  106. @staticmethod
  107. def verify_code(mobile: str, code: str) -> bool:
  108. key = f"SMS:{mobile}"
  109. stored_code = redis_client.get(key)
  110. if not stored_code:
  111. return False
  112. if stored_code == code:
  113. redis_client.delete(key) # One-time use
  114. return True
  115. return False