- 新增图像生成接口,支持试用、积分和自定义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): 添加示例系统日志文件 - 记录用户请求、验证码发送成功与失败的日志信息
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)
|