找回密码
 立即注册
首页 业界区 业界 在基于FastAPI的Python开发框架后端,增加阿里云短信和 ...

在基于FastAPI的Python开发框架后端,增加阿里云短信和邮件发送通知处理

役魅肋 2025-11-13 12:35:00
在一些业务系统中,整合短信和邮件通知是一种常见的处理方式,之前我在多篇随笔中介绍过基于.NET的整合处理,本篇随笔介绍基于FastAPI的Python开发框架后端,增加阿里云短信和邮件发送通知处理。
1、短信通知处理的介绍

之前我在多篇随笔中介绍过基于.NET的《SqlSugar开发框架》中整合过短信接入的内容:《使用阿里云的短信服务发送短信》、《基于SqlSugar的开发框架循序渐进介绍(17)-- 基于CSRedis实现缓存的处理》、《循序渐进VUE+Element 前端应用开发(32)--- 手机短信动态码登陆处理》。
短信通知,一般用于系统的登录,或者对重要数据变更的身份确认,各个平台都相关的短信接口,不过好像华为服务器已经不提供短信接入了,阿里云还可以,或者也可以找一些其他的短信服务商,基本上都会提供相应的接口或者SDK,对接还是很方便的。
本篇随笔基于阿里云的短信接口进行对接短信通知,也主要就是解决BS端 或者H5端的身份登录及密码修改、重置等重要处理的通知。
对于Python开发来说,基于阿里云的短信处理,可以使用它的 alibabacloud_dysmsapi20170525 SDK包来进行对接,虽然这个包时间上比较老,好像也没有看到更新的SDK了。
1.png

使用pip 命令安装了该SDK即可。
  1. pip install alibabacloud_dysmsapi20170525
复制代码
我们使用阿里云API发送短信,一般需要提供下面的身份信息。
使用阿里云的短信服务,需要注册登录自己的阿里云控制台,然后进入AccessKeys的处理界面
2.png

这里我们获取到AccessKey ID 和Access Key Secret两个关键信息,需要用在数据签名的里面的。
发送接口还需要提供下面的的一些信息,包括必要的手机号码,签名,服务器端模板代码,以及短信码等必要信息。
签名,一般为我们短信提示的公司名称,如【广州爱奇迪】这样的字样。
3.png

服务器端模板代码,阿里云默认提供了一些基础模板,我们可以从中选择。
4.png

如下具体接口请求需要提供的JSON数据。
  1. {
  2.   "phone_numbers": "13800138000",
  3.   "sign_name": "YourSignName",
  4.   "template_code": "SMS_123456789",
  5.   "template_param": {
  6.     "code": "123456"
  7.   }
  8. }
复制代码
短信真实接收到的效果如下。
5.png

 
2、在基于FastAPI的Python开发框架后端整合接口发送短信

上面了解了短信的处理大致的内容,我们就需要整合它进行短信的发送了。
首先我们为了方便,需要在项目的配置文件.env 中增加配置文件,方便统一使用。
6.png

 为了能够在多个地方使用,我们对短信发送的处理进行简单的封装一下,如下所示的辅助类,初始化的时候,提供相应的配置的参数即可。
  1. from alibabacloud_dysmsapi20170525.client import Client as Dysmsapi20170525Client
  2. from alibabacloud_tea_openapi import models as open_api_models
  3. from alibabacloud_dysmsapi20170525 import models as dysmsapi_20170525_models
  4. from alibabacloud_tea_util import models as util_models
  5. from alibabacloud_tea_util.client import Client as UtilClient
  6. from typing import List, Dict, Any
  7. class SMSHelper:
  8.     """ 阿里云短信服务工具类 """
  9.     def __init__(self, access_key_id: str, access_key_secret: str,
  10.                  sign_name: str, template_code: str, endpoint : str = 'dysmsapi.aliyuncs.com' ):
  11.         """
  12.         初始化阿里云短信服务客户端
  13.         
  14.         :param access_key_id: 阿里云访问密钥 ID
  15.         :param access_key_secret: 阿里云访问密钥 Secret
  16.         :param endpoint: 阿里云 API 网关地址
  17.         :param sign_name: 短信签名
  18.         :param template_code: 短信模板代码
  19.         """
  20.         
  21.         if not access_key_id or not access_key_secret:
  22.             raise ValueError('没有设置阿里云访问凭据,请设置环境变量或在代码中设置')
  23.         
  24.         config = open_api_models.Config(
  25.             access_key_id=access_key_id,
  26.             access_key_secret=access_key_secret,
  27.             endpoint=endpoint)
  28.         
  29.         self.client = Dysmsapi20170525Client(config)
  30.         self.sign_name = sign_name
  31.         self.template_code = template_code
