132 lines
5.4 KiB
Python
132 lines
5.4 KiB
Python
|
|
import random
|
|||
|
|
import json
|
|||
|
|
from extensions import redis_client
|
|||
|
|
from config import Config
|
|||
|
|
|
|||
|
|
# 阿里云号码认证服务 SDK
|
|||
|
|
try:
|
|||
|
|
from alibabacloud_dypnsapi20170525.client import Client as Dypnsapi20170525Client
|
|||
|
|
from alibabacloud_tea_openapi import models as open_api_models
|
|||
|
|
from alibabacloud_dypnsapi20170525 import models as dypnsapi_20170525_models
|
|||
|
|
from alibabacloud_tea_util import models as util_models
|
|||
|
|
ALIYUN_SDK_AVAILABLE = True
|
|||
|
|
except ImportError:
|
|||
|
|
ALIYUN_SDK_AVAILABLE = False
|
|||
|
|
print("⚠️ 阿里云号码认证服务 SDK 未安装,请运行: pip install alibabacloud_dypnsapi20170525")
|
|||
|
|
|
|||
|
|
class SMSService:
|
|||
|
|
@staticmethod
|
|||
|
|
def _create_client():
|
|||
|
|
"""创建号码认证服务客户端"""
|
|||
|
|
config = open_api_models.Config(
|
|||
|
|
access_key_id=Config.ALIBABA_CLOUD_ACCESS_KEY_ID,
|
|||
|
|
access_key_secret=Config.ALIBABA_CLOUD_ACCESS_KEY_SECRET
|
|||
|
|
)
|
|||
|
|
config.endpoint = 'dypnsapi.aliyuncs.com'
|
|||
|
|
return Dypnsapi20170525Client(config)
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def send_code(phone):
|
|||
|
|
# 检查是否在冷却期内
|
|||
|
|
cooldown_key = f"sms_cooldown:{phone}"
|
|||
|
|
if redis_client.exists(cooldown_key):
|
|||
|
|
ttl = redis_client.ttl(cooldown_key)
|
|||
|
|
return False, f"请{ttl}秒后再试"
|
|||
|
|
|
|||
|
|
# 开发模式:使用固定验证码
|
|||
|
|
if Config.DEV_MODE:
|
|||
|
|
code = Config.DEV_SMS_CODE
|
|||
|
|
redis_client.set(f"sms_code:{phone}", code, ex=1800)
|
|||
|
|
redis_client.set(cooldown_key, "1", ex=60)
|
|||
|
|
print(f"🟡 [开发模式] 验证码: {code} (手机: {phone})")
|
|||
|
|
return True, "验证码已发送(开发模式)"
|
|||
|
|
|
|||
|
|
# 生产模式:真实发送
|
|||
|
|
# 调用阿里云号码认证服务(自动生成验证码)
|
|||
|
|
success, biz_id = SMSService._call_aliyun(phone, None)
|
|||
|
|
|
|||
|
|
if success:
|
|||
|
|
# 发送成功后存储 BizId 用于校验
|
|||
|
|
redis_client.set(f"sms_biz_id:{phone}", biz_id, ex=1800)
|
|||
|
|
redis_client.set(cooldown_key, "1", ex=60)
|
|||
|
|
print(f"✅ 短信已发送至 {phone}, BizId: {biz_id}")
|
|||
|
|
return True, "发送成功"
|
|||
|
|
else:
|
|||
|
|
print(f"❌ 短信发送失败: {biz_id}") # biz_id 此时是错误信息
|
|||
|
|
return False, biz_id
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def verify_code(phone, code):
|
|||
|
|
"""校验验证码 - 统一使用本地 Redis 校验"""
|
|||
|
|
# 从 Redis 获取验证码
|
|||
|
|
cached_code = redis_client.get(f"sms_code:{phone}")
|
|||
|
|
|
|||
|
|
if not cached_code:
|
|||
|
|
print(f"⚠️ 未找到验证码,手机号: {phone}")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
# 比对验证码
|
|||
|
|
stored_code = cached_code.decode('utf-8') if isinstance(cached_code, bytes) else str(cached_code)
|
|||
|
|
input_code = str(code)
|
|||
|
|
|
|||
|
|
print(f"🔍 校验验证码: 输入={input_code}, 存储={stored_code}")
|
|||
|
|
|
|||
|
|
if stored_code == input_code:
|
|||
|
|
# 校验成功,删除验证码(一次性使用)
|
|||
|
|
redis_client.delete(f"sms_code:{phone}")
|
|||
|
|
redis_client.delete(f"sms_biz_id:{phone}") # 也删除 BizId
|
|||
|
|
print(f"✅ 验证码校验成功")
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
print(f"❌ 验证码不匹配")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _call_aliyun(phone, code):
|
|||
|
|
"""调用阿里云号码认证服务发送验证码"""
|
|||
|
|
if not ALIYUN_SDK_AVAILABLE:
|
|||
|
|
return False, "SDK未安装"
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
client = SMSService._create_client()
|
|||
|
|
|
|||
|
|
# 使用 SendSmsVerifyCode 接口
|
|||
|
|
request = dypnsapi_20170525_models.SendSmsVerifyCodeRequest(
|
|||
|
|
phone_number=phone,
|
|||
|
|
sign_name=Config.SMS_SIGN_NAME,
|
|||
|
|
template_code=Config.SMS_TEMPLATE_CODE,
|
|||
|
|
# 模板需要 code 和 min 两个参数
|
|||
|
|
template_param='{"code":"##code##","min":"30"}',
|
|||
|
|
code_type=1, # 1=纯数字
|
|||
|
|
code_length=6, # 6位验证码
|
|||
|
|
interval=60, # 60秒重发间隔
|
|||
|
|
valid_time=1800, # 30分钟有效期(秒)
|
|||
|
|
return_verify_code=True # 返回验证码用于本地校验
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
runtime = util_models.RuntimeOptions()
|
|||
|
|
resp = client.send_sms_verify_code_with_options(request, runtime)
|
|||
|
|
|
|||
|
|
if resp.body.code == "OK":
|
|||
|
|
# 获取阿里云生成的验证码
|
|||
|
|
verify_code = resp.body.model.verify_code if hasattr(resp.body.model, 'verify_code') else None
|
|||
|
|
biz_id = resp.body.model.biz_id
|
|||
|
|
|
|||
|
|
if verify_code:
|
|||
|
|
# 将验证码存储到 Redis 用于本地校验
|
|||
|
|
redis_client.set(f"sms_code:{phone}", verify_code, ex=1800)
|
|||
|
|
print(f"✅ 短信已发送至 {phone}, 验证码: {verify_code}, BizId: {biz_id}")
|
|||
|
|
else:
|
|||
|
|
# 如果没有返回验证码,只存储 BizId
|
|||
|
|
redis_client.set(f"sms_biz_id:{phone}", biz_id, ex=1800)
|
|||
|
|
print(f"✅ 短信已发送至 {phone}, BizId: {biz_id}")
|
|||
|
|
|
|||
|
|
return True, biz_id
|
|||
|
|
else:
|
|||
|
|
return False, resp.body.message
|
|||
|
|
except Exception as e:
|
|||
|
|
import traceback
|
|||
|
|
error_detail = traceback.format_exc()
|
|||
|
|
print(f"❌ 详细错误信息:\n{error_detail}")
|
|||
|
|
return False, str(e)
|