ai_v/services/sms_service.py

132 lines
5.4 KiB
Python
Raw Permalink Normal View History

feat(api): 实现图像生成及后台同步功能 - 新增图像生成接口,支持试用、积分和自定义API Key模式 - 实现生成图片结果异步上传至MinIO存储,带重试机制 - 优化积分预扣除和异常退还逻辑,保障用户积分准确 - 添加获取生成历史记录接口,支持时间范围和分页 - 提供本地字典配置接口,支持模型、比例、提示模板和尺寸 - 实现图片批量上传接口,支持S3兼容对象存储 feat(admin): 增加管理员角色管理与权限分配接口 - 实现角色列表查询、角色创建、更新及删除功能 - 增加权限列表查询接口 - 实现用户角色分配接口,便于统一管理用户权限 - 增加系统字典增删查改接口,支持分类过滤和排序 - 权限控制全面覆盖管理接口,保证安全访问 feat(auth): 完善用户登录注册及权限相关接口与页面 - 实现手机号验证码发送及校验功能,保障注册安全 - 支持手机号注册、登录及退出接口,集成日志记录 - 增加修改密码功能,验证原密码后更新 - 提供动态导航菜单接口,基于权限展示不同菜单 - 实现管理界面路由及日志、角色、字典管理页面访问权限控制 - 添加系统日志查询接口,支持关键词和等级筛选 feat(app): 初始化Flask应用并配置蓝图与数据库 - 创建应用程序工厂,加载配置,初始化数据库和Redis客户端 - 注册认证、API及管理员蓝图,整合路由 - 根路由渲染主页模板 - 应用上下文中自动创建数据库表,保证运行环境准备完毕 feat(database): 提供数据库创建与迁移支持脚本 - 新增数据库创建脚本,支持自动检测是否已存在 - 添加数据库表初始化脚本,支持创建和删除所有表 - 实现RBAC权限初始化,包含基础权限和角色创建 - 新增字段手动修复脚本,添加用户API Key和积分字段 - 强制迁移脚本支持清理连接和修复表结构,初始化默认数据及角色分配 feat(config): 新增系统配置参数 - 配置数据库、Redis、Session和MinIO相关参数 - 添加AI接口地址及试用Key配置 - 集成阿里云短信服务配置及开发模式相关参数 feat(extensions): 初始化数据库、Redis和MinIO客户端 - 创建全局SQLAlchemy数据库实例和Redis客户端 - 配置基于boto3的MinIO兼容S3客户端 chore(logs): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
2026-01-12 00:53:31 +08:00
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)