复制代码
初始化后,就可以调用参数进行发送短信了
  1.     def send_sms(self, phone_numbers: str, template_param: Dict[str, Any] = None) -> Dict[str, Any]:
  2.         """
  3.         使用阿里云 SMS 服务发送短信
  4.         
  5.         Args:
  6.             phone_numbers: 短信接收号码
  7.             template_param: 短信模板参数,字典形式
  8.         
  9.         Returns:
  10.             短信发送结果,字典形式
  11.         """
  12.       
  13.         # 创建 request 对象
  14.         send_sms_request = dysmsapi_20170525_models.SendSmsRequest(
  15.             phone_numbers=phone_numbers,
  16.             sign_name= self.sign_name,
  17.             template_code=self.template_code,
  18.             template_param=str(template_param) if template_param else None
  19.         )
  20.         
  21.         response = {}
  22.         try:
  23.             # 发送短信
  24.             result = self.client.send_sms_with_options(send_sms_request, util_models.RuntimeOptions())
  25.             # 转换结果为字典形式
  26.             response = {
  27.                 'success': True,
  28.                 'message': 'SMS 发送成功',
  29.                 'request_id': result.body.request_id,
  30.                 'code': result.body.code,
  31.                 'message': result.body.message,
  32.                 'biz_id': result.body.biz_id
  33.             }
  34.         except Exception as error:
  35.             response = {
  36.                 'success': False,
  37.                 'message': str(error.message) if hasattr(error, 'message') else str(error),
  38.                 'recommend': error.data.get("Recommend") if hasattr(error, 'data') else None
  39.             }
  40.         
  41.         return response
