smssend/smssend/smssend.py
yumoqing bbd10a6471 fix: add mark_used parameter to check_sms_code for multi-account login flow
When multiple accounts share a phone number, the first SMS code check
should verify without consuming the code, so the second call (after
account selection) can re-verify. Default mark_used=True preserves
backward compatibility.
2026-05-29 11:31:40 +08:00

250 lines
8.2 KiB
Python
Raw 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.

# -*- coding: utf-8 -*-
"""
SMS Send Module v1.0
=====================
Features:
- 百度短信发送
- 验证码生成与验证
- 配置从文件读取
"""
import os
from functools import partial
import datetime
import random
import string
from appPublic.jsonConfig import getConfig
from baidubce.bce_client_configuration import BceClientConfiguration
from baidubce.auth.bce_credentials import BceCredentials
import baidubce.services.sms.sms_client as sms
import baidubce.exception as ex
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID as uuid
from ahserver.serverenv import ServerEnv
from sqlor.dbpools import get_sor_context
from appPublic.worker import awaitify
from appPublic.log import debug, exception, error
SMS_TEMPLATE_TABLE = {
"summary": [{"name": "sms_template", "primary": "id"}],
"fields": [
{"name": "id", "type": "str", "length": 32, "nullable": "no"},
{"name": "name", "type": "str", "length": 32},
{"name": "template_type", "type": "str", "length": 32},
{"name": "code", "type": "str", "length": 32},
{"name": "content", "type": "str", "length": 200},
{"name": "description", "type": "str", "length": 100},
{"name": "provider", "type": "str", "length": 100},
{"name": "del_flg", "type": "str", "length": 1, "default": "0"},
{"name": "create_at", "type": "timestamp"}
]
}
VALIDATE_CODE_TABLE = {
"summary": [{"name": "validatecode", "primary": "id"}],
"fields": [
{"name": "id", "type": "str", "length": 32, "nullable": "no"},
{"name": "vcode", "type": "str", "length": 32},
{"name": "expire_time", "type": "timestamp", "nullable": "no"},
{"name": "del_flg", "type": "str", "length": 1, "default": "0"},
{"name": "create_at", "type": "timestamp", "nullable": "no"}
]
}
SMS_RECORD_TABLE = {
"summary": [{"name": "sms_record", "primary": "id"}],
"fields": [
{"name": "id", "type": "str", "length": 32, "nullable": "no"},
{"name": "customerid", "type": "str", "length": 32},
{"name": "send_type", "type": "str", "length": 32},
{"name": "mobile", "type": "str", "length": 15},
{"name": "email", "type": "str", "length": 32},
{"name": "message", "type": "str", "length": 510},
{"name": "send_time", "type": "str", "length": 32},
{"name": "send_status", "type": "str", "length": 1},
{"name": "task_status", "type": "str", "length": 2},
{"name": "remark", "type": "str", "length": 200},
{"name": "del_flg", "type": "str", "length": 1},
{"name": "create_at", "type": "timestamp"}
]
}
class SMSEngine:
doc = "https://cloud.baidu.com/doc/SMS/s/zjwvxry6e"
def __init__(self):
self.access_key = os.getenv('BAIDU_SMS_ACCESS_KEY')
self.access_key_secret = os.getenv('BAIDU_SMS_ACCESS_KEY_SECRET')
self.host = os.getenv('BAIDU_SMS_HOST')
self.signature_id = os.getenv('BAIDU_SMS_SIGNATURE_ID')
# 检查必需的环境变量是否都已设置,若缺失则抛出异常
required_vars = [self.access_key, self.access_key_secret, self.host, self.signature_id]
if not all(required_vars):
missing = [name for name, val in zip(
['BAIDU_SMS_ACCESS_KEY', 'BAIDU_SMS_ACCESS_KEY_SECRET', 'BAIDU_SMS_HOST', 'BAIDU_SMS_SIGNATURE_ID'],
required_vars) if val is None]
raise EnvironmentError(f"Missing required environment variables: {', '.join(missing)}")
self.sms_client = self.create_client()
self.sms_types = {}
def create_client(self):
baiDuSmsConfig = BceClientConfiguration(
credentials=BceCredentials(self.access_key, self.access_key_secret),
endpoint=self.host
)
return sms.SmsClient(baiDuSmsConfig)
async def send(self, stype, template_id, phone, params) -> dict:
try:
f = partial(self.sms_client.send_message, signature_id=self.signature_id,
template_id=template_id,
mobile=phone,
content_var_dict=params)
resp = await awaitify(f)()
return await self.__validation(stype, template_id, params, phone, resp)
except ex.BceHttpClientError as e:
if isinstance(e.last_error, ex.BceServerError):
print('send request failed. Response %s, code: %s, request_id: %s'
% (e.last_error.status_code, e.last_error.code, e.last_error.request_id))
else:
print('send request failed. Unknown exception: %s' % e)
return {'status': False, 'msg': str(e)}
async def send_vcode(self, phone: str, stype: str, vcode) -> dict:
env = ServerEnv()
async with get_sor_context(env, 'smssend') as sor:
template_info = await sor.R('sms_template', {'name': stype, 'del_flg': '0'})
debug(f'{phone=},{stype=}, {vcode=}, {template_info=}')
if template_info:
template_id = template_info[0]['code']
else:
log = {
'id': uuid(),
'send_type': stype,
'mobile': phone,
'message': str(vcode),
'send_time': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
'send_status': '0',
'remark': f'未找到模板类型: {stype}'
}
await sor.C('sms_record', log)
return {'status': False, 'msg': '模板未配置请检查sms_template表'}
return await self.send(stype, template_id, phone, vcode)
async def __validation(self, stype, template_id, params, phone, resp) -> dict:
env = ServerEnv()
async with get_sor_context(env, 'smssend') as sor:
send_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log = {
'id': uuid(),
'send_type': stype,
'mobile': phone,
'message': str(params),
'send_time': send_time,
'send_status': '1'
}
if resp.code == '1000':
msg = f'{send_time} {phone} 百度短信发送成功code: {resp.code}'
log['send_status'] = '1'
await sor.C('sms_record', log)
return {'status': True, 'msg': msg}
else:
msg = f'{send_time} {phone} 百度短信发送失败code: {resp.code},参考文档: {self.doc}'
log['send_status'] = '0'
log['remark'] = msg
await sor.C('sms_record', log)
return {'status': False, 'msg': msg}
async def generate_sms_code(self, phone, length=None, expire_minutes=None):
length = int(length) if length is not None else 6
expire_minutes = int(expire_minutes) if expire_minutes is not None else 5
code = ''.join(random.choices(string.digits, k=length))
code_id = uuid()
expire_time = datetime.datetime.now() + datetime.timedelta(minutes=expire_minutes)
env = ServerEnv()
async with get_sor_context(env, 'smssend') as sor:
await sor.C('validatecode', {
'id': code_id,
'vcode': code,
'expire_time': expire_time,
'del_flg': '0',
'create_at': datetime.datetime.now()
})
vcode = {'SMSvCode': code}
# d = await self.send_vcode(phone, "用户注册登录验证", vcode)
d = await self.send_vcode(phone, "用户注册登录验证", vcode)
debug(f'{d=}, {code=}, {phone=}')
if d['status']:
return code_id, code
else:
return None
async def check_sms_code(self, code_id: str, vcode: str, mark_used: bool = True) -> bool:
env = ServerEnv()
async with get_sor_context(env, 'smssend') as sor:
code_info = await sor.R('validatecode', {'id': code_id})
if not code_info:
debug(f'check_sms_code():{code_id=} validatecode not found')
return False
code_info = code_info[0]
if code_info['vcode'] != vcode:
debug(f'check_sms_code():{vcode=} , {code_info["vcode"]} not match')
return False
now = datetime.datetime.now()
# 处理 code_info['expire_time'] 为 datetime 对象
if isinstance(code_info['expire_time'], str):
code_info['expire_time'] = datetime.datetime.fromisoformat(code_info['expire_time'])
if now > code_info['expire_time']:
debug(f'check_sms_code():timeout ')
return False
if mark_used:
await sor.U('validatecode', {'id': code_id, 'del_flg': '1'})
debug(f'check_sms_code(): return True ')
return True
async def send_sms(self, phone: str, stype: str, code: str) -> dict:
"""
code_id, code = await self.generate_sms_code()
if code_id is None:
return {
'status': 'error',
'data': {
'message': '生成的手机号出错'
}
}
"""
vcode = {'SMSvCode': code}
result = await self.send_vcode(phone, stype, vcode)
if result.get('status'):
return {
'status': 'ok',
'data': {
'codeid': code_id
}
}
else:
return {
'status': 'error',
'data': {
'message': result.get('msg', '发送失败')
}
}
_sms_engine = None
def get_sms_engine() -> SMSEngine:
global _sms_engine
if _sms_engine is None:
_sms_engine = SMSEngine()
return _sms_engine