Files

169 lines
6.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
短信发送客户端(可复用)
支持:短信宝 (SMSBao)、阿里云短信。
所有发送函数接收一个 SmsConfig ORM 行对象(或具有相同字段的对象)作为配置。
设计:
- 不依赖 Web 框架,纯工具函数,供 auth / profile / token_usage / admin.settings 复用。
- 短信宝走国内直连,必须禁用代理(requests proxies={"http":None,"https":None})。
"""
import hashlib
import logging
import urllib.parse
import uuid
logger = logging.getLogger("aiflow")
# ============================================================
# 通用辅助
# ============================================================
def smsbao_md5(password: str) -> str:
"""短信宝密码:登录密码的 MD5(32位,不区分大小写)"""
return hashlib.md5(password.encode("utf-8")).hexdigest()
SMSBAO_ERRORS = {
"0": "发送成功",
"30": "密码错误",
"40": "账号不存在",
"41": "余额不足",
"43": "IP地址限制",
"50": "内容含有敏感词",
"51": "手机号码不正确",
}
def _aliyun_sign(secret: str, params: dict, method: str = "GET") -> str:
"""阿里云签名机制 (HMAC-SHA1)"""
sorted_keys = sorted(params.keys())
qs = "&".join([
f"{urllib.parse.quote(k, safe='')}={urllib.parse.quote(str(params[k]), safe='')}"
for k in sorted_keys
])
string_to_sign = f"{method}&{urllib.parse.quote('/', safe='')}&{urllib.parse.quote(qs, safe='')}"
key = f"{secret}&"
import hmac
import base64
h = hmac.new(key.encode('utf-8'), string_to_sign.encode('utf-8'), hashlib.sha1)
return base64.b64encode(h.digest()).decode('utf-8')
def _wrap_sign(config_sign: str) -> str:
"""包裹短信签名:缺少【】时自动加【】"""
sign = config_sign or ""
if "【" not in sign:
sign = f"【{sign}】" if sign else "【七虎AI】"
return sign
# ============================================================
# 核心发送
# ============================================================
def send_sms(config, phone: str, content: str):
"""
发送任意内容的短信。
:param config: SmsConfig ORM 行(需有 provider/username/password/goods_id/sign_name/api_key/api_secret/template_code)
:param phone: 接收手机号
:param content: 已包含签名的短信正文
:return: (ok: bool, message: str, provider_code: str|None)
"""
if not config or not getattr(config, "provider", None):
return False, "短信服务未配置,请联系管理员", None
provider = config.provider
# 短信宝 (SMSBao)
if provider == "短信宝":
if not config.username or not config.password:
return False, "短信宝账号未配置(缺少用户名或密码)", None
pwd_md5 = smsbao_md5(config.password)
params = {"u": config.username, "p": pwd_md5, "m": phone, "c": content}
if getattr(config, "goods_id", None):
params["g"] = config.goods_id
import requests
try:
resp = requests.get(
"https://api.smsbao.com/sms", params=params, timeout=10,
proxies={"http": None, "https": None},
)
code_str = resp.text.strip().split("\n")[0]
except Exception as e:
logger.error(f"短信宝请求失败: {e}")
return False, f"短信宝请求失败: {e}", None
if code_str == "0":
return True, "发送成功", code_str
return False, SMSBAO_ERRORS.get(code_str, f"未知错误码 {code_str}"), code_str
# 阿里云短信
if "阿里云" in provider:
if not config.api_key or not config.api_secret:
return False, "阿里云短信 AccessKey 未配置", None
sign = _wrap_sign(getattr(config, "sign_name", "") or "")
# 内容里已含验证码,阿里云模板需要 TemplateParam;此处用通用签名+内容方式不便,
# 为兼容现有配置,直接以单变量模板发送(TemplateParam 透传 content)。
from datetime import datetime as _dt
params = {
"AccessKeyId": config.api_key,
"Action": "SendSms",
"Format": "JSON",
"PhoneNumbers": phone,
"RegionId": "cn-hangzhou",
"SignName": sign.replace("【", "").replace("】", ""),
"SignatureMethod": "HMAC-SHA1",
"SignatureNonce": str(uuid.uuid4()),
"SignatureVersion": "1.0",
"TemplateCode": getattr(config, "template_code", None) or "SMS_00000001",
"TemplateParam": '{"code":"000000"}',
"Timestamp": _dt.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"),
"Version": "2017-05-25",
}
params["Signature"] = _aliyun_sign(config.api_secret, params)
import requests
try:
resp = requests.post(
"https://dysmsapi.aliyuncs.com/",
data=params,
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=10,
)
result = resp.json()
except Exception as e:
logger.error(f"阿里云短信请求失败: {e}")
return False, f"阿里云短信请求失败: {e}", None
if result.get("Code") == "OK":
return True, "发送成功", result.get("Code")
return False, result.get("Message", result.get("Code", "未知错误")), result.get("Code")
return False, f"不支持的短信服务商: {provider}", None
# ============================================================
# 业务封装
# ============================================================
_PURPOSE_TEXT = {
"register": "注册",
"reset_password": "重置密码",
"change_password": "修改密码",
"recharge": "充值",
}
def send_sms_code(config, phone: str, code: str, purpose: str):
"""发送验证码类短信。"""
purpose_text = _PURPOSE_TEXT.get(purpose, "验证")
sign = _wrap_sign(getattr(config, "sign_name", "") or "")
content = f"{sign}您的{purpose_text}验证码为{code},5分钟内有效,请勿泄露给他人。"
return send_sms(config, phone, content)
def send_sms_text(config, phone: str, text: str):
"""发送通知类短信(如充值成功、密码已修改等)。text 不含签名,自动包裹。"""
sign = _wrap_sign(getattr(config, "sign_name", "") or "")
content = f"{sign}{text}"
return send_sms(config, phone, content)