复制代码
完成了上面的简单封装,就可以再API的控制器端进行短信处理了。
如我们在登录login的EndPoint(类似C#的控制器类)中需要先初始化短信的服务辅助类。
7.png

上面也介绍过,短信主要就是解决BS端 或者H5端的身份登录及密码修改、重置等重要处理的通知。
我们在以手机号码和短信号码登录的时候,需要先发送短信,然后短信会在服务端通过Redis的缓存驻留几分钟,这几分钟内容,通过手机和验证码即可登录,登录后验证码失效,如果超时验证码也失效。
通过手机号码发送短信的过程如下所示。
  1. @router.post(
  2.     "/send-login-smscode",
  3.     summary="发送登录验证码",
  4.     response_model=AjaxResponse[CommonResult | None],
  5. )
  6. async def send_login_smscode(
  7.     input: Annotated[PhoneCaptchaModel, Body()],
  8.     request: Request,
  9.     db: AsyncSession = Depends(get_db),
  10. ):
  11.     ip = await get_ip(request)
  12.     # 校验手机号码是否合法
  13.     if not input.phonenumber.isdigit() or len(input.phonenumber) != 11:
  14.         raise CustomExceptionBase(detail="手机号码格式不正确")
  15.     # 校验用户是否存在
  16.     user = await user_crud.get_by_column(db, "mobilephone", input.phonenumber)
  17.     if not user:
  18.         raise CustomExceptionBase(detail="用户不存在")
  19.     # 生成6位数字验证码
  20.     code = RandomUtil.random_digit_string(6)
  21.     # 发送短信验证码
  22.     res = sms_helper.send_sms(
  23.         phone_numbers=input.phonenumber,
  24.         template_param={"code": code},
  25.     )
  26.     success = res.get("success", False) == True
  27.     message = res.get("message", "")
  28.     if success:
  29.         #以手机号码作为键存储验证码缓存
  30.         cache_key = input.phonenumber.strip()
  31.         cache_item = SmsLoginCodeCacheItem(
  32.             phonenumber=input.phonenumber.strip(),
  33.             code=code,
  34.         ).model_dump()
  35.         redis_helper = RedisHelper(client =  redis_client)
  36.         await redis_helper.set(cache_key, cache_item, 60 * settings.SMS_EXPIRED_MINUTES)  # 默认5分钟过期
  37.     # 短信验证码发送结果
  38.     result = CommonResult(success=success, errormessage=message)
  39.     return AjaxResponse(result)
复制代码
上面短信发送后,号码机验证码会驻留在Redis的缓存中一段时间,那么此时如果使用手机验证码进行登录即可匹配到。
  1. @router.post(
  2.     "/authenticate-byphone",
  3.     summary="手机短信验证码登录授权处理",
  4.     response_model=AjaxResponse[AuthenticateResultDto],
  5. )
  6. async def authenticate_by_phone(
  7.     input: Annotated[PhoneCaptchaModel, Body()],
  8.     request: Request,
  9.     db: AsyncSession = Depends(get_db),
  10. ):
  11.     ip = await get_ip(request)
  12.     auth_result = AuthenticateResultDto(ip=ip, success=False)
  13.     # 从缓存中获取验证码
  14.     redis_helper = RedisHelper(client=redis_client)
  15.     cache_key = input.phonenumber.strip()
  16.    
  17.     if not cache_key.isdigit() or len(cache_key) != 11:
  18.         raise CustomExceptionBase(detail="手机号码格式不正确")
  19.    
  20.     # 校验验证码是否正确
  21.     if not input.smscode.isdigit() or len(input.smscode) != 6:
  22.         raise CustomExceptionBase(detail="验证码格式不正确")
  23.     cache_item = await redis_helper.get(cache_key)
  24.     if not cache_item:
  25.         raise CustomExceptionBase(detail="验证码已过期或不存在,请重新获取")
  26.    
  27.     cache_item = SmsLoginCodeCacheItem(**cache_item)
  28.     if cache_item.code != input.smscode:
  29.         raise CustomExceptionBase(detail="验证码不正确,请重新输入")
  30.    
  31.     # 校验用户是否存在
  32.     user = await user_crud.get_by_column(db, "mobilephone", input.phonenumber)
  33.     if not user:
  34.         raise CustomExceptionBase(detail="用户不存在")
  35.    
  36.     # 验证码正确,继续登录流程
  37.     if cache_item and user:
  38.         #获取用户角色类型
  39.         auth_result.roletype = await role_crud.get_user_roletype(
  40.             db, user.id
  41.         )  # 超级管理员、系统管理员、其他
  42.         # 根据用户身份生成tokenresult
  43.         auth_result.expires = int((datetime.utcnow() + timedelta(seconds=settings.TOKEN_EXPIRE_SECONDS)).timestamp())
  44.         auth_result.userid = user.id
  45.         auth_result.name = user.name
  46.         auth_result.success = True
  47.         auth_result.accesstoken = await generate_token(vars(user), role_type=auth_result.roletype)
  48.         #移除缓存短信键值
  49.         await redis_helper.delete(cache_key)
  50.     else:
  51.         auth_result.error = "登录失败,无法生成令牌"
  52.     return AjaxResponse(auth_result)
复制代码
其他的重置密码,修改密码,修改重要信息等通知的处理也是类似的处理过程,不在赘述。
 
3、在基于FastAPI的Python开发框架中整合邮件发送

我们通过pip命令安装fastapi_mail组件进行邮件发送的处理。
  1. pip install  fastapi_mail
复制代码
邮件发送,一般也是基于模板文件的方式,通过对模板文件的变量进行变化,实现内容的发送过程。
我们的模板路径如下所示,里面有类似下面的几个模板文件,如果需要更多场合的右键,可以进行不同的模板编写即可。
  1. #   templates/
  2. #     email/
  3. #       welcome.html
  4. #       reset_password.html
复制代码
增加一个EmailHelper.py的辅助类简单处理下,方便后续的邮件发送处理。
  1. from typing import List, Optional, Dict
  2. from fastapi.templating import Jinja2Templates
  3. from fastapi_mail import FastMail, MessageSchema, ConnectionConfig, MessageType
  4. from fastapi import BackgroundTasks
  5. from jinja2 import Environment, FileSystemLoader, select_autoescape
  6. class EmailHelper:
  7.     """ 邮件发送辅助类,基于 fastapi-mail """
  8.     def __init__(
  9.         self,
  10.         username: str,
  11.         password: str,
  12.         mail_from: str,
  13.         server: str = "smtp.example.com",
  14.         port: int = 587,
  15.         use_tls: bool = True,
  16.         use_ssl: bool = False,
  17.         template_folder: str = "app/templates/email",
  18.         enable_template_cache: bool = True,   # 开关:是否启用模板缓存
  19.     ):
  20.         self.conf = ConnectionConfig(
  21.             MAIL_USERNAME=username,
  22.             MAIL_PASSWORD=password,
  23.             MAIL_FROM=mail_from,
  24.             MAIL_PORT=port,
  25.             MAIL_SERVER=server,
  26.             MAIL_SSL_TLS=use_ssl,
  27.             MAIL_STARTTLS=True if use_tls else False,
  28.             USE_CREDENTIALS=True,
  29.         )
  30.         self.fm = FastMail(self.conf)
  31.         # 模板目录
  32.         # self.templates = Jinja2Templates(directory=template_folder)
  33.         
  34.         # 增加 模板缓存 / 热更新开关,这样在 开发环境下可以实时修改模板生效,在 生产环境下则启用缓存提升性能。
  35.         # Jinja2 环境
  36.         self.env = Environment(
  37.             loader=FileSystemLoader(template_folder),
  38.             autoescape=select_autoescape(["html", "xml"]),
  39.             cache_size=50 if enable_template_cache else 0,  # 0 = 每次重新加载模板
  40.         )
  41.     async def send_email(
  42.         self,
  43.         subject: str,
  44.         recipients: List[str],
  45.         body: str,
  46.         subtype: MessageType = MessageType.plain,
  47.         background_tasks: Optional[BackgroundTasks] = None,
  48.         attachments: Optional[List[str]] = None,
  49.     ):
  50.         """
  51.         发送邮件(支持同步调用和后台任务)
  52.         :param subject: 邮件主题
  53.         :param recipients: 收件人列表
  54.         :param body: 邮件正文
  55.         :param subtype: 内容类型(plain 或 html)
  56.         :param background_tasks: FastAPI BackgroundTasks(可选)
  57.         :param attachments: 附件路径列表(可选)
  58.         """
  59.         message = MessageSchema(
  60.             subject=subject,
  61.             recipients=recipients,
  62.             body=body,
  63.             subtype=subtype,
  64.             attachments=attachments or []   # 如果是 None,自动变成 []
  65.         )
  66.         if background_tasks:
  67.             background_tasks.add_task(self.fm.send_message, message)
  68.         else:
  69.             await self.fm.send_message(message)
  70.     async def send_template_email(
  71.         self,
  72.         subject: str,
  73.         recipients: List[str],
  74.         template_name: str,
  75.         context: Dict,
  76.         background_tasks: Optional[BackgroundTasks] = None,
  77.         attachments: Optional[List[str]] = None,
  78.     ):
  79.         """
  80.         发送基于 Jinja2 模板的邮件
  81.         :param subject: 邮件主题
  82.         :param recipients: 收件人列表
  83.         :param template_name: 模板文件名 (如 welcome.html)
  84.         :param context: 模板变量
  85.         """
  86.         # 渲染模板
  87.         # template = self.templates.get_template(template_name)
  88.         
  89.         template = self.env.get_template(template_name)
  90.         body = template.render(**context)
  91.         await self.send_email(
  92.             subject=subject,
  93.             recipients=recipients,
  94.             body=body,
  95.             subtype=MessageType.html,
  96.             background_tasks=background_tasks,
  97.             attachments=attachments or []   # 如果是 None,自动变成 []
  98.         )
复制代码
我们在一个独立的EndPoint的类中提供邮件发送的处理API。
初始化接口如下所示
8.png

两个利用邮件模板发送邮件的例子如下所示。
9.png

 
以上就是基于FastAPI的Python开发框架后端,增加阿里云短信和邮件发送通知处理的相关实现过程,希望对你有所帮助。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

4 天前

举报

喜欢鼓捣这些软件,现在用得少,谢谢分享!
您需要登录后才可以回帖 登录 | 立即注册