Skip to content

消息通知 ​

概述 ​

消息通知能力封装在后端框架层,为业务系统提供统一的短信与邮件发送入口。

短信 SMS ​

短信框架位于 app/framework/sms,采用策略模式 + 工厂模式:每种短信厂商实现一个客户端子类,屏蔽厂商 SDK 差异;工厂负责按渠道配置创建并缓存客户端,调用方只依赖统一接口。

设计核心 ​

  • SmsChannelEnum:渠道编码枚举(如 ALIYUN、TENCENT、HUAWEI、QINIU、DINGTALK),是业务层与框架层共用的单一真源,新增渠道必须在此登记,避免两处枚举值漂移。
  • SmsChannelProperties:渠道配置属性(Pydantic 模型),用于初始化具体客户端。extra="ignore" 使其可宽松接收数据库行字段。关键映射:
    • api_key → 厂商 AccessKeyId / SecretId
    • api_secret → 厂商 AccessKeySecret / SecretKey
    • signature → 短信签名
    • sms_sdk_app_id → 腾讯云等所需的独立应用 ID(其他渠道可为空)
  • SmsClient(抽象基类):策略接口,定义 send_sms、parse_sms_receive_status、get_sms_template 三个方法。调用方仅依赖此接口,不感知厂商差异。
  • AbstractSmsClient:公共逻辑模板,实现通用初始化与 build_param_set(将模板参数按指定顺序构建为有序数组,腾讯云等厂商要求 TemplateParamSet 与变量顺序一致)。
  • 结果模型:SmsSendResult(发送结果,含 success/api_code/serial_no 等)、SmsReceiveResult(回执状态)、SmsTemplateResult(模板查询)。

客户端工厂 SmsClientFactory ​

工厂为单例,维护两份缓存:

  • channel_id_clients:渠道编号 → 客户端(发送场景,编号确定)
  • channel_code_clients:渠道编码 → 客户端(回调场景,仅有编码)

常用方法:

  • create_or_update_sms_client(properties):按配置创建或更新客户端并缓存;已存在同编号客户端则更新配置后复用。
  • get_sms_client(channel_id):按渠道编号取已缓存客户端(发送场景)。
  • get_sms_client_by_code(channel_code):按渠道编码取客户端(回调场景)。
  • parse_receive_status(channel_code, text):按编码解析回执;即便该渠道从未发送过(未初始化),也会临时构造一个轻量解析实例(解析不需密钥),保证回调始终可用。

厂商实现示例 ​

厂商客户端关键要求
阿里云AliyunSmsClientsignature 必填;模板参数为 JSON 字符串,键名对应模板变量
腾讯云TencentSmsClientsms_sdk_app_id 必填;手机号自动转 E.164(默认 +86);TemplateParamSet 有序数组

业务层调用 ​

后台发送短信的入口在 app/api/v1/module_system/sms/service.py 的 send_sms:

python
# 由业务代码调用,传入手机号、模板编码、模板参数
await send_sms(db, auth, mobile, template_code, template_params)

其内部流程:

  1. 按 template_code 查询短信模板,不存在则报错;
  2. 按模板关联渠道读取渠道配置;
  3. 先创建一条 发送中(INIT) 的短信日志(记录手机号、模板内容、渲染后的正文等);
  4. 用渠道配置构造 SmsChannelProperties,经工厂 create_or_update_sms_client 拿到客户端;
  5. 调用 client.send_sms(...),将结果(send_status、API 返回码/流水号)回写日志;
  6. 接收状态(receive_status)由厂商异步回调更新,发送阶段保持 INIT。

模板内容支持 {name} / { code } 风格占位符,发送前通过 render_template 用 template_params 渲染。

新增一个短信渠道 ​

  1. 在 SmsChannelEnum 登记新编码;
  2. 在 app/framework/sms/ 新建 provider_xxx.py,实现 AbstractSmsClient 子类(至少实现 send_sms、parse_sms_receive_status);
  3. 在 factory.py 的 _CHANNEL_CLIENT_CLASS 注册 编码 → 客户端类;
  4. 如需密钥,在 SmsChannelProperties 已有字段基础上扩展(或复用现有字段)。

邮件 Email ​

邮件能力位于 app/framework/common/email.py,底层依赖 fastapi-mail,并集成 Jinja2 模板渲染。

模板与渲染 ​

模板占位符统一使用 {key} 风格(与短信一致,并和 MailTemplateService.extract_params 提取的参数名对齐):

  • render_template(template_str, variables):渲染字符串模板,仅替换 variables 中存在的参数,未匹配部分原样保留。
  • render_template_file(file_path, variables):从 common/templates/ 目录加载 .jinja2 文件模板渲染。
  • EmailUtil:静态方法代理类,便于以 EmailUtil.send_email(...) 形式调用。

注意:文件模板(welcome.jinja2)使用标准 Jinja2 语法,由 render_template_file 渲染;字符串模板使用 {var} 语法,由 render_template 渲染。两者适用场景不同。

发送邮件 ​

核心异步函数 send_email,参数涵盖 SMTP 连接信息与邮件内容:

python
await send_email(
    smtp_host=config.host,
    smtp_port=config.port,
    smtp_user=config.username,
    smtp_password=config.password,
    use_tls=config.starttls_enable,
    from_name=config.mail,
    to_email=to_mail,
    subject=title,
    body_html=content,
)

内部依据端口自动判定加密方式:465 走 SSL,其余在 use_tls 为真时启用 STARTTLS。

业务层调用 ​

邮件业务封装在 app/api/v1/module_system/mail/service.py:

  • MailAccountService.test(...):测试发送,返回 sendStatus (0 成功 / 1 失败) 与异常信息;账号维护在 system_mail_account 表。
  • MailTemplateService.send_mail(to_mails, cc_mails, bcc_mails, template_code, template_params):按模板编码渲染标题与正文,先写一条 INIT 邮件日志,再逐收件人发送,最后回写 send_status、send_time、send_exception。

邮件模板内容中的 {var} 占位符由 extract_params 自动提取为 params 列表,无需前端传参。

前端对应 ​

系统管理模块提供了完整的消息通知后台界面(路由位于 apps/web-ele/src/views/system):

  • 短信:sms/channel(渠道)、sms/template(模板)、sms/log(发送日志)
  • 邮件:mail/account(邮箱账号)、mail/template(模板)、mail/log(发送日志)
  • 接口封装:apps/web-ele/src/api/system/sms/*、apps/web-ele/src/api/system/mail/*

前端创建/更新模板时只需选择渠道或账号、填写内容,参数数组由后端按占位符自动提取;发送后可在日志页查看发送/接收状态与厂商返回的明